diff --git a/src/components/todo-card/TodoCard.stories.tsx b/src/components/todo-card/TodoCard.stories.tsx new file mode 100644 index 0000000..f051a8e --- /dev/null +++ b/src/components/todo-card/TodoCard.stories.tsx @@ -0,0 +1,116 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; +import type { ComponentProps } from 'react'; + +import { useState } from 'react'; +import { fn } from 'storybook/test'; + +import TodoCard from './TodoCard'; +import type { TodoItem } from './types/types'; + +const sampleItems: TodoItem[] = [ + { id: '1', text: '법인 설립 안내 드리기', checked: false }, + { id: '2', text: '법인 설립 혹은 변경 등기 비용 안내 드리기', checked: false }, + { id: '3', text: '입력해주신 정보를 바탕으로 등기신청서 제...', checked: true }, +]; + +const completedItems: TodoItem[] = [ + { id: '1', text: '법인 설립 안내 드리기', checked: true }, + { id: '2', text: '법인 설립 혹은 변경 등기 비용 안내 드리기', checked: true }, + { id: '3', text: '입력해주신 정보를 바탕으로 등기신청서 제...', checked: true }, +]; + +const meta = { + title: 'Components/TodoCard', + component: TodoCard, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + args: { + title: '법인 설립', + items: sampleItems, + onKebabClick: fn(), + }, + argTypes: { + expanded: { control: 'boolean' }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const ControlledTodoCard = (args: ComponentProps) => { + const [items, setItems] = useState(args.items); + + const handleCheckedChange = (id: string, checked: boolean) => { + setItems((prev) => prev.map((item) => (item.id === id ? { ...item, checked } : item))); + }; + + return ; +}; + +export const Default: Story = { + render: (args) => , +}; + +export const AllCompleted: Story = { + render: (args) => , + args: { + items: completedItems, + }, +}; + +export const Collapsed: Story = { + render: (args) => , + args: { + expanded: false, + }, +}; + +export const CollapsedCompleted: Story = { + render: (args) => , + args: { + items: completedItems, + expanded: false, + }, +}; + +export const Overview: Story = { + render: (args) => { + const [items1, setItems1] = useState(sampleItems); + const [items2, setItems2] = useState(completedItems); + + return ( +
+ + setItems1((prev) => prev.map((item) => (item.id === id ? { ...item, checked } : item))) + } + /> + + setItems2((prev) => prev.map((item) => (item.id === id ? { ...item, checked } : item))) + } + /> + + +
+ ); + }, + parameters: { + controls: { disable: true }, + }, +}; diff --git a/src/components/todo-card/TodoCard.tsx b/src/components/todo-card/TodoCard.tsx new file mode 100644 index 0000000..f52657f --- /dev/null +++ b/src/components/todo-card/TodoCard.tsx @@ -0,0 +1,61 @@ +'use client'; + +import clsx from 'clsx'; +import Image from 'next/image'; + +import Badge from '@/components/badge/Badge'; +import CheckBox from '@/components/checkbox/CheckBox'; + +import styles from './styles/TodoCard.module.css'; +import { TODO_CARD_ICONS } from './constants/todoCardConstants'; +import type { TodoCardProps } from './types/types'; + +/** + * 할일 카드 컴포넌트. + * 제목, 진행 상태 뱃지, 체크박스 리스트를 포함합니다. + * expanded가 false이면 헤더만 표시됩니다. + */ +export default function TodoCard({ + title, + items, + onItemCheckedChange, + onKebabClick, + expanded = true, + className, +}: TodoCardProps) { + const checkedCount = items.filter((item) => item.checked).length; + const totalCount = items.length; + const badgeLabel = `${checkedCount}/${totalCount}`; + const badgeState = checkedCount === totalCount && totalCount > 0 ? 'done' : 'ongoing'; + + return ( +
+
+ {title} + + +
+ + {expanded && items.length > 0 && ( +
+ {items.map((item) => ( +
+ {item.text}} + onCheckedChange={ + onItemCheckedChange + ? (checked) => onItemCheckedChange(item.id, checked) + : undefined + } + /> +
+ ))} +
+ )} +
+ ); +} diff --git a/src/components/todo-card/constants/todoCardConstants.ts b/src/components/todo-card/constants/todoCardConstants.ts new file mode 100644 index 0000000..de08e04 --- /dev/null +++ b/src/components/todo-card/constants/todoCardConstants.ts @@ -0,0 +1,5 @@ +import kebabSmall from '@/assets/icons/kebab/kebabSmall.svg'; + +export const TODO_CARD_ICONS = { + kebab: kebabSmall, +} as const; diff --git a/src/components/todo-card/index.ts b/src/components/todo-card/index.ts new file mode 100644 index 0000000..a5756de --- /dev/null +++ b/src/components/todo-card/index.ts @@ -0,0 +1,2 @@ +export { default as TodoCard } from './TodoCard'; +export type { TodoCardProps, TodoItem } from './types/types'; diff --git a/src/components/todo-card/styles/TodoCard.module.css b/src/components/todo-card/styles/TodoCard.module.css new file mode 100644 index 0000000..933cafc --- /dev/null +++ b/src/components/todo-card/styles/TodoCard.module.css @@ -0,0 +1,73 @@ +.card { + display: flex; + flex-direction: column; + gap: 16px; + padding: 12px; + border-radius: 12px; + background-color: var(--color-background-inverse); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.06), + 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.header { + display: flex; + align-items: center; + gap: 8px; +} + +.title { + flex: 1; + min-width: 0; + font-size: 14px; + font-weight: 600; + line-height: 1.4; + color: var(--color-text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kebab { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + background: none; + border-radius: 4px; + cursor: pointer; +} + +.kebab:hover { + background-color: var(--color-background-tertiary); +} + +.body { + display: flex; + flex-direction: column; + gap: 6px; +} + +.item { + display: flex; + align-items: center; +} + +.itemLabel { + font-size: 12px; + font-weight: 400; + line-height: 1.4; + color: var(--color-text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.itemChecked .itemLabel { + color: var(--color-text-disabled); + text-decoration: line-through; +} diff --git a/src/components/todo-card/types/types.ts b/src/components/todo-card/types/types.ts new file mode 100644 index 0000000..45e1fa7 --- /dev/null +++ b/src/components/todo-card/types/types.ts @@ -0,0 +1,23 @@ +export type TodoItem = { + /** 항목 고유 ID */ + id: string; + /** 항목 텍스트 */ + text: string; + /** 체크 여부 */ + checked: boolean; +}; + +export type TodoCardProps = { + /** 카드 제목 */ + title: string; + /** 체크박스 항목 목록 */ + items: TodoItem[]; + /** 항목 체크 상태 변경 시 호출되는 콜백 */ + onItemCheckedChange?: (id: string, checked: boolean) => void; + /** 케밥(⋮) 버튼 클릭 시 호출되는 콜백 */ + onKebabClick?: () => void; + /** 체크리스트 펼침 여부 (기본값: true) */ + expanded?: boolean; + /** 추가 CSS 클래스 */ + className?: string; +};