From 45793367dc9771cd0699bfe783e7b6edfeba9a30 Mon Sep 17 00:00:00 2001 From: kimminna Date: Tue, 30 Jun 2026 00:35:35 +0900 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20design=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=A0=95=EB=B9=84=20=EB=B0=8F=20Storybook=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 +- README.md | 2 +- README_ko.md | 2 +- docs/architecture/components.md | 2 +- docs/design/figma.md | 16 +-- docs/design/storybook.md | 182 ++++++++++++++++++++++++++++++++ docs/design/tokens.md | 118 ++++++++++++++------- 7 files changed, 269 insertions(+), 57 deletions(-) create mode 100644 docs/design/storybook.md diff --git a/AGENTS.md b/AGENTS.md index 1739f0d5..ac4c3c76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ Claude Code / Codex 공통 진입점입니다. - Product: Timo 클라이언트 - 도구: Claude Code, Codex -- 스택: Next.js 16 (App Router) · TypeScript · Tailwind CSS · Zustand · TanStack React Query · pnpm · Turborepo +- 스택: Next.js 16 (App Router) · TypeScript · Tailwind CSS · Zustand · TanStack React Query · pnpm · Turborepo · Storybook --- @@ -67,4 +67,4 @@ Claude Code / Codex 공통 진입점입니다. | ------------------------------------------ | -------------------- | | 커밋·브랜치·이슈·코드 스타일·네이밍 컨벤션 | `docs/conventions/` | | 기술 스택·컴포넌트 계층·상태 전략·스캐폴딩 | `docs/architecture/` | -| 디자인 토큰·피그마 MCP 연동 | `docs/design/` | +| 디자인 토큰·피그마 MCP 연동·스토리북 | `docs/design/` | diff --git a/README.md b/README.md index a8c78d69..88b5757b 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ Timo is optimized for desktop use — designed for students and job seekers who | ----------------------------------------------------- | ------------------------------------------ | | Conventions (commit · branch · code style · naming) | [docs/conventions/](./docs/conventions/) | | Architecture (stack · structure · components · state) | [docs/architecture/](./docs/architecture/) | -| Design (tokens · Figma) | [docs/design/](./docs/design/) | +| Design (tokens · Figma · Storybook) | [docs/design/](./docs/design/) | ## AI Collaboration diff --git a/README_ko.md b/README_ko.md index 40254027..05c32b76 100644 --- a/README_ko.md +++ b/README_ko.md @@ -132,7 +132,7 @@ Timo는 데스크탑 환경에서 장시간 자기주도적 작업을 수행하 | --------------------------------------- | ------------------------------------------ | | 컨벤션 (커밋·브랜치·코드 스타일·네이밍) | [docs/conventions/](./docs/conventions/) | | 아키텍처 (스택·구조·컴포넌트·상태 전략) | [docs/architecture/](./docs/architecture/) | -| 디자인 (토큰·피그마 연동) | [docs/design/](./docs/design/) | +| 디자인 (토큰·피그마·스토리북) | [docs/design/](./docs/design/) | ## AI 협업 diff --git a/docs/architecture/components.md b/docs/architecture/components.md index 0bf99e54..98a066e3 100644 --- a/docs/architecture/components.md +++ b/docs/architecture/components.md @@ -21,7 +21,7 @@ app/ auth/ _components/ # LoginForm, SocialLoginButton 등 순수 UI _containers/ # LoginFormContainer (useQuery·zustand 결합) - _queries/ # useLoginMutation, useUserProfileQuery 등 + _queries/ # use-login-mutation, use-user-profile-query 등 page.tsx # 레이아웃 조합만 (Server Component) layout.tsx ``` diff --git a/docs/design/figma.md b/docs/design/figma.md index cc130ea7..7374c22e 100644 --- a/docs/design/figma.md +++ b/docs/design/figma.md @@ -3,7 +3,6 @@ ## 개요 Claude Code의 Figma MCP를 통해 피그마 디자인을 직접 참조하면서 컴포넌트를 구현합니다. -`timo-figma` 스킬이 이 문서를 참조합니다. ## 설정 @@ -15,7 +14,7 @@ Claude Code의 Figma MCP를 통해 피그마 디자인을 직접 참조하면서 ### 2. 환경 변수 설정 -`.env.local`에 토큰을 추가한다 (커밋하지 않는다): +`.env.local`에 추가한다 (커밋 금지): ```text FIGMA_API_KEY=your_personal_access_token @@ -23,7 +22,7 @@ FIGMA_API_KEY=your_personal_access_token ### 3. MCP 서버 등록 -프로젝트 루트 `.mcp.json` 또는 `~/.claude/settings.json`에 추가: +`~/.claude/settings.json`에 추가: ```json { @@ -43,16 +42,7 @@ FIGMA_API_KEY=your_personal_access_token Claude Code 세션에서 피그마 파일 URL을 공유하면 MCP가 자동으로 디자인 데이터를 로드한다. -## 사용 방법 - -`timo-figma` 스킬 실행 시 피그마 URL을 함께 전달한다. -MCP를 통해 아래 정보를 자동 추출한다: - -- 컴포넌트 스펙 (크기·색상·폰트·간격) -- 디자인 토큰 매핑 (`docs/design/tokens.md` 참조) -- 컴포넌트 계층 구조 및 variant 목록 - ## 주의사항 - Personal Access Token은 절대 커밋하지 않는다 -- `.mcp.json`을 `.gitignore`에 추가하거나 환경 변수 방식을 사용한다 +- 피그마 값이 `docs/design/tokens.md` 토큰 표에 없으면 hex를 하드코딩하지 않고 토큰 추가 여부를 먼저 확인한다 diff --git a/docs/design/storybook.md b/docs/design/storybook.md new file mode 100644 index 00000000..eb3044e7 --- /dev/null +++ b/docs/design/storybook.md @@ -0,0 +1,182 @@ +# Storybook 컨벤션 + +Storybook **8.6** 기반. `@storybook/react-vite` 프레임워크 사용. + +## 설치된 Addon + +| Addon | 역할 | +| -------------------- | --------------------------------------------------- | +| `addon-essentials` | Controls, Actions, Backgrounds, Viewport, Docs 통합 | +| `addon-interactions` | `play` 함수로 인터랙션 시나리오 실행 | +| `addon-a11y` | 접근성 자동 검사 (Addons 패널 → Accessibility 탭) | +| `addon-links` | Story 간 링크 연결 | + +--- + +## 파일 위치 + +Story 파일은 컴포넌트와 **같은 폴더**에 둔다. + +``` +src/components/ + tag/ + Tag.tsx + Tag.stories.tsx + priority-icon/ + PriorityIcon.tsx + PriorityIcon.stories.tsx +``` + +--- + +## CSF(Component Story Format) 기본 구조 + +```tsx +import { ComponentName } from "./ComponentName"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Components/ComponentName", + component: ComponentName, + parameters: { + layout: "centered", + }, + argTypes: { + propName: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { propName: "value" }, +}; +``` + +- `satisfies Meta` — 타입 추론을 유지하면서 meta를 검증한다 +- `type Story = StoryObj` — meta에서 타입을 추론해 Story 타입을 정의한다 + +--- + +## title 네이밍 + +Storybook 사이드바 계층 구조를 `title`로 제어한다. +프로젝트의 storySort 순서: `Guides → Tokens → Components → Icons` + +| 컨텐츠 유형 | title 형식 | 예시 | +| ----------- | --------------------- | -------------------------- | +| 가이드 문서 | `"Guides/{이름}"` | `"Guides/Storybook Guide"` | +| 토큰 전시 | `"Tokens/{이름}"` | `"Tokens/Color"` | +| UI 컴포넌트 | `"Components/{이름}"` | `"Components/Tag"` | +| 아이콘 | `"Icons/{이름}"` | `"Icons/ChevronIcon"` | + +--- + +## layout 선택 + +| 상황 | layout | +| ------------------ | ------------ | +| 작은 단일 컴포넌트 | `"centered"` | +| 목록·넓은 컨텐츠 | `"padded"` | + +전역 기본값은 `preview.ts`에서 `"centered"`로 설정되어 있다. + +--- + +## argTypes + +Controls 패널에 표시되는 prop 조작 UI를 정의한다. + +```tsx +argTypes: { + // 선택형 (유니언 타입 variant) + priority: { + control: "select", + options: ["매우중요", "중요", "보통", "낮음", "Disable"], + }, + // 텍스트 입력 + text: { + control: "text", + description: "태그에 표시될 텍스트", + }, + // 색상 피커 + color: { control: "color" }, + // 불리언 토글 + disabled: { control: "boolean" }, + // 숫자 슬라이더 + size: { control: "number" }, +} +``` + +`preview.ts`에 `actions: { argTypesRegex: "^on[A-Z].*" }` 설정이 있어 `on`으로 시작하는 prop은 자동으로 action 로깅된다. + +### table 옵션 (Docs 탭 테이블 제어) + +```tsx +argTypes: { + label: { + description: "버튼에 표시되는 텍스트", + table: { + type: { summary: "string" }, + defaultValue: { summary: "확인" }, + category: "Content", + }, + control: "text", + }, +} +``` + +--- + +## autodocs + +`main.ts`에 `docs: { autodocs: "tag" }` 설정 → meta에 `tags: ["autodocs"]`를 추가하면 Docs 탭이 자동 생성된다. + +```tsx +const meta = { + title: "Components/Button", + component: Button, + tags: ["autodocs"], +} satisfies Meta; +``` + +--- + +## play 함수 (addon-interactions) + +인터랙션 시나리오를 코드로 작성해 Canvas에서 자동 실행한다. + +```tsx +import { userEvent, within, expect } from "@storybook/test"; + +export const Filled: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(canvas.getByRole("textbox"), "입력값"); + await expect(canvas.getByRole("textbox")).toHaveValue("입력값"); + }, +}; +``` + +버튼 클릭, 폼 입력 등 상태 변화가 있는 컴포넌트에 활용한다. + +--- + +## 배경·뷰포트 + +`preview.ts`에 전역 설정이 되어 있다. + +**배경**: white / gray(`#f5f5f5`) / dark(`#1f1f1f`) +**뷰포트**: Mobile(375px) / Tablet(768px) / Desktop(1280px) + +Story별로 기본값을 덮어쓸 수 있다: + +```tsx +export const DarkMode: Story = { + parameters: { + backgrounds: { default: "dark" }, + viewport: { defaultViewport: "mobile" }, + }, +}; +``` diff --git a/docs/design/tokens.md b/docs/design/tokens.md index 2200f4f6..6bf73c27 100644 --- a/docs/design/tokens.md +++ b/docs/design/tokens.md @@ -6,49 +6,89 @@ ## 네이밍 규칙 - Tailwind CSS 클래스명과 1:1 대응을 원칙으로 한다 -- CSS 변수명: `--timo-{category}-{scale}` +- CSS 변수명: `--color-timo-{category}-{scale}` - 컴포넌트 내에서는 Tailwind 클래스를 직접 사용하고, 커스텀 값은 `tailwind.config`에 등록한다 ## Color - +소스: `packages/tailwind-config/tokens/colors.css` +Tailwind 클래스 사용 예: `bg-timo-blue-50`, `text-timo-gray-900` + +### Blue + +| 토큰 | CSS 변수 | 값 | +| --------------- | ----------------------- | --------- | +| `timo-blue-50` | `--color-timo-blue-50` | `#f1f6fe` | +| `timo-blue-65` | `--color-timo-blue-65` | `#dee9ff` | +| `timo-blue-75` | `--color-timo-blue-75` | `#c6d8fc` | +| `timo-blue-100` | `--color-timo-blue-100` | `#aec8fb` | +| `timo-blue-200` | `--color-timo-blue-200` | `#8cb0f9` | +| `timo-blue-300` | `--color-timo-blue-300` | `#74a0f8` | + +### Gray + +| 토큰 | CSS 변수 | 값 | +| --------------- | ----------------------- | --------- | +| `timo-gray-300` | `--color-timo-gray-300` | `#f5f5f5` | +| `timo-gray-500` | `--color-timo-gray-500` | `#e6e8e8` | +| `timo-gray-600` | `--color-timo-gray-600` | `#d8d8d8` | +| `timo-gray-700` | `--color-timo-gray-700` | `#9f9f9f` | +| `timo-gray-800` | `--color-timo-gray-800` | `#757575` | +| `timo-gray-900` | `--color-timo-gray-900` | `#3f3f3f` | +| `timo-gray` | `--color-timo-gray` | `#bebebe` | + +### Accent + +| 토큰 | CSS 변수 | 값 | +| ----------------- | ------------------------- | --------- | +| `timo-yellow-300` | `--color-timo-yellow-300` | `#f2fc9f` | +| `timo-red` | `--color-timo-red` | `#ff6650` | +| `timo-orange` | `--color-timo-orange` | `#ffb157` | + +### Base + +| 토큰 | CSS 변수 | 값 | +| ------------ | -------------------- | --------- | +| `timo-black` | `--color-timo-black` | `#121212` | ## Typography - - -## Spacing - - - -## Border Radius - - +소스: `packages/tailwind-config/tokens/typography.css` +Tailwind 클래스 사용 예: `typo-headline-b-24`, `typo-body-r-12` +모든 토큰의 `line-height: 1.5`, `letter-spacing: -0.03em`은 공통 적용됩니다. + +웨이트 약어: `b` = Bold(700) · `sb` = SemiBold(600) · `m` = Medium(500) · `r` = Regular(400) + +### Headline + +| 토큰 | 클래스 | 크기 | 굵기 | +| --------------- | -------------------- | ---- | ---- | +| `headline-b-50` | `typo-headline-b-50` | 50px | 700 | +| `headline-b-30` | `typo-headline-b-30` | 30px | 700 | +| `headline-m-26` | `typo-headline-m-26` | 26px | 500 | +| `headline-b-24` | `typo-headline-b-24` | 24px | 700 | +| `headline-b-22` | `typo-headline-b-22` | 22px | 700 | +| `headline-b-20` | `typo-headline-b-20` | 20px | 700 | +| `headline-m-20` | `typo-headline-m-20` | 20px | 500 | +| `headline-b-18` | `typo-headline-b-18` | 18px | 700 | +| `headline-b-16` | `typo-headline-b-16` | 16px | 700 | +| `headline-m-16` | `typo-headline-m-16` | 16px | 500 | +| `headline-b-14` | `typo-headline-b-14` | 14px | 700 | +| `headline-m-14` | `typo-headline-m-14` | 14px | 500 | +| `headline-r-14` | `typo-headline-r-14` | 14px | 400 | + +### Body + +| 토큰 | 클래스 | 크기 | 굵기 | +| ------------ | ----------------- | ---- | ---- | +| `body-b-12` | `typo-body-b-12` | 12px | 700 | +| `body-sb-12` | `typo-body-sb-12` | 12px | 600 | +| `body-m-12` | `typo-body-m-12` | 12px | 500 | +| `body-r-12` | `typo-body-r-12` | 12px | 400 | +| `body-sb-11` | `typo-body-sb-11` | 11px | 600 | + +### Caption + +| 토큰 | 클래스 | 크기 | 굵기 | +| -------------- | ------------------- | ---- | ---- | +| `caption-r-10` | `typo-caption-r-10` | 10px | 400 | From d48ac884ca73dcbc9c964d0ed4732cd3016abfc9 Mon Sep 17 00:00:00 2001 From: kimminna Date: Tue, 30 Jun 2026 00:35:52 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20AI=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=A4=ED=82=AC=C2=B7=EA=B0=80=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=C2=B7=EB=A6=AC=EB=B7=B0=20=EA=B7=9C=EC=B9=99=20=EC=A0=95?= =?UTF-8?q?=EB=B9=84=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/ui/timo-figma/SKILL.md | 169 ++++++++++++++-- .agents/skills/ui/timo-storybook/SKILL.md | 164 +++++++++++++-- .coderabbit.yaml | 2 +- .../src/guides/StorybookGuide.mdx | 191 ++++++++++++++---- 4 files changed, 459 insertions(+), 67 deletions(-) diff --git a/.agents/skills/ui/timo-figma/SKILL.md b/.agents/skills/ui/timo-figma/SKILL.md index eea66d71..94d47ba7 100644 --- a/.agents/skills/ui/timo-figma/SKILL.md +++ b/.agents/skills/ui/timo-figma/SKILL.md @@ -5,32 +5,171 @@ ## 트리거 - "피그마에서 컴포넌트 뽑아줘" +- "피그마 링크 줄게, 이거 구현해줘" - 피그마 링크 또는 노드 ID를 제공받은 경우 ## 참조 -- `docs/design/tokens.md` → 디자인 토큰 -- `docs/design/figma.md` → 피그마 MCP 연동 방식 -- `docs/conventions/code-style.md` → 컴포넌트 규칙 +- `docs/design/tokens.md` → 색상·타이포 토큰 목록 및 클래스명 +- `docs/design/figma.md` → MCP 설정 방법 +- `docs/architecture/components.md` → 컴포넌트 계층 +- `docs/architecture/structure.md` → 모노레포 구조 +- `docs/conventions/code-style.md` → 컴포넌트 구조·네이밍 규칙 -## 워크플로우 +--- -### Phase 1 — 피그마 노드 읽기 +## Phase 1 — 피그마 노드 읽기 -피그마 MCP를 통해 대상 프레임/컴포넌트 스펙을 읽는다. +아래 기준으로 MCP 도구를 선택한다: -- 크기, 패딩, 색상, 타이포그래피, 간격 +| 목적 | 도구 | +| -------------------------------------- | ---------------------- | +| 컴포넌트 크기·색상·폰트·간격 전체 읽기 | `get_design_context` | +| 레이아웃을 시각적으로 확인 | `get_screenshot` | +| 피그마 변수(토큰) 목록 확인 | `get_variable_defs` | +| 컴포넌트명으로 검색 | `search_design_system` | +| SVG·이미지 에셋 추출 | `download_assets` | -### Phase 2 — 디자인 토큰 매핑 +`get_design_context`로 아래 값을 수집한다: -읽어온 값을 `docs/design/tokens.md` 의 토큰에 매핑한다. -토큰에 없는 값이 있으면 사용자에게 토큰 추가 여부 확인한다. +- **크기**: width, height +- **색상**: fill hex, text hex +- **타이포**: font-size, font-weight +- **간격**: padding, gap +- **radius**: border-radius +- **variant 목록**: 피그마 component set의 variant 이름 -### Phase 3 — 컴포넌트 생성 +--- -`timo-component` 워크플로우를 따라 컴포넌트 생성. -피그마 스펙을 Tailwind 클래스로 변환한다. +## Phase 2 — 디자인 토큰 매핑 -### Phase 4 — Story 생성 +`docs/design/tokens.md`를 읽어 수집한 값을 Tailwind 클래스로 변환한다. -`timo-storybook` 워크플로우를 따라 Story 파일을 함께 생성한다. +**색상**: Color 표에서 hex를 찾아 `bg-timo-{token}` / `text-timo-{token}` 형태로 변환한다. +표에 없는 hex가 나오면 → 사용자에게 "토큰에 없는 값입니다. 추가할까요?" 확인 후 진행. + +**타이포**: Typography 표에서 font-size + font-weight 조합을 찾아 `typo-{token}` 클래스로 변환한다. + +**크기·간격**: Tailwind 기본 단위 **1 = 4px**. `px ÷ 4`로 수치를 구한다. +4px 단위로 떨어지지 않으면 `h-[{n}px]`, `w-[{n}px]` arbitrary value를 사용한다. + +**Border Radius**: + +| 피그마 값 | 클래스 | +| ---------- | --------------- | +| 4px | `rounded-[4px]` | +| 8px | `rounded-lg` | +| 12px | `rounded-xl` | +| 50% / 원형 | `rounded-full` | + +--- + +## Phase 3 — 컴포넌트 위치 결정 + +피그마 컴포넌트가 어느 계층에 속하는지 판단한다. + +| 판단 기준 | 위치 | +| ----------------------------------- | ---------------------------------------------------- | +| 여러 앱·패키지에서 공유하는 순수 UI | `packages/timo-design-system/src/components/{name}/` | +| timo-web 전역에서 공유하는 순수 UI | `apps/timo-web/components/` | +| 특정 도메인에서만 쓰이는 순수 UI | `apps/timo-web/app/(domain)/_components/` | +| 특정 도메인 + 상태·쿼리 결합 | `apps/timo-web/app/(domain)/_containers/` | + +**순수 UI 판단**: `'use client'` 없이도 동작 가능하면 `_components`. useQuery·zustand·useState가 필요하면 `_containers`. + +--- + +## Phase 4 — 컴포넌트 구현 + +### 공통 규칙 (`docs/conventions/code-style.md`) + +- Arrow function + Named export: `export const {Name} = (props: {Name}Props) => {}` +- Props 타입은 `interface {Name}Props`로 정의, `any` 금지 +- Tailwind 클래스만 사용, 인라인 style 금지 +- 조건부 클래스는 `cn()` 유틸 사용 + +### variant가 있는 경우 — Record 패턴 + +```tsx +import { cn } from "../../lib"; + +export type Priority = "매우중요" | "중요" | "보통" | "낮음" | "Disable"; + +const PRIORITY_COLOR: Record = { + 매우중요: "bg-timo-red", + 중요: "bg-timo-orange", + 보통: "bg-timo-gray-600", + 낮음: "bg-timo-black", + Disable: "bg-timo-gray-500", +}; + +export interface PriorityIconProps { + priority: Priority; +} + +export const PriorityIcon = ({ priority = "매우중요" }: PriorityIconProps) => { + return ( +
+ ); +}; +``` + +### variant가 없는 경우 — 인라인 클래스 + +```tsx +export interface TagProps { + text: string; +} + +export const Tag = ({ text = "과제" }: TagProps) => { + return ( +
+ + {text} + +
+ ); +}; +``` + +### design-system 컴포넌트 추가 작업 + +`packages/timo-design-system`에 추가하는 경우에만 아래를 추가로 수행한다: + +**파일 구조**: 컴포넌트별 폴더로 분리 + +``` +packages/timo-design-system/src/components/ + {name}/ + {Name}.tsx + {Name}.stories.tsx +``` + +**index.ts re-export 등록**: + +```ts +// packages/timo-design-system/src/components/index.ts +export { {Name} } from "./{name}/{Name}"; +``` + +--- + +## Phase 5 — Story 작성 (design-system 한정) + +`timo-storybook` 워크플로우를 따른다. design-system 컴포넌트가 아니면 생략한다. + +- variant가 있으면 각 variant마다 Story export 추가 +- `argTypes`에 props Control 연동 +- 위치: `{Name}.stories.tsx` (컴포넌트와 같은 폴더) + +--- + +## 자가 검토 + +- [ ] 피그마 hex가 토큰 클래스로 변환되고 하드코딩되지 않았는가 +- [ ] variant별 클래스가 Record로 분리되어 있는가 +- [ ] `_components` vs `_containers` 구분이 올바른가 +- [ ] design-system이면 `index.ts` re-export가 추가되었는가 +- [ ] `any` 미사용, ESLint 오류 없음 diff --git a/.agents/skills/ui/timo-storybook/SKILL.md b/.agents/skills/ui/timo-storybook/SKILL.md index eff8d458..7812346b 100644 --- a/.agents/skills/ui/timo-storybook/SKILL.md +++ b/.agents/skills/ui/timo-storybook/SKILL.md @@ -7,29 +7,161 @@ ## 참조 -- 대상 컴포넌트의 `XxxProps` 인터페이스 +- 대상 컴포넌트의 Props 인터페이스 +- `docs/design/storybook.md` → Story 작성 컨벤션 (패턴 A/B/C, argTypes, autodocs, play 함수) - `docs/conventions/naming.md` → 파일 네이밍 -## 워크플로우 +--- -### Phase 1 — 컴포넌트 분석 +## Phase 1 — 컴포넌트 분석 -대상 컴포넌트를 읽어 props 인터페이스와 사용 패턴 파악한다. +대상 컴포넌트를 읽어 아래를 파악한다: -### Phase 2 — Story 구성 계획 +- props 인터페이스와 각 prop의 타입 +- variant/열거형 prop이 있는지 (유니언 타입, Record 패턴) +- 단순 string/boolean prop인지 +- 여러 항목을 목록으로 전시하는 컴포넌트인지 (Color, Typography 등 토큰 전시용) -- `Default`: 기본 상태 -- 주요 props 변형마다 Story 추가 (`Primary`, `Disabled`, `WithIcon` 등) -- 인터랙티브 요소가 있으면 `play` 함수로 동작 시나리오 추가 +--- -### Phase 3 — Story 파일 생성 +## Phase 2 — 패턴 선택 -- 위치: 컴포넌트와 동일 폴더 (`Xxx.stories.tsx`) -- `argTypes` 로 Controls 패널 연동 -- a11y addon 설정 포함 +컴포넌트 성격에 따라 아래 세 패턴 중 하나를 선택한다. -### Phase 4 — 자가 검토 +### 패턴 A — variant별 개별 Story (PriorityIcon 참조) -- [ ] 필수 props가 Controls에 노출됨 -- [ ] 빈 상태·에러 상태 등 엣지케이스 Story 포함 -- [ ] 컴포넌트 description 작성 +variant(유니언 타입) prop이 있는 컴포넌트. variant마다 Story를 따로 export한다. + +```tsx +import { PriorityIcon } from "./PriorityIcon"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Components/PriorityIcon", + component: PriorityIcon, + parameters: { + layout: "centered", + }, + argTypes: { + priority: { + control: "select", + options: ["매우중요", "중요", "보통", "낮음", "Disable"], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const 매우중요: Story = { args: { priority: "매우중요" } }; +export const 중요: Story = { args: { priority: "중요" } }; +export const 보통: Story = { args: { priority: "보통" } }; +export const 낮음: Story = { args: { priority: "낮음" } }; +export const Disable: Story = { args: { priority: "Disable" } }; +``` + +### 패턴 B — Default 단일 Story (Tag 참조) + +string/boolean 등 자유 입력 prop을 받는 컴포넌트. Controls에서 직접 수정한다. + +```tsx +import { Tag } from "./Tag"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Components/Tag", + component: Tag, + parameters: { + layout: "centered", + }, + argTypes: { + text: { + control: "text", + description: "태그에 표시될 텍스트", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { text: "과제" }, +}; +``` + +### 패턴 C — render() 전시 Story (Color, Typography 참조) + +토큰·목록을 전체 나열하는 전시용 컴포넌트. `component`를 meta에서 생략하고 `render()`로 직접 렌더링한다. + +```tsx +import { Color } from "./Color"; +import { COLOR_TOKENS } from "../../tokens/color-token"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Tokens/Color", + parameters: { + layout: "padded", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const All: Story = { + name: "All Colors", + render: () => ( +
+ {COLOR_TOKENS.map((token) => ( + + ))} +
+ ), +}; +``` + +--- + +## Phase 3 — 세부 규칙 + +### title 네이밍 + +| 컴포넌트 유형 | title | +| -------------------------- | --------------------- | +| 일반 UI 컴포넌트 | `"Components/{Name}"` | +| 토큰 전시 (색상·타이포 등) | `"Tokens/{Name}"` | + +### layout 선택 + +| 상황 | layout | +| ------------------ | ------------ | +| 작은 단일 컴포넌트 | `"centered"` | +| 목록·넓은 컨텐츠 | `"padded"` | + +### argTypes control 선택 + +| prop 타입 | control | +| --------------------- | ----------------------------- | +| 유니언 타입 (variant) | `"select"` + `options: [...]` | +| string | `"text"` | +| boolean | `"boolean"` | +| number | `"number"` | + +### Story export 이름 + +- 패턴 A: variant 값 그대로 export (`export const 매우중요`, `export const Disable`) +- 패턴 B: `Default` +- 패턴 C: 목적을 나타내는 이름 (`All`, `Scale` 등) + +--- + +## Phase 4 — 자가 검토 + +- [ ] `satisfies Meta` 형식 사용 (패턴 C는 `satisfies Meta`) +- [ ] `type Story = StoryObj` 선언 +- [ ] 파일 위치: 컴포넌트와 동일 폴더 (`{Name}.stories.tsx`) +- [ ] variant 있는 컴포넌트는 모든 variant에 Story가 있는가 +- [ ] Controls 패널에서 props가 조작 가능한가 diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 12f2782d..73848b31 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -45,7 +45,7 @@ reviews: - 컴포넌트는 반드시 Arrow function + Named export: `export const Component = () => {}` - `default export`는 `page.tsx`, `layout.tsx` 등 Next.js 필수 파일에만 허용 - 자식 없으면 self-closing: `` - - 최상단 래퍼는 Fragment: `<>…` + - 불필요한 div 래핑 금지 — 컴포넌트 자체가 시각적 컨테이너(배경색·크기·border-radius 등 스타일을 정의)인 경우는 div 허용, 그 외 단순 그룹핑 목적이면 Fragment `<>…` 사용 ### Import - 절대 경로 import 사용 (상대 경로 `../../` 지양) diff --git a/packages/timo-design-system/src/guides/StorybookGuide.mdx b/packages/timo-design-system/src/guides/StorybookGuide.mdx index 6d8b37ec..ea928c96 100644 --- a/packages/timo-design-system/src/guides/StorybookGuide.mdx +++ b/packages/timo-design-system/src/guides/StorybookGuide.mdx @@ -6,69 +6,190 @@ import { Meta } from "@storybook/addon-docs/blocks"; Timo Design System의 공통 UI 컴포넌트는 Storybook을 통해 상태별 예시와 사용 방법을 문서화합니다. -## Story 위치 +--- -컴포넌트 story는 컴포넌트와 같은 폴더에 작성합니다. +## 파일 위치 -```txt -components/ - Button/ - Button.tsx - Button.stories.tsx - index.ts +Story 파일은 컴포넌트와 **같은 폴더**에 둡니다. + +``` +src/components/ + tag/ + Tag.tsx + Tag.stories.tsx ``` -## 파일 네이밍 +--- + +## 사이드바 구조 + +`title`로 사이드바 카테고리를 제어합니다. 프로젝트 정렬 순서는 다음과 같습니다: + +| 카테고리 | title 형식 | 용도 | +|---------|-----------|------| +| Guides | `"Guides/{이름}"` | 가이드 문서 | +| Tokens | `"Tokens/{이름}"` | 색상·타이포 토큰 전시 | +| Components | `"Components/{이름}"` | UI 컴포넌트 | +| Icons | `"Icons/{이름}"` | 아이콘 | + +--- + +## Story 작성 패턴 -Story 파일은 `ComponentName.stories.tsx` 형식을 사용합니다. +### 패턴 A — variant별 개별 Story + +유니언 타입 variant prop이 있는 컴포넌트. variant마다 Story를 따로 export합니다. + +```tsx +import { PriorityIcon } from "./PriorityIcon"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Components/PriorityIcon", + component: PriorityIcon, + parameters: { layout: "centered" }, + argTypes: { + priority: { + control: "select", + options: ["매우중요", "중요", "보통", "낮음", "Disable"], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; -```txt -Button.stories.tsx -Input.stories.tsx +export const 매우중요: Story = { args: { priority: "매우중요" } }; +export const 중요: Story = { args: { priority: "중요" } }; +export const 보통: Story = { args: { priority: "보통" } }; +export const 낮음: Story = { args: { priority: "낮음" } }; +export const Disable: Story = { args: { priority: "Disable" } }; ``` -## 기본 Story 작성 예시 +### 패턴 B — Default 단일 Story + +string 등 자유 입력 prop을 받는 컴포넌트. Controls 패널에서 직접 수정합니다. ```tsx -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Button } from "./Button"; +import { Tag } from "./Tag"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Components/Tag", + component: Tag, + parameters: { layout: "centered" }, + argTypes: { + text: { + control: "text", + description: "태그에 표시될 텍스트", + }, + }, +} satisfies Meta; -const meta: Meta = { - title: "Components/Button", - component: Button, - tags: ["autodocs"], +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { text: "과제" }, }; +``` -export default meta; +### 패턴 C — render() 전시 Story -type Story = StoryObj; +색상·타이포그래피 등 토큰을 전체 나열하는 전시용. `component`를 meta에서 생략하고 `render()`로 직접 렌더링합니다. -export const Default: Story = { - args: { - children: "Button", - }, +```tsx +import { Color } from "./Color"; +import { COLOR_TOKENS } from "../../tokens/color-token"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Tokens/Color", + parameters: { layout: "padded" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const All: Story = { + name: "All Colors", + render: () => ( +
+ {COLOR_TOKENS.map((token) => ( + + ))} +
+ ), }; ``` +--- + +## argTypes control 종류 + +| prop 타입 | control | +|-----------|---------| +| 유니언 타입 (variant) | `"select"` + `options: [...]` | +| string | `"text"` | +| boolean | `"boolean"` | +| number | `"number"` | +| 색상 | `"color"` | + +--- + ## Autodocs -컴포넌트 문서를 자동 생성하려면 story meta에 `tags: ["autodocs"]`를 추가합니다. +`tags: ["autodocs"]`를 meta에 추가하면 Docs 탭이 자동 생성됩니다. ```tsx -const meta: Meta = { +const meta = { title: "Components/Button", component: Button, tags: ["autodocs"], +} satisfies Meta; +``` + +--- + +## play 함수 — 인터랙션 테스트 + +버튼 클릭, 폼 입력 등 상태 변화가 있는 컴포넌트에 사용합니다. + +```tsx +import { userEvent, within, expect } from "@storybook/test"; + +export const Filled: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(canvas.getByRole("textbox"), "입력값"); + await expect(canvas.getByRole("textbox")).toHaveValue("입력값"); + }, }; ``` -## Accessibility +--- + +## 배경·뷰포트 전환 + +Toolbar에서 배경과 뷰포트를 전환할 수 있습니다. + +**배경**: white / gray / dark +**뷰포트**: Mobile(375px) / Tablet(768px) / Desktop(1280px) + +Story별 기본값 지정: + +```tsx +export const DarkMobile: Story = { + parameters: { + backgrounds: { default: "dark" }, + viewport: { defaultViewport: "mobile" }, + }, +}; +``` -접근성 검사는 Storybook 하단 패널의 `Accessibility` 탭에서 확인합니다. +--- -## Storybook 화면 구성 +## 접근성 검사 -- Sidebar: 등록된 story 목록을 확인합니다. -- Canvas: 선택한 story가 렌더링되는 영역입니다. -- Toolbar: viewport, zoom 등을 조정합니다. -- Addons panel: Controls, Actions, Accessibility 결과를 확인합니다. +하단 Addons 패널 → **Accessibility** 탭에서 자동 검사 결과를 확인합니다. +Violations(위반)이 있으면 컴포넌트 구현을 수정하세요. From 19b190c6762e692612698dc2a6155465aad1a828 Mon Sep 17 00:00:00 2001 From: kimminna Date: Wed, 1 Jul 2026 14:14:06 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs(root):=20=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EB=AC=B8=EC=84=9C=20=EB=B3=B4=EC=99=84?= =?UTF-8?q?=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS 변수 네이밍 규칙에서 scale을 선택 항목으로 수정했습니다 - 누락된 timo-gray-200 컬러 토큰을 Gray 테이블에 추가했습니다 - 누락된 headline-b-40 타이포그래피 토큰을 Headline 테이블에 추가했습니다 --- docs/design/tokens.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/design/tokens.md b/docs/design/tokens.md index 6bf73c27..30760170 100644 --- a/docs/design/tokens.md +++ b/docs/design/tokens.md @@ -6,7 +6,7 @@ ## 네이밍 규칙 - Tailwind CSS 클래스명과 1:1 대응을 원칙으로 한다 -- CSS 변수명: `--color-timo-{category}-{scale}` +- CSS 변수명: `--color-timo-{category}[-{scale}]` (단색 토큰은 scale 생략) - 컴포넌트 내에서는 Tailwind 클래스를 직접 사용하고, 커스텀 값은 `tailwind.config`에 등록한다 ## Color @@ -29,6 +29,7 @@ Tailwind 클래스 사용 예: `bg-timo-blue-50`, `text-timo-gray-900` | 토큰 | CSS 변수 | 값 | | --------------- | ----------------------- | --------- | +| `timo-gray-200` | `--color-timo-gray-200` | `#fbfbfb` | | `timo-gray-300` | `--color-timo-gray-300` | `#f5f5f5` | | `timo-gray-500` | `--color-timo-gray-500` | `#e6e8e8` | | `timo-gray-600` | `--color-timo-gray-600` | `#d8d8d8` | @@ -64,6 +65,7 @@ Tailwind 클래스 사용 예: `typo-headline-b-24`, `typo-body-r-12` | 토큰 | 클래스 | 크기 | 굵기 | | --------------- | -------------------- | ---- | ---- | | `headline-b-50` | `typo-headline-b-50` | 50px | 700 | +| `headline-b-40` | `typo-headline-b-40` | 40px | 700 | | `headline-b-30` | `typo-headline-b-30` | 30px | 700 | | `headline-m-26` | `typo-headline-m-26` | 26px | 500 | | `headline-b-24` | `typo-headline-b-24` | 24px | 700 |