# 스토리북 작성 가이드

> `/gen-storybook` 이 실제 story 파일을 생성할 때 참고하는 상세 Storybook 규칙이다.
> 모든 패키지(`packages/`)에서 `.stories.tsx` 파일을 작성할 때 적용한다.

---

## 유형 판단

| 유형 | 판단 기준 | 대상 패키지 |
|------|-----------|-------------|
| **1. UI 컴포넌트 (단일)** | Props로 렌더링을 제어하는 단일 컴포넌트 | design-system, components |
| **2. Compound 컴포넌트** | Root + 하위 컴포넌트 조합 패턴 | design-system, components |
| **3. 유틸리티 / 훅** | 순수 함수 또는 React 훅 | utils, hooks |
| **4. 인프라 / Provider** | 상태 관리, 통신, 환경 설정 등 | stores, bridge, api, features, sentry |
| **5. Foundation 토큰** | 디자인 토큰 시각 레퍼런스 | design-system (Foundation) |

---

## 필수 스토리 구성

### 유형 1/2: UI 컴포넌트

| 순서 | export명 | name | Chromatic |
|------|----------|------|-----------|
| 1 | `UsageGuide` | `'사용 가이드'` | disableSnapshot |
| 2 | `Faq` (선택) | `'자주 묻는 질문'` | disableSnapshot |
| 3 | `Playground` / `PlaygroundXxx` | `'Playground'` / `'Playground--Xxx'` | capture |
| 4+ | `CaseXxx` | `'Cases--설명'` | capture |

- **Playground** — args로 Props 인터랙티브 조작. args 조작이 의미 없으면 생략 가능
- **Playground--Xxx** — Compound 전용. 하위 컴포넌트별로 분리. args 조작이 가능하면 args 사용, 불가하면 render 함수로 기본 동작 확인용으로 구성
- **Cases--** — 개별 props/조합 케이스. Chromatic 스냅샷 겸용
- **Faq** — 비자명한 동작 시나리오(옵션 상호작용, 누적, 라이프사이클 등)를 Q&A 형식으로 모아두는 별도 스토리. 상세는 후술 [비자명한 동작 시나리오 → FAQ 스토리 분리] 참조

### 유형 3/4: 유틸리티 / 훅 / 인프라

| 순서 | export명 | name | Chromatic |
|------|----------|------|-----------|
| 1 | `UsageGuide` | `'사용 가이드'` | disableSnapshot |
| 2 | `Faq` (선택) | `'자주 묻는 질문'` | disableSnapshot |
| 3+ | `ExampleXxx` | `'예제--설명'` | disableSnapshot |

### 유형 5: Foundation 토큰

| 순서 | export명 | name | Chromatic |
|------|----------|------|-----------|
| 1 | `UsageGuide` | `'사용 가이드'` | disableSnapshot |
| 2 | `Playground` | `'Playground'` | capture |
| 3 | `Total` | `'전체'` | capture |

---

## Meta 정의 규칙

### Title 패턴

패키지명이 반드시 1depth로 위치:

```
Design System/Component/{Category}/{Name}
Design System/Composite/{Name}
Design System/Foundation/{Name}
Components/{Category}/{Name}
Utils/{모듈명}
Hooks/{hook-name}
API/{topic}
```

### Meta 타입 선택

| 상황 | 패턴 |
|------|------|
| UI 컴포넌트 | `const meta: Meta<typeof Component> = { ... }` |
| Compound | `const meta: Meta<typeof Component.Root> = { ... }` + `subcomponents` |
| 비컴포넌트 | `const meta = { ... } satisfies Meta` |

### 공통 필수 항목

```typescript
const meta = {
  title: '패키지명/중간구분/기능명',
  tags: ['autodocs'],
  parameters: {
    docs: {
      description: {
        component: `...`  // 아래 포맷 참조
      }
    }
  }
};
```

---

## description.component 포맷

### 개별 컴포넌트/유틸 스토리

패키지명/설치하기는 **대표 Docs에만** 작성. 개별 스토리에는 포함하지 않는다.

````markdown
컴포넌트/함수 역할 요약 (1~2줄)

### 사용하기

```javascript
import { Xxx } from 'sales-frontend-xxx';
```

## 특징
- 특징 1
- 특징 2

## 유의사항 (해당하는 경우)
- 주의할 점 1
````

### 대표 Docs (패키지 인덱스 스토리)

````markdown
패키지 역할 요약 (1~2줄)

## 패키지명

**sales-frontend-xxx**

### 설치하기

```bash
pnpm add sales-frontend-xxx
```

### Peer Dependencies (해당하는 경우)

```bash
pnpm add @tanstack/react-query
```

### 사용하기

```javascript
import { Xxx } from 'sales-frontend-xxx';
```

## 특징 / 유의사항 / 제공 모듈
````

---

## 사용 가이드 스토리 작성 패턴

```tsx
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';

const sampleCode = `import { Xxx } from 'sales-frontend-xxx';
// 코드 예제...`;

export const UsageGuide: Story = {
  name: '사용 가이드',
  parameters: { chromatic: { disableSnapshot: true } },
  render: function UsageGuide() {
    return (
      <div style={{ display: 'grid', gap: '24px', maxWidth: '980px' }}>
        <section className="sb-code-section">
          <h3 style={{ margin: 0 }}>제목</h3>
          <p style={{ margin: 0 }}>설명 (선택)</p>
          <SyntaxHighlighter language="tsx" style={vscDarkPlus} className="sb-code-block">
            {sampleCode}
          </SyntaxHighlighter>
        </section>
      </div>
    );
  }
};
```

- 샘플코드 변수는 meta 정의 **전에** 선언
- 여러 섹션이면 번호 prefix: `1. 기본 사용법`, `2. 고급 사용법`
- 단일 섹션이면 `{컴포넌트명} 사용 예제`

### 비자명한 동작 시나리오 → FAQ 스토리 분리

**원칙**: 스토리북 사용 가이드는 "소비자(타 프로젝트 개발자)가 패키지 코드와 JSDoc을 보지 않고도 안전하게 사용할 수 있도록" 작성한다.
타입/시그니처만 보고 알 수 없는 모든 **비자명한 동작 시나리오**는 별도 스토리 **`Faq` (`'자주 묻는 질문'`)** 로 분리해 작성한다.

#### 왜 별도 스토리인가

- `UsageGuide`(사용 가이드)는 "처음 쓰는 사람이 5분 안에 따라 칠 수 있는 핵심 사용법"에 집중한다. 시나리오·예외 처리·내부 동작 설명이 섞이면 가이드가 무거워져 학습 비용이 커진다.
- FAQ 스토리는 "이미 쓰고 있는 사람이 막혔을 때 찾아오는 참고서" 역할이다. Q&A 포맷으로 검색·스캔이 쉽도록 한다.
- Storybook sidebar에서 가이드와 FAQ가 분리되어 노출되므로, 신규 학습 vs 트러블슈팅 두 동선이 명확히 갈린다.

#### 작성 원칙

- 패키지 개발자에게는 코드 주석으로 충분하지만, 소비자는 패키지 코드를 보지 않으므로 FAQ에만 있는 정보는 전달되지 않는다.
- 각 항목은 **`Q. ...?`** 헤더로 시작해 검색성을 확보한다.
- 시나리오는 **차수/순서/상태 전이**가 드러나도록 단계별로 기술한다(테이블 권장).
- Chromatic은 `disableSnapshot`으로 두어 시각 회귀 대상에서 제외한다.

#### 어떤 동작이 "비자명"인가 — 추가 신호 체크리스트

다음 중 하나라도 해당하면 가이드에 시나리오 섹션을 만든다.

1. **옵션 간 상호작용**: props가 서로 의존하거나(예: `maxPhotos × pageLimit`, `min × max`, `debounceMs × enabled`) 한쪽 값이 다른 쪽의 의미를 바꾸는 경우
2. **누적/덮어쓰기/리셋**: 호출 차수·시간 흐름에 따라 내부 상태가 누적되거나 덮어써지는 경우 (예: `type/mode` 값에 따라 누적 vs 단일 처리가 갈림)
3. **한도/제한 분기**: 초과 시 정책 분기(`error` / `truncate` / `silent` / 재시도 등)와 각 모드별 기본값
4. **라이프사이클 부수효과**: mount/unmount 시 자동 cleanup 여부, `ref`/리소스(blob URL, subscription) 해제 책임, 의존성 변경 시 리셋 여부
5. **비동기 흐름**: 호출 순서·경합(race) 처리 방식, 중복 호출 시 동작(가장 최근만 유효 / 직렬화 / 무시), 진행 중 unmount 시 동작
6. **에러/예외 경로**: 어떤 입력이 throw 하는지, 호출자가 catch 해야 하는지, 에러 후 내부 상태가 어떻게 되는지(복구 가능 여부)
7. **플랫폼/환경 분기**: OS·브라우저·디바이스별 동작 차이(iOS Safari, Android WebView, 데스크탑), 네이티브 브리지 유무에 따른 폴백
8. **성능/리렌더 특성**: 메모이즈 권장 props, 큰 입력 시 동기 vs 비동기 처리, 이벤트 디바운스/스로틀의 기본값
9. **접근성·키보드 동작**: 포커스 이동 규칙, Esc/Enter 등 키 바인딩, `aria-*` 상태가 변하는 시점
10. **외부 의존**: 특정 Provider/Context 필수 여부, peerDependency 버전 제약, 전역 설정 키와의 충돌

> Rule of thumb: "이 시그니처만 보고 처음 쓰는 개발자가 30분 안에 디버깅할 수 있는가?" 답이 No 라면 그 동작은 가이드 섹션이 필요하다.

#### 시나리오 작성 형식

각 섹션은 다음 4요소 중 해당하는 것을 포함한다.

- **What**: 어떤 시나리오인지 한 줄 요약
- **How**: 차수/단계별 동작 (`1차 → 2차 → 3차`, `mount → 사용 → unmount`, `정상 → 에러 → 복구` 등 상태 전이)
- **Edge cases**: 경계 조건(0, 음수, 빈 배열, 최대치, undefined), 다른 옵션 조합에 따른 분기
- **주의 사항**: 자주 발생하는 오해/안티패턴/사이드이펙트

#### 예시 1 — 옵션 상호작용 (`maxPhotos × pageLimit` 누적 동작)

> FAQ 스토리 내부 각 Q&A 섹션은 **요약 헤더(callout) + 단계별 테이블(`sb-table`) + 주의 사항 박스** 패턴을 권장한다.
> 평문 `<p>`/`<ul>`만 나열하면 시각적으로 뭉개지므로 피한다.

```tsx
export const Faq: Story = {
  name: '자주 묻는 질문',
  parameters: { chromatic: { disableSnapshot: true } },
  render: function Faq() {
    return (
      <div style={{ display: 'grid', gap: '32px', maxWidth: '980px' }}>
        <section className="sb-code-section">
          <h3 style={{ margin: 0 }}>Q. maxPhotos × pageLimit 은 어떻게 누적되나요?</h3>
          <p style={{ margin: 0, lineHeight: 1.7 }}>
            한 번의 <code>onClick</code>은 최대 <code>pageLimit</code> 장을 캡처하고, 결과는 기존 목록에 누적됩니다.
            누적 총량은 <code>maxPhotos</code>로 제한됩니다.
          </p>

          {/* 요약 헤더 (callout): 핵심 조합과 결과를 한 줄로 */}
          <div
            style={{
              display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center',
              padding: '12px 16px', fontSize: '14px',
              background: '#f6f8fa', border: '1px solid #d0d7de', borderRadius: '8px'
            }}
          >
            <strong style={{ color: '#0969da' }}>예시</strong>
            <code>maxPhotos: 10</code>
            <span style={{ color: '#6e7781' }}>×</span>
            <code>pageLimit: 5</code>
            <span style={{ color: '#6e7781' }}>→</span>
            <span>5장씩 두 번 촬영 = <strong>총 10장</strong></span>
          </div>

          {/* 단계별 테이블: 차수/상태 전이를 한눈에 */}
          <table className="sb-table">
            <thead>
              <tr>
                <th style={{ width: 56 }}>차수</th>
                <th style={{ width: 96 }}>current</th>
                <th style={{ width: 96 }}>remaining</th>
                <th>등록 결과</th>
                <th style={{ width: 132 }}>누적 총량</th>
              </tr>
            </thead>
            <tbody>
              <tr><td>1차</td><td>0</td><td>10</td><td>5장 전부 통과</td><td>5장</td></tr>
              <tr><td>2차</td><td>5</td><td>5</td><td>5장 전부 통과</td><td>10장</td></tr>
              <tr>
                <td>3차</td><td>10</td><td>0</td>
                <td>등록 차단 — <code>'error'</code>(기본)면 핸들러 호출, <code>'truncate'</code>면 조용히 잘림</td>
                <td>10장 (변동 없음)</td>
              </tr>
            </tbody>
          </table>

          {/* 주의 사항 박스 (warning callout) */}
          <div
            style={{
              padding: '12px 16px', fontSize: '14px', lineHeight: 1.7,
              background: '#fffbeb', border: '1px solid #fde68a',
              borderLeft: '4px solid #f59e0b', borderRadius: '6px'
            }}
          >
            <strong style={{ color: '#92400e' }}>주의 사항</strong>
            <ul style={{ margin: '6px 0 0', paddingLeft: '20px' }}>
              <li><code>type === 'single'</code>이면 매번 덮어써 누적되지 않음</li>
              <li>한 번에 남은 슬롯 초과 입력 시 — <code>'error'</code>는 전부 거부, <code>'truncate'</code>는 남은 슬롯만큼 잘라 등록</li>
            </ul>
          </div>
        </section>
        {/* Q&A 추가 시 같은 패턴으로 <section>을 늘려간다 */}
      </div>
    );
  }
};
```

**스타일 토큰 가이드** — 인라인 스타일이 길어지면 가독성을 해친다. 가능하면 `apps/docs/app/global.css`의 기존 클래스(`sb-code-section`, `sb-table`)를 우선 활용하고, 위와 같이 자주 쓰는 패턴(요약 callout, warning callout)이 반복되면 글로벌 CSS에 새 클래스 추가를 검토한다.

#### 예시 2 — 라이프사이클·비동기 (Q&A 양식만)

```tsx
<section className="sb-code-section">
  <h3 style={{ margin: 0 }}>Q. 비동기 호출 중 컴포넌트가 unmount 되면 어떻게 되나요?</h3>
  <p>요청 진행 중 unmount 되면 결과는 무시되고, 진행 중 요청은 abort 됩니다.</p>
  <ul>
    <li>mount → fetch 시작 → unmount → 응답 도착: setState 호출 안 됨 (warning 없음)</li>
    <li>동일 인자로 중복 호출 시: 가장 마지막 호출만 유효, 이전 호출은 abort</li>
    <li>에러 후: 내부 상태는 마지막 성공 응답을 유지. 재호출 시 새 요청 시작</li>
  </ul>
</section>
```

---

## 네이밍 규칙

| 항목 | 규칙 |
|------|------|
| `name` | 한글 (`'사용 가이드'`, `'자주 묻는 질문'`, `'Cases--설명'`, `'예제--설명'`). 예외: `'Playground'`, `'Playground--Xxx'` |
| export명 | PascalCase 영문 (`UsageGuide`, `Playground`, `CaseDisabled`) |
| `--` prefix | sidebar flat 나열용 시각적 구분 (폴더 아님) |
| render 함수명 | export명과 동일 (`function UsageGuide() {}`) |
| 샘플코드 변수 | camelCase (`sampleCodeBasic`, `sampleCodeAdvanced`) |

---

## argTypes 규칙

| Props 유형 | control |
|------------|---------|
| 열거형 (4개+) | `{ type: 'select' }` |
| 열거형 (2~3개) | `{ type: 'radio' }` |
| boolean | `{ type: 'boolean' }` |
| 텍스트 | `{ type: 'text' }` |
| 복합 타입 (ReactNode, 함수) | `false` |

- 모든 props에 `description` (한글) 필수
- `table.type.summary` — 타입 요약
- `table.defaultValue.summary` — 기본값 (있는 경우)
- required props — `type: { required: true, name: 'string' }`

---

## import 순서

```typescript
// 1. React (필요시)
import React from 'react';

// 2. 외부 라이브러리
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';

// 3. 내부 컴포넌트/유틸
import { SomeComponent } from './some-component';

// 4. type import
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
```

---

## Chromatic 스냅샷 규칙

| 캡처 대상 | 제외 대상 |
|-----------|-----------|
| Playground, Playground--Xxx, Cases--Xxx, Total | UsageGuide, Faq, 예제--, 테스트 |

### Cases-- 작성 원칙 (스냅샷 겸용)

- 랜덤/시간 의존 데이터 사용 금지
- 고정 컨테이너 크기: `style={{ width: 600 }}`
- 반응형 컴포넌트: `viewports: [375, 768, 1192]`
- 애니메이션: `delay` + `pauseAnimationAtEnd`

---

## 파일 위치 및 구조

| 구분 | 위치 | 파일명 |
|------|------|--------|
| 컴포넌트 스토리 | 컴포넌트와 같은 디렉토리 | `{component-name}.stories.tsx` |
| 유틸 스토리 | 유틸 파일과 같은 디렉토리 | `{util-name}.stories.tsx` |
| 패키지 인덱스 | 패키지 `src/` 루트 | `{package-name}-docs.stories.tsx` |

### 파일 내부 구조 순서

```
1. import 문
2. 샘플코드 변수 (const sampleCode = `...`)
3. Meta 정의
4. export default meta
5. type Story 정의
6. UsageGuide 스토리
7. Faq 스토리 (비자명한 동작 시나리오가 있는 경우 — UsageGuide와 함께 reference 도큐먼트로 묶기 위해 바로 뒤에 둔다)
8. Playground 스토리 (해당시)
9. Cases-- / 예제-- 스토리
```

---

## 글로벌 CSS 클래스

`apps/docs/app/global.css`에 정의:

| 클래스 | 용도 |
|--------|------|
| `sb-code-section` | 코드 섹션 컨테이너 (`<section>`) |
| `sb-code-block` | SyntaxHighlighter의 `className` |
| `sb-table` | 테이블 스타일 |
