diff --git a/.storybook/preview.ts b/.storybook/preview.ts index dba8b05..29deab7 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,15 +1,28 @@ -import type { Preview } from "@storybook/react"; -import "@/styles/globals.css"; +import type { Preview } from '@storybook/react'; +import '@/styles/globals.css'; const preview: Preview = { parameters: { - actions: { argTypesRegex: "^on[A-Z].*" }, + actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, + backgrounds: { + default: 'white', + values: [ + { + name: 'gray', + value: '#151515', + }, + { + name: 'white', + value: '#FFFFFF', + }, + ], + }, }, }; diff --git a/eslint.config.mjs b/eslint.config.mjs index 7563ea7..8b73bbd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -18,7 +18,7 @@ const compat = new FlatCompat({ const config = [ { - ignores: ['dist', 'node_modules', '.next'], + ignores: ['dist', 'node_modules', '.next', '.storybook'], }, ...compat.extends('next/core-web-vitals', 'plugin:storybook/recommended'), { @@ -60,7 +60,17 @@ const config = [ /* React */ 'react/react-in-jsx-scope': 'off', // React를 import 하지 않아도 됨 - 'no-restricted-imports': ['warn', { name: 'react' }], // react import 제한 + 'no-restricted-imports': [ + 'warn', + { + paths: [ + { + name: 'react', + importNames: ['default'], + }, + ], + }, + ], 'react/jsx-props-no-spreading': 'off', // props spreading 허용 (...props) 'react-hooks/rules-of-hooks': 'error', // Hooks 규칙 강제 'react-hooks/exhaustive-deps': 'warn', // useEffect의 의존성 배열 검사 @@ -103,7 +113,7 @@ const config = [ ], // JSX Accessibility - 'jsx-a11y/click-events-have-key-events': 'warn', // 클릭 이벤트가 있는 요소는 키보드 이벤트도 필요 (접근성) + //'jsx-a11y/click-events-have-key-events': 'warn', // 클릭 이벤트가 있는 요소는 키보드 이벤트도 필요 (접근성) 'jsx-a11y/no-static-element-interactions': 'warn', // div 등의 일반 요소에 이벤트 핸들러 사용 시 경고 // General diff --git a/src/components/.gitkeep b/src/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/common/CheckBox.stories.tsx b/src/components/common/CheckBox.stories.tsx new file mode 100644 index 0000000..e3c7f45 --- /dev/null +++ b/src/components/common/CheckBox.stories.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react'; +import { CheckBox } from './CheckBox'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: 'Components/CheckBox', + component: CheckBox, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: '체크박스 컴포넌트입니다.', + }, + }, + layout: 'centered', + }, + argTypes: { + checked: { + description: '체크박스의 선택 상태를 제어합니다.', + default: false, + control: 'boolean', + }, + disabled: { + description: '체크박스의 비활성화 상태를 제어합니다.', + default: false, + control: 'boolean', + }, + variant: { + description: '체크박스의 스타일을 설정합니다.', + default: 'gray', + control: 'radio', + options: ['gray', 'primary'], + }, + onChange: { + description: '체크박스의 상태가 변경될 때 호출되는 함수입니다.', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + checked: false, + onChange: () => {}, + }, +}; + +export const Selected_Default: Story = { + args: { + checked: true, + onChange: () => {}, + }, +}; + +export const Selected_Primary: Story = { + args: { + checked: true, + variant: 'primary', + onChange: () => {}, + }, +}; + +export const Disabled: Story = { + args: { + checked: false, + disabled: true, + onChange: () => {}, + }, +}; + +const CheckBoxGroupExample = () => { + const [selectedChecks, setSelectedChecks] = useState([]); + + const handleCheckChange = (value: string) => (isChecked: boolean) => { + setSelectedChecks((prev) => + isChecked ? [...prev, value] : prev.filter((item) => item !== value), + ); + }; + + return ( +
+
+ + 체크박스 1 +
+
+ + 체크박스 2(primary) +
+
+ + 비활성화된 체크박스 +
+
+ ); +}; + +export const Group: Story = { + render: () => , +}; diff --git a/src/components/common/CheckBox.tsx b/src/components/common/CheckBox.tsx new file mode 100644 index 0000000..4d6c37a --- /dev/null +++ b/src/components/common/CheckBox.tsx @@ -0,0 +1,81 @@ +import { cva } from 'class-variance-authority'; +import { cn } from '@/utils/cn'; + +interface CheckBoxProps { + checked: boolean; + onChange: (checked: boolean) => void; + disabled?: boolean; // default: false, 만일 비활성화 원할 시 props로 전달 + variant?: 'gray' | 'primary'; // default: gray, 만일 primary 원할 시 props로 전달 +} + +const checkboxVariants = cva('relative h-6 w-6 rounded-sm cursor-pointer border', { + variants: { + variant: { + primary: [ + 'bg-gray-600 border-gray-400', + 'data-[checked=true]:bg-purple-600', + 'data-[checked=true]:border-0', + ], + gray: ['bg-gray-600 border-gray-400'], + }, + disabled: { + true: 'bg-gray-700 border-gray-600 cursor-not-allowed', + false: '', + }, + }, + defaultVariants: { + variant: 'gray', // default: gray + disabled: false, // default: false + }, +}); + +const checkIconVariants = cva('absolute inset-0 flex items-center justify-center', { + variants: { + checked: { + false: 'scale-0 opacity-0', + true: 'scale-100 opacity-100', + }, + }, + defaultVariants: { + checked: false, + }, +}); + +export function CheckBox({ checked, disabled, variant, onChange }: CheckBoxProps) { + return ( +
!disabled && onChange(!checked)} + className={cn(checkboxVariants({ variant, disabled }))} + > +
+ {/* TODO: 추후 svg style 정립되면 이전 예정 */} + + + + + + + + + + +
+
+ ); +} + +export default CheckBox; diff --git a/src/components/common/Radio.stories.tsx b/src/components/common/Radio.stories.tsx new file mode 100644 index 0000000..51be0bf --- /dev/null +++ b/src/components/common/Radio.stories.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; +import { Radio } from './Radio'; +import type { Meta, StoryObj } from '@storybook/react'; + +const meta = { + title: 'Components/Radio', + component: Radio, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: '라디오 버튼 컴포넌트입니다.', + }, + }, + layout: 'centered', + }, + argTypes: { + checked: { + description: '라디오 버튼의 선택 상태를 제어합니다.', + defaultValue: false, + control: 'boolean', + }, + onChange: { + description: '라디오 버튼의 상태가 변경될 때 호출되는 함수입니다.', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + checked: false, + onChange: () => {}, + }, +}; + +export const Checked: Story = { + args: { + checked: true, + onChange: () => {}, + }, +}; + +export const Unchecked: Story = { + args: { + checked: false, + onChange: () => {}, + }, +}; + +const RadioGroup = () => { + const [selected, setSelected] = useState('1'); + + return ( +
+
+ setSelected('1')} /> + 옵션 1 +
+
+ setSelected('2')} /> + 옵션 2 +
+
+ setSelected('3')} /> + 옵션 3 +
+
+ ); +}; + +export const Group: Story = { + render: () => , +}; diff --git a/src/components/common/Radio.tsx b/src/components/common/Radio.tsx new file mode 100644 index 0000000..3a92c8d --- /dev/null +++ b/src/components/common/Radio.tsx @@ -0,0 +1,37 @@ +import { cva } from 'class-variance-authority'; +import { cn } from '@/utils/cn'; + +interface RadioProps { + checked: boolean; + onChange: (checked: boolean) => void; +} + +const indicatorVariants = cva( + 'absolute left-1/2 top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-gray-0', + { + variants: { + checked: { + true: 'scale-100 bg-gray-0', + false: 'scale-0 bg-transparent', + }, + }, + defaultVariants: { + checked: false, + }, + }, +); + +export function Radio({ checked, onChange }: RadioProps) { + return ( +
onChange(!checked)} + className={'relative h-6 w-6 cursor-pointer rounded-full bg-gray-600'} + > +
+
+ ); +} + +export default Radio; diff --git a/src/components/common/check.svg b/src/components/common/check.svg new file mode 100644 index 0000000..42a740c --- /dev/null +++ b/src/components/common/check.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 936377f..49d4168 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -9,6 +9,7 @@ const Home = () => {
+
diff --git a/src/pages/test-taeryong/index.tsx b/src/pages/test-taeryong/index.tsx new file mode 100644 index 0000000..3c888d4 --- /dev/null +++ b/src/pages/test-taeryong/index.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import CheckBox from '@/components/common/CheckBox'; +import Radio from '@/components/common/Radio'; + +export default function TestTaeryong() { + const [selectedRadio, setSelectedRadio] = useState('option1'); + const [selectedChecks, setSelectedChecks] = useState([]); + + const handleCheckChange = (value: string) => { + setSelectedChecks((prev) => + prev.includes(value) ? prev.filter((item) => item !== value) : [...prev, value], + ); + }; + + return ( +
+
+

라디오 버튼 테스트

+ +
+
+ setSelectedRadio('option1')} + /> + 옵션 1 +
+ +
+ setSelectedRadio('option2')} + /> + 옵션 2 +
+ +
+ handleCheckChange('check1')} + /> + 회색 체크박스 1 +
+
+ +
선택된 라디오: {selectedRadio}
+ +

체크박스 테스트

+ +
+
+ handleCheckChange('check1')} + /> + 회색 체크박스 1 +
+ +
+ handleCheckChange('check2')} + /> + 회색 체크박스2 +
+ +
+ handleCheckChange('check3')} + /> + 보라색 체크박스 +
+ +
+ handleCheckChange('check4')} + /> + 비활성화 +
+
+ +
+
+            선택된 체크박스: {JSON.stringify(selectedChecks, null, 2)}
+          
+
+
+
+ ); +}