diff --git a/.github/pull_request_template.yml b/.github/PULL_REQUEST_TEMPLATE/solo-template.yml
similarity index 100%
rename from .github/pull_request_template.yml
rename to .github/PULL_REQUEST_TEMPLATE/solo-template.yml
diff --git a/src/assets/check-mark.svg b/src/assets/check-mark.svg
new file mode 100644
index 0000000..542b426
--- /dev/null
+++ b/src/assets/check-mark.svg
@@ -0,0 +1,26 @@
+
diff --git a/src/componets/Button/Button.scss b/src/componets/Button/Button.scss
index c80e690..5107506 100644
--- a/src/componets/Button/Button.scss
+++ b/src/componets/Button/Button.scss
@@ -1,17 +1,16 @@
-@use '../../styles/index' as v;
+@use '../../styles/index' as s;
+@use 'sass:map';
-.xp-btn {
+.btn {
background: #d4d4d4;
- color: #5a5a5a;
+ @include s.text-style-extended(lg, 400, gray-900);
border: none;
- font-family: 'Tahoma', 'Verdana', sans-serif;
- font-size: 16px;
padding: 6px 18px;
cursor: pointer;
box-shadow:
- inset 1px 1px 0 0 #fff,
- 1px 1px 0 0 #868686,
- 2px 2px 0 0 #222;
+ inset 1px 1px 0 0 s.color(white),
+ 1px 1px 0 0 s.color(gray-600),
+ 2px 2px 0 0 s.color(gray-900);
transition:
background-color 0.12s,
color 0.12s,
@@ -19,19 +18,19 @@
&:hover,
&:focus {
- background: #e5e5e5;
- color: #222;
+ background: s.color(gray-300);
+ color: s.color(gray-900);
box-shadow:
- inset 1px 1px 0 0 #fff,
- 2px 2px 0 0 #222;
+ inset 1px 1px 0 0 s.color(white),
+ 2px 2px 0 0 s.color(gray-900);
}
&:active {
background: #bbbbbb;
- color: #222;
+ color: s.color(gray-900);
box-shadow:
- inset 1px 1px 0 0 #fff,
+ inset 1px 1px 0 0 s.color(white),
1px 1px 0 0 #868686,
- 0px 0px 0 0 #222;
+ 0px 0px 0 0 s.color(gray-900);
}
@media (max-width: 640px) {
@@ -41,30 +40,30 @@
}
// 빨강 버튼 (secondary)
-.xp-btn.secondary {
- background: #d43c3c;
- color: #fff;
+.btn.secondary {
+ background: s.color(red_btn);
+ color: s.color(white);
border: none;
box-shadow:
- inset 1px 1px 0 0 #fff,
- 1px 1px 0 0 #b92a2a,
- 2px 2px 0 0 #7a1818;
+ inset 1px 1px 0 0 s.color(white),
+ 1px 1px 0 0 s.color(red_btn_hover),
+ 2px 2px 0 0 s.color(red_btn_shadow);
&:hover,
&:focus {
- background: #b92a2a;
- color: #fff;
+ background: s.color(red_btn_hover);
+ color: s.color(white);
box-shadow:
- inset 1px 1px 0 0 #fff,
- 2px 2px 0 0 #7a1818;
+ inset 1px 1px 0 0 s.color(white),
+ 2px 2px 0 0 s.color(red_btn_shadow);
}
&:active {
- background: #a12222;
- color: #fff;
+ background: s.color(red_btn_active);
+ color: s.color(white);
box-shadow:
- inset 1px 1px 0 0 #fff,
- 1px 1px 0 0 #b92a2a,
- 0px 0px 0 0 #7a1818;
+ inset 1px 1px 0 0 s.color(white),
+ 1px 1px 0 0 s.color(red_btn_hover),
+ 0px 0px 0 0 s.color(red_btn_shadow);
}
@media (max-width: 640px) {
diff --git a/src/componets/Button/Button.stories.tsx b/src/componets/Button/Button.stories.tsx
index 1179b75..1c30925 100644
--- a/src/componets/Button/Button.stories.tsx
+++ b/src/componets/Button/Button.stories.tsx
@@ -12,8 +12,16 @@ type Story = StoryObj;
export const Primary: Story = {
args: {
- children: '클릭',
+ children: 'Button',
variant: 'default',
color: 'rgba(255, 0, 0, 1)'
}
};
+
+export const Secondary: Story = {
+ args: {
+ children: 'Button',
+ variant: 'secondary',
+ color: 'rgba(255, 0, 0, 1)'
+ }
+};
diff --git a/src/componets/Button/Button.test.tsx b/src/componets/Button/Button.test.tsx
index f979c16..73371da 100644
--- a/src/componets/Button/Button.test.tsx
+++ b/src/componets/Button/Button.test.tsx
@@ -1,12 +1,13 @@
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { test, expect, vi } from "vitest";
-import Button from "./Button";
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { test, expect, vi } from 'vitest';
-test("renders 버튼과 클릭", async () => {
+import Button from './Button';
+
+test('renders 버튼과 클릭', async () => {
const onClick = vi.fn();
render();
- const btn = screen.getByRole("button", { name: /클릭/i });
+ const btn = screen.getByRole('button', { name: /클릭/i });
await userEvent.click(btn);
expect(onClick).toHaveBeenCalled();
});
diff --git a/src/componets/Button/Button.tsx b/src/componets/Button/Button.tsx
index 3beca5d..5266b30 100644
--- a/src/componets/Button/Button.tsx
+++ b/src/componets/Button/Button.tsx
@@ -1,17 +1,18 @@
import React from 'react';
import './Button.scss';
-export type ButtonProps = React.ButtonHTMLAttributes & {
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes {
variant?: 'default' | 'secondary';
-};
+}
const Button: React.FC = ({
children,
variant = 'default',
...rest
}) => {
- // xp-btn, secondary 클래스 동적 조합
- const classNames = ['xp-btn', variant === 'secondary' ? 'secondary' : '']
+ // btn, secondary 클래스 동적 조합
+ const classNames = ['btn', variant !== 'default' && variant]
.filter(Boolean)
.join(' ');
diff --git a/src/componets/Checkbox/Checkbox.scss b/src/componets/Checkbox/Checkbox.scss
new file mode 100644
index 0000000..bece911
--- /dev/null
+++ b/src/componets/Checkbox/Checkbox.scss
@@ -0,0 +1,55 @@
+@use '../../styles/index' as s;
+@use 'sass:map';
+
+.checkbox {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ position: relative;
+ width: fit-content;
+ height: fit-content;
+}
+
+.checkbox_png {
+ position: absolute;
+ left: 3px;
+ top: -5px;
+ width: 20px;
+ height: 20px;
+ background: url('../../assets/check-mark.svg') center center no-repeat;
+ background-size: contain;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.2s;
+ z-index: 3;
+ @media (max-width: 640px) {
+ width: 16px;
+ height: 16px;
+ left: 4px;
+ top: -2px;
+ }
+}
+
+.checkbox_input {
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ border-radius: 0;
+ border: 2px solid s.color(gray-400);
+ background: s.color(white);
+ cursor: pointer;
+ transition:
+ background-color 0.2s,
+ border-color 0.2s;
+
+ &:checked {
+ ~ .checkbox_png {
+ opacity: 1;
+ }
+ }
+
+ @media (max-width: 640px) {
+ width: 16px;
+ height: 16px;
+ }
+}
diff --git a/src/componets/Checkbox/Checkbox.stories.tsx b/src/componets/Checkbox/Checkbox.stories.tsx
new file mode 100644
index 0000000..2a3cf79
--- /dev/null
+++ b/src/componets/Checkbox/Checkbox.stories.tsx
@@ -0,0 +1,24 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import Checkbox from './Checkbox';
+
+const meta: Meta = {
+ title: 'Components/Checkbox',
+ component: Checkbox,
+ argTypes: { onClick: { action: 'changed' } }
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ label: 'Checkbox Default'
+ }
+};
+
+export const Secondary: Story = {
+ args: {
+ label: 'Checkbox Checked',
+ checked: true
+ }
+};
diff --git a/src/componets/Checkbox/Checkbox.test.tsx b/src/componets/Checkbox/Checkbox.test.tsx
new file mode 100644
index 0000000..bc8640d
--- /dev/null
+++ b/src/componets/Checkbox/Checkbox.test.tsx
@@ -0,0 +1,15 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { test, expect, vi } from 'vitest';
+
+import Checkbox from './Checkbox';
+
+test('체크박스 렌더링과 체크 이벤트', async () => {
+ const onChange = vi.fn();
+ render();
+ const checkbox = screen.getByRole('checkbox', { name: /동의/i });
+ expect(checkbox).not.toBeChecked();
+ await userEvent.click(checkbox);
+ expect(checkbox).toBeChecked();
+ expect(onChange).toHaveBeenCalled();
+});
diff --git a/src/componets/Checkbox/Checkbox.tsx b/src/componets/Checkbox/Checkbox.tsx
new file mode 100644
index 0000000..62bea45
--- /dev/null
+++ b/src/componets/Checkbox/Checkbox.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import './Checkbox.scss';
+
+export interface CheckboxProps
+ extends React.InputHTMLAttributes {
+ label: string;
+}
+
+const Checkbox: React.FC = ({ label, ...rest }) => {
+ const reactId = React.useId();
+ const id = rest.id ?? reactId;
+
+ return (
+
+ );
+};
+export default Checkbox;
diff --git a/src/styles/_functions.scss b/src/styles/_functions.scss
new file mode 100644
index 0000000..0fd84b3
--- /dev/null
+++ b/src/styles/_functions.scss
@@ -0,0 +1,10 @@
+@use 'sass:map';
+@use 'variables' as v;
+
+@function color($name) {
+ $val: map.get(v.$colors, $name);
+ @if $val == null {
+ @error "Unknown color token: #{$name}";
+ }
+ @return $val;
+}
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
index 175dd12..b85b052 100644
--- a/src/styles/_mixins.scss
+++ b/src/styles/_mixins.scss
@@ -1,22 +1,36 @@
+@use 'sass:map';
@use 'variables' as v;
+@use 'functions' as f;
-// 반응형 미디어 쿼리 믹스인
+// 반응형 미디어 쿼리
@mixin mq($breakpoint) {
- @if map-has-key(v.$breakpoints, $breakpoint) {
- @media (min-width: map-get(v.$breakpoints, $breakpoint)) {
+ @if map.has-key(v.$breakpoints, $breakpoint) {
+ @media (min-width: map.get(v.$breakpoints, $breakpoint)) {
@content;
}
}
}
-// 타이포그래피 믹스인
+// 기본 타이포
@mixin text-style($size, $weight: 400) {
- font-size: map-get(map-get(v.$typography, $size), fontSize);
- line-height: map-get(map-get(v.$typography, $size), lineHeight);
+ $scale: map.get(v.$typography, $size);
+ @if $scale == null {
+ @error "Unknown typography scale: #{$size}";
+ }
+ font-size: map.get($scale, fontSize);
+ line-height: map.get($scale, lineHeight);
font-weight: $weight;
}
-// flex 믹스인
+// 확장 타이포 (색상)
+@mixin text-style-extended($size, $weight: 400, $color: null) {
+ @include text-style($size, $weight);
+ @if $color != null {
+ color: f.color($color);
+ }
+}
+
+// flex
@mixin flex-box($justify: center, $align: center, $direction: row) {
display: flex;
flex-direction: $direction;
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index 86ae1ca..78f5b1c 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -6,9 +6,16 @@ $colors: (
gray-300: #e0e0e0,
gray-400: #bdbdbd,
gray-500: #9e9e9e,
+ gray-600: #757575,
+ gray-700: #616161,
+ gray-800: #424242,
gray-900: #212121,
+
+ // 빨강 버튼
red_btn: #d43c3c,
- red_btn_hover: #b92a2a
+ red_btn_active: #a12222,
+ red_btn_hover: #b92a2a,
+ red_btn_shadow: #7a1818
);
// 타이포그래피 변수
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 38f91cd..e5bf6e6 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -1,2 +1,3 @@
-@forward "variables";
-@forward "mixins";
+@forward 'variables';
+@forward 'functions';
+@forward 'mixins';