# ClaBi UI Agent Guide

> **대상**: AI coding agent  
> **목적**: - 비개발자도 소비 프로젝트(또는 기존 앱)에서 이 파일만으로 ClaBi Storybook UI를 가져와 **화면을 구현**한다.  
- 사용자가 이미지를 첨부하여 만들 경우, 이미지를 분석해 버튼이 필요할 경우 Button 컴포넌트등 목적에 맞는 컴포넌트 사용을 우선으로 한다. 
> **원칙**: UI·토큰 소스는 **Storybook 한 저장소**. 소비 앱은 **파일 복사 + npm 패키지**로 붙인다 (npm private package 배포 아님).  
> **clabi_ds를 클론하지 않는다.** 토큰은 Storybook의 `tokens/tokens.json` 기준. 갱신 시 JSON을 **파일로 다운로드·교체** 후 재빌드.  
> 이 문서는 **소비 앱 가이드**이지만 **Storybook 레포에 보관**한다.

에이전트는 사용자 요청을 받으면 이 문서의 **Mandatory Rules → Bootstrap → Component Catalog → Screen Build** 순으로 따른다.

---

## 1. Repositories (Source of Truth)

GitLab 권한 필요. HTTPS 또는 SSH 중 환경에 맞는 것 사용.

### 1.1 Storybook — 유일한 소스 (UI · tokens · 빌드 CSS)

| 항목 | 값 |
|------|-----|
| HTTPS | `https://gitlab.clabi.co.kr/clabi/projects/storybook.git` |
| SSH | `git@gitlab.clabi.co.kr:clabi/projects/storybook.git` |
| 권장 브랜치 | `main` |
| 런타임 UI (스토리북 소스) | `src/components/<name>/` |
| **소비 앱 설치 위치** | **`src/components/ui/<name>/`** (반드시 `ui` 하위) |
| 소비 앱 import | `import { … } from "@/components/ui/<name>"` |
| 토큰 원본 (JSON) | `tokens/tokens.json` |
| 토큰 버전 메모 (선택) | `tokens/VERSION`, `package.json` → `config.clabiDsTag` |
| 토큰 CSS (빌드 산출) | `src/styles/tokens.css` |
| Tailwind 매핑 | `src/styles/clabi-theme.css` |
| 토큰 빌드 | `scripts/build-tokens.mjs` (`--build`: JSON → CSS) |
| 문서·프리뷰 (복사 금지) | `src/catalog/*`, Storybook stories, `src/app/globals.css` |

로컬 관례 이름: `storybook` (소비 앱과 형제 또는 `SB` 경로만 알면 됨)

```bash
git clone https://gitlab.clabi.co.kr/clabi/projects/storybook.git storybook
# 또는
git clone git@gitlab.clabi.co.kr:clabi/projects/storybook.git storybook
cd storybook && git checkout main && git pull
```

### 1.2 토큰 기준 — Storybook `tokens.json` (clabi_ds 클론 금지)

| 항목 | 값 |
|------|-----|
| 작업 기준 | **Storybook 레포의 `tokens/tokens.json`** |
| 기본 적용 | 이미 빌드된 `src/styles/tokens.css` + `clabi-theme.css` 복사 |
| 재빌드 | `tokens/tokens.json` 갱신 → `node scripts/build-tokens.mjs --build` |
| **금지** | `clabi_ds` git clone · `../clabi_ds` 경로 · DS checkout 후 sync |

디자인/토큰 업데이트가 필요할 때는 팀에서 **`tokens.json` 파일만** 받는다  
(웹 다운로드, 메일/슬랙 첨부, 아티팩트 등). **DS 저장소 전체를 clone하지 않는다.**

빌드 스크립트가 가정하는 경로:

```text
(소비 앱 루트)/
  scripts/build-tokens.mjs    ← Storybook에서 복사
  tokens/tokens.json          ← Storybook 복사 또는 직접 다운로드본
  src/styles/tokens.css       ← --build 결과
  src/styles/clabi-theme.css  ← Storybook에서 복사 (스크립트가 만들지 않음)
```

### 1.3 역할 분담 (에이전트)

| 소스 | 가져올 것 |
|------|-----------|
| **Storybook** | `components/*`, `tokens.css`, `clabi-theme.css`. 필요 시 `tokens/tokens.json` + `build-tokens.mjs` |
| **tokens.json 파일 단독 갱신** | 새 JSON 덮어쓰기 → `--build`. clabi_ds 클론 없음 |

---

## 2. Mandatory Rules (절대)

1. **React 19 · Tailwind CSS v4 · `clsx` · Node 20+ · path alias `@/` → `src/`**
2. 컴포넌트 className 병합: **`import { clsx } from "clsx"`** 만 허용. `cn` / `@/shared/lib/utils` / `shared/*` **복사·도입 금지**
3. **소비 앱 레이아웃**: Storybook `src/components/<name>` → 소비 앱 **`src/components/ui/<name>`** (폴더 **통째** 복사, assets 포함). 앱 화면 코드는 `src/components/` 루트가 아니라 **`ui/` 아래** DS 컴포넌트만 둔다.
4. 복사 후 컴포넌트 소스 안의 `@/components/<sibling>` import는 **`@/components/ui/<sibling>`** 으로 고친다 (Field→Label, Alert→Button 등). 앱 페이지 import도 동일: `@/components/ui/button`.
5. 복사함: `tokens.css`, `clabi-theme.css`, 필요한 컴포넌트 폴더(위 경로 규칙)
6. 복사 금지: `catalog/*`, `*.stories.tsx`, docs MDX, Storybook `app/globals.css`, `shared/*`(카탈로그용)
7. **CSS 변수명 변경 금지** (`--surface-primary` 등). 값만 소비 앱 globals에서 동일 이름으로 재정의
8. **`tokens.css` / `clabi-theme.css` 값을 앱에서 직접 수정 금지**. 브랜드 색은 앱 `:root` 재정의 또는 `className`. `tokens.json`은 **파일 전체 교체**만 (손편집 금지)
9. **`clabi_ds` 저장소 clone 금지**. 토큰은 Storybook `tokens/tokens.json` 또는 제공된 JSON 파일만 사용
10. 컴포넌트 소스에 **hex 하드코딩 추가 금지** (앱 전역 테마·인스턴스 className 예외만)
11. 컴포넌트 형제 의존이 있으면 **`ui/` 아래로 같이 복사** (아래 카탈로그 표)
12. Radix peer는 **쓰는 컴포넌트만** 설치
13. 설치 후 컴포넌트 API는 **`src/components/ui/<name>/`의 `types.ts` · JSX 주석 · `index.ts` export**를 읽고 쓴다. 카탈로그 코드 복사 금지

### 소비 앱 폴더 규칙

```text
(소비 앱)/
  src/
    app/                    ← 라우팅·metadata·layout 만
      globals.css
      login/page.tsx        ← feature 화면을 import 해서 연결
      page.tsx
    feature/                ← 페이지·도메인별 화면 구현
      login/
        LoginPage.tsx
        index.ts
    components/
      ui/                   ← DS 컴포넌트만 여기
        button/
        field/
        …
    styles/
      tokens.css
      clabi-theme.css
```

| 역할 | 경로 | 담당 |
|------|------|------|
| 라우팅 | `src/app/.../page.tsx` | URL · metadata · feature 연결만 |
| 화면 구현 | `src/feature/<page>/` | UI·상태·로직 |
| DS 컴포넌트 | `src/components/ui/<name>/` | Storybook 복사본 |

```tsx
// src/app/login/page.tsx — 라우팅만
import { LoginPage } from "@/feature/login";
export default function Page() {
  return <LoginPage />;
}
```

| 구분 | 경로 |
|------|------|
| Storybook 원본 | `$SB/src/components/button` |
| 소비 앱 복사본 | `src/components/ui/button` |
| import (DS) | `@/components/ui/button` |
| import (화면) | `@/feature/login` |

### Token 우선순위

```

(1) tokens.css 기본값
 → (2) 소비 앱 globals.css :root 동일 변수명 재정의
 → (3) 인스턴스 className

```


### globals.css 템플릿 (소비 앱 소유 — Storybook globals 복사 금지)


```css

@import "tailwindcss";
@import "../styles/tokens.css";
@import "../styles/clabi-theme.css";

/* 필요할 때만 브랜드 덮기 — 이름 동일 */

:root {
  /* --surface-primary: #…; */
  /* --text-primary: #…; */
}

body {
  background: var(--background-default);
  color: var(--text-default);
  font-family: var(--font-sans);
}

```

---

## 3. Bootstrap Procedure (새 프로젝트 · 디자인 시스템 붙이기)

에이전트 작업 순서. 실패 시 롤백하지 말고 누락 step부터 재개.

### Step 0 — 환경 확인

- Next.js App Router 권장 (또는 React 19 SPA + Tailwind v4 동일 규칙)
- `package.json`에 `react@19`, `tailwindcss@^4`, `@tailwindcss/postcss`
- `tsconfig` paths: `"@/*": ["./src/*"]`

### Step 1 — Storybook 확보

```bash

# 이미 형제 폴더에 있으면 pull만
git -C ../storybook pull origin main
# 없으면 clone
git clone https://gitlab.clabi.co.kr/clabi/projects/storybook.git ../storybook

```

경로 변수 (이 문서에서 `SB` = Storybook 루트 절대/상대 경로).

### Step 2 — 토큰 · 테마 복사 (기본: 빌드 산출 복사)

```bash

mkdir -p src/styles src/components/ui
cp "$SB/src/styles/tokens.css" src/styles/
cp "$SB/src/styles/clabi-theme.css" src/styles/

```

**선택 B** — `tokens.json` 기준으로 CSS를 다시 빌드할 때만  
(`clabi_ds` 클론 하지 않음)

```bash
mkdir -p tokens scripts src/styles

# 1) Storybook에 올라온 JSON을 가져오거나
cp "$SB/tokens/tokens.json" tokens/tokens.json
cp "$SB/scripts/build-tokens.mjs" scripts/

# 2) 또는 팀에서 다운로드한 새 tokens.json 으로 교체
# cp ~/Downloads/tokens.json tokens/tokens.json

# 3) JSON → tokens.css
node scripts/build-tokens.mjs --build

# clabi-theme 은 Storybook에서 복사 (스크립트 미생성)
cp "$SB/src/styles/clabi-theme.css" src/styles/
```

`tokens.json`만 바꿨을 때(디자인 전달 파일):

```bash
# 교체 후 재빌드
cp /path/to/new/tokens.json tokens/tokens.json
node scripts/build-tokens.mjs --build
```



### Step 3 — globals 연결

`src/app/globals.css`(또는 앱 CSS 진입점)에 §2 템플릿 적용. layout에서 1회 import.

### Step 4 — 공통 의존성

```bash

npm i clsx

```

<!--  -->


Component를 추가할 때 반드시 다음 순서를 따른다. 

1. Storybook에서 해당 컴포넌트가 존재하는지 확인한다.
2. Component의 Required Props를 확인한다. 
3. Required Props가 모두 제공되지 않았다면 사용자에게 질문한다. 
4. 모든 Required Props가 수집된 후 Component를 생성한다. 
5. Component를 생성한 후 필요한 Asset을 복사한다.
6. 필요한 import를 자동으로 추가한다. 

### Step 5 — 화면에 필요한 컴포넌트 폴더만 복사 (`ui/` 하위 필수)

```bash
mkdir -p src/components/ui

# 예: 로그인 폼 — Storybook → 소비 앱 ui/
cp -R "$SB/src/components/button" src/components/ui/
cp -R "$SB/src/components/field" src/components/ui/
cp -R "$SB/src/components/label" src/components/ui/
cp -R "$SB/src/components/input" src/components/ui/
cp -R "$SB/src/components/badge" src/components/ui/   # 뱃지 필요 시
cp -R "$SB/src/components/icon" src/components/ui/    # 아이콘 필요 시
```

**형제 의존 표(§4) 확인 후 `ui/` 아래에 누락 폴더 추가.**  
예: Field → Label, Alert → Button.

**내부 path 교체 (복사 직후 필수)**  
Storybook 소스는 `@/components/field` 형태이므로, 소비 앱에서는 `@/components/ui/…`로 맞춤:

```bash
# macOS sed 예시 — 형제 참조를 ui 아래로
find src/components/ui -type f \( -name '*.ts' -o -name '*.tsx' \) -exec \
  sed -i '' 's|@/components/\([a-zA-Z0-9_-]*\)|@/components/ui/\1|g' {} +
# 이중 치환 방지
find src/components/ui -type f \( -name '*.ts' -o -name '*.tsx' \) -exec \
  sed -i '' 's|@/components/ui/ui/|@/components/ui/|g' {} +
```

앱 화면 코드:

```tsx
import { Button } from "@/components/ui/button";  // OK
// import { Button } from "@/components/button"; // 금지 — ui 생략 금지
```


### Step 6 — peer / Radix (해당 시)


```bash

npm i @radix-ui/react-dialog    # Modal
npm i @radix-ui/react-select    # Select
npm i @radix-ui/react-tabs      # Tabs
npm i @radix-ui/react-toast     # Toast
npm i @radix-ui/react-tooltip   # Tooltip
npm i react-datepicker date-fns @floating-ui/react   # DatePicker

```


### Step 7 — 화면 구현

§5 Screen Build Playbook 따라 페이지/컴포넌트 작성.  

import는 `@/components/ui/<name>` (index export).


### Step 8 — 검증


```bash

# Typecheck / build
npm run build
# 런타임: primary 등 토큰 색 적용, 콘솔에 path alias 에러 없음, @/shared import 없음

```

---

## 4. Component Catalog (에이전트 조회 표)


| | 경로 |
|--|------|
| Storybook 소스 | `$SB/src/components/<folder>/` |
| 소비 앱 설치 | `src/components/ui/<folder>/` |
| import | `import { Button } from "@/components/ui/button"` |


| folder | npm peer (추가) | 형제 의존 (같이 복사) | 비고 |
|--------|-----------------|------------------------|------|
| `button` | — | — | `variant`: primary/secondary/tertiary/ghost · `size` |
| `label` | — | — | |
| `field` | — | **`label`** | Field / FieldLabel / FieldDescription / FieldError |
| `input` | — | (폼이면 field+label) | |
| `textarea` | — | (폼이면 field+label) | |
| `select` | `@radix-ui/react-select` | (폼이면 field+label) | 합성: SelectTrigger, SelectContent, SelectItem… |
| `checkbox` | — | — | CheckboxGroup + CheckboxItem |
| `radio` | — | — | RadioGroup + RadioItem |
| `toggle` | — | — | |
| `badge` | — | — | |
| `icon` | — | — | `Icon` default + `icon-meta` / `generated` |
| `tabs` | `@radix-ui/react-tabs` | — | TabsList · TabsTrigger · TabsContent |
| `tooltip` | `@radix-ui/react-tooltip` | — | 편의: `<Tooltip content="…">` 또는 합성 API |
| `alert` | — | **`button`** | 합성: AlertTitle · AlertContents · `onClose` 필수 |
| `toast` | `@radix-ui/react-toast` | **`alert`** (+ alert→button) | `ToastProvider` · `useToast` |
| `modal` | `@radix-ui/react-dialog` | (CTA에 Button) | ModalTrigger · ModalContent · ModalFooter… |
| `table` | — | — | TableHeader · TableBody · TableRow · TableCell… |
| `pagination` | — | — | controlled `page` · `totalPages` · `onPageChange` |
| `datepicker` | `react-datepicker` · `date-fns` · `@floating-ui/react` | (폼이면 field+label) | **DatePicker** — 아래 §4.1 참조. 엔진 react-datepicker + Trigger 합성 |
| `skeleton` | — | — | **Skeleton** — 아래 §4.2 참조. `Skeleton` + `SkeletonItem`, 레이아웃·색은 className |

**미구현 / 비어 있음 (복사하지 말 것, 폴더만 존재할 수 있음)**  

- `calendar` (전체 화면 월간/연간 달력 — 별도 추가 예정; **날짜/기간 입력은 datepicker 사용**)  
- `bagde`, `icon-button` 등 빈 디렉터리는 무시  

### API 확인 순서 (카탈로그 복사 대신)

1. Storybook 또는 소비 앱 `…/ui/<name>/index.ts` — public export  
2. `types.ts` — props  
3. `*.tsx` 상단 JSDoc 예시  
4. (참고만) Storybook 앱 `/components/<slug>/overview` — **catalog 소스는 복사 금지**  
5. 복사본은 항상 **`src/components/ui/`** 에만 둔다 (`src/components/button` 처럼 루트에 두지 않음)

### 자주 쓰는 import 스니펫

```tsx

import { Button } from "@/components/ui/button";
import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Alert, AlertTitle, AlertContents } from "@/components/ui/alert";
import {
  Modal, ModalTrigger, ModalContent, ModalTitle,
  ModalDescription, ModalFooter, ModalClose,
} from "@/components/ui/modal";
import { ToastProvider, useToast } from "@/components/ui/toast";
import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
import Icon from "@/components/ui/icon/Icon";
import {
  DatePicker,
  DatePickerTrigger,
  emptyDuration,
  type DatePickerDuration,
} from "@/components/ui/datepicker";
import { Skeleton, SkeletonItem } from "@/components/ui/skeleton";

```



```tsx

// Alert (title prop 없음 — 합성)

<Alert variant="fail" position="inline" onClose={() => {}}>
  <AlertTitle>오류</AlertTitle>
  <AlertContents>요청을 처리하지 못했습니다.</AlertContents>
</Alert>

// Tooltip 편의 API
<Tooltip content="도움말">
  <Button variant="secondary" size="small">?</Button>
</Tooltip>

```

### 4.1 DatePicker — 날짜 · 기간 입력 (에이전트 설계 가이드)

> 시안/스크린샷에 **날짜 입력·캘린더 팝오버·기간(시작~종료)** 이 보이면 **Input type="date" 또는 자체 달력 구현 대신 `datepicker` 폴더를 복사**해 사용한다.

#### 언제 쓰는가 (이미지·요구 매핑)

| 시안에서 보이는 것 | 사용 |
|--------------------|------|
| 달력 아이콘 + `YYYY.MM.DD` / 빈 날짜 필드, 클릭 시 월간 그리드 | `<DatePicker />` (default 트리거) |
| 버튼/칩 모양인데 “날짜 선택” | `<DatePicker><DatePickerTrigger asChild>…</DatePickerTrigger></DatePicker>` |
| **한 필드**에서 시작일·종료일을 **같은 달력**으로 고름 | `mode="duration"` + `duration` / `onDurationChange` |
| 일 단위가 아니라 **연·월만** (예: `2026.04`) | `view="month"` + `format="yyyy.MM"` |
| 캘린더가 입력 **아래/위/왼/오른** 으로 열림 | `side="bottom" \| "top" \| "left" \| "right"` (기본 bottom) |
| 기간이 하이라이트(양끝 진한 primary, 중간 연한 파란 구간) | duration 모드 기본 스타일 — 추가 CSS 불필요 |
| **전체 화면·월간 스케줄 보드** 달력 | `calendar` 미구현 → datepicker로 대체하지 말고 요구 분리 확인 |

**쓰지 말 것**

- `native <input type="date">` 로 DS 룩 재현  
- Storybook `catalog/datepicker` 코드 복사  
- 구 API: `DatePickerCalendar`, dayjs `duration` 유틸, 자체 그리드 (제거됨)

#### 설치 (소비 앱)

```bash
# 1) 폴더 복사
mkdir -p src/components/ui
cp -R "$SB/src/components/datepicker" src/components/ui/
# path alias: @/components/* → @/components/ui/* (Step 5 sed)

# 2) npm (datepicker 전용 peer)
npm i react-datepicker date-fns @floating-ui/react
# CSS 모듈·clsx 는 기존 Bootstrap 공통 전제
```

엔진이 `react-datepicker` CSS를 컴포넌트 내부에서 import한다. 앱 globals에 별도로 datepicker CSS를 또 넣을 필요 없다.

#### 구조 (한 줄 요약)

```
DatePicker (루트: 상태 · 팝오버 · 달력)
  └─ children 생략 → 기본 DatePickerTrigger (아이콘 + 날짜 라벨)
  └─ DatePickerTrigger asChild → 커스텀 버튼/엘리먼트
```

- **default 트리거** = children 생략 (아이콘 + format/선택값). 명시 `<DatePickerTrigger />` 는 기본과 동일 UI이므로 overview/문서 시 default 하나로 취급.  
- **커스텀만** `asChild` 사용.

#### 핵심 props

| prop | 기본 | 설명 |
|------|------|------|
| `mode` | `"single"` | `"single"` 하루 · `"duration"` 한 피커에서 start→end |
| `selected` / `onChange` | — | single 제어 값 (`Date \| null`) |
| `defaultSelected` | `null` | single 비제어 초기값 |
| `duration` / `onDurationChange` | — | duration 제어 `{ start, end }` (`Date \| null` 각각) |
| `defaultDuration` | `{start:null,end:null}` | duration 비제어 (`emptyDuration()`) |
| `rangeSeparator` | `" - "` | 기간 표시 구분자 (트리거 라벨) |
| `format` | `"yyyy.MM.dd"` | date-fns 토큰. `YYYY`/`DD` 도 자동 변환 |
| `emptyLabel` | format 또는 `format - format` | 미선택 시 플레이스홀더 텍스트 |
| `view` | `"day"` | `"day"` 일 그리드 · `"month"` 월 피커 |
| `side` | `"bottom"` | `top` / `bottom` / `left` / `right` (flip 없음, 지정 방향 유지) |
| `popperPlacement` | — | floating 정렬까지 직접 지정 시. 있으면 `side` 보다 우선 |
| `minDate` / `maxDate` | — | 선택 가능 범위 |
| `disabled` | `false` | |
| `open` / `onCalendarOpen` / `onCalendarClose` | — | 열림 제어·콜백 |
| children | 기본 Trigger | `DatePickerTrigger` 또는 asChild 자식 |

**상태 타입**

```ts
type DatePickerDuration = { start: Date | null; end: Date | null };
// emptyDuration() → { start: null, end: null }
```

#### 상호작용 (duration)

1. 첫 날짜 클릭 → `start`  
2. 둘째 날짜 클릭 → `end`  
3. start·end 모두 있으면 패널 닫힘  
4. 트리거 표시: `2026.04.10 - 2026.04.20` (format + rangeSeparator)

#### 기간 선택 UX 권장 (권장 · 강제 아님 · 소비 앱 레이어)

DatePicker는 기간의 비즈니스 규칙을 강제하지 않는다. 아래는 **조회/필터 기간 UI** 에서 권장하는 앱 측 처리이다.

| 상황 | 권장 동작 | 구현 힌트 |
|------|-----------|-----------|
| **시작일 재설정** | 종료일을 **최대 조회 기간** 상한으로 다시 맞춤 (또는 구간을 비우고 재선택 유도) | `onDurationChange` 에서 start만 바뀌었을 때 `end = start + maxPeriod` (또는 `emptyDuration` 후 end만 클리어) |
| **종료일 선택** | **시작일보다 이전 날짜를 고를 수 없게** 하는 것을 **disabled 처리로 우선** | start 선택 후 `minDate={start}` (종료 픽 단계). 또는 filterDate로 start 이전 disable |
| 최대 조회 기간 초과 | end 또는 구간 길이를 `maxDate` / filter / 앱 검증으로 제한 | 예: 최대 90일 → `maxDate = addDays(start, 90)` |
| 종료일 > 시작일이 이미 역전된 값 유입 | 정렬(`start ≤ end`) 또는 에러 표시 | 컴포넌트 기본 swap 동작에만 기대하지 말 것 |

**주의**

- 권장이지 DatePicker API 강제 사항이 아니다. 제품 정책에 맞게 생략·완화 가능.  
- `minDate` / `maxDate` 는 루트 props 이며 **동적으로 바꿔** 단계별 (start 고른 뒤 minDate 갱신) 구현한다.  
- 시작일 재설정 시 end를 자동으로 최대 기간으로 두는 것은 “빈 기간 방지·조회 상한 명확화”용 권장안이다.

```tsx
// 권장 예시 — 최대 조회 90일, end 는 start 이전 선택 불가
import { addDays } from "date-fns";

const MAX_RANGE_DAYS = 90;

function QueryPeriodPicker() {
  const [range, setRange] = useState<DatePickerDuration>(emptyDuration());

  const handleDurationChange = (next: DatePickerDuration) => {
    // 시작일만 새로 (end 비움 or start만 갱신) → end 를 최대 조회 기간으로
    if (next.start && !next.end) {
      setRange({
        start: next.start,
        end: addDays(next.start, MAX_RANGE_DAYS),
      });
      return;
    }
    // start 재설정으로 구간이 바뀐 경우 예시: end 가 start보다 앞이면 보정
    if (next.start && next.end && next.end < next.start) {
      setRange({
        start: next.start,
        end: addDays(next.start, MAX_RANGE_DAYS),
      });
      return;
    }
    setRange(next);
  };

  return (
    <DatePicker
      mode="duration"
      duration={range}
      onDurationChange={handleDurationChange}
      // 종료(및 재선택) 시 시작일 이전 비활성 — 권장
      minDate={range.start ?? undefined}
      // 최대 조회 기간 상한 (start 기준)
      maxDate={
        range.start ? addDays(range.start, MAX_RANGE_DAYS) : undefined
      }
      format="yyyy.MM.dd"
    />
  );
}
```

> `minDate`/`maxDate` 를 start 선택 전에는 전역 조회 가능 구간(오늘~N년 등)으로 두고, start 확정 후 end 단계에만 start 이전을 disable 하는 방식이 자연스럽다. 라이브러리 selectsRange 의 클릭 순서에 맞춰 앱 state를 해석해 적용한다.

#### 시각 · 레이아웃 (시안 정합 — 커스텀 그리드 CSS 금지)

| 요소 | 동작/크기 |
|------|-----------|
| 트리거 | 왼쪽 **캘린더 아이콘**, 오른쪽 날짜 또는 format 플레이스홀더 (`text-tertiary`) |
| 헤더 | 왼쪽 `yyyy년 M월` (month view는 `yyyy년`), 오른쪽 `<` `>` — **space-between** |
| 요일 | 일~토 (locale ko) |
| 일자 셀 | default / hover(연한 배경) / selected(primary+흰 글자) / outside·disabled(회색) |
| 기간 중간 | primary-subtler 배경, 양끝 selected 동일 |
| 패널 너비 | mobile max **20rem(320)**, PC **344px**; 패딩 16 / 24; radius 12 / 16 |
| 입력↔패널 간격 | 약 8px; 팝오버 `position: fixed` (DocSection overflow clip 대비) |
| 그림자 | `var(--shadow-md)` |

#### 코드 패턴 (복사·참고용)

```tsx
"use client";
import { useState } from "react";
import {
  DatePicker,
  DatePickerTrigger,
  emptyDuration,
  type DatePickerDuration,
} from "@/components/ui/datepicker";
import { Field, FieldLabel } from "@/components/ui/field";
import { Button } from "@/components/ui/button";

// ── 하루 (가장 흔함) ──
function BirthDateField() {
  const [date, setDate] = useState<Date | null>(null);
  return (
    <Field>
      <FieldLabel>생년월일</FieldLabel>
      <DatePicker
        selected={date}
        onChange={setDate}
        format="yyyy.MM.dd"
      />
    </Field>
  );
}

// ── 기간 (예약·필터 등) ──
function StayPeriodField() {
  const [range, setRange] = useState<DatePickerDuration>(emptyDuration());
  return (
    <Field>
      <FieldLabel>숙박 기간</FieldLabel>
      <DatePicker
        mode="duration"
        duration={range}
        onDurationChange={setRange}
        format="yyyy.MM.dd"
      />
    </Field>
  );
}

// ── 월만 (청구 월 등) ──
function BillingMonthField() {
  const [month, setMonth] = useState<Date | null>(null);
  return (
    <DatePicker
      view="month"
      format="yyyy.MM"
      selected={month}
      onChange={setMonth}
    />
  );
}

// ── 커스텀 트리거 ──
function CustomTriggerPicker() {
  const [date, setDate] = useState<Date | null>(null);
  return (
    <DatePicker selected={date} onChange={setDate} format="yyyy.MM.dd">
      <DatePickerTrigger asChild>
        <Button type="button" variant="secondary" size="medium">
          날짜 선택
        </Button>
      </DatePickerTrigger>
    </DatePicker>
  );
}

// ── 열림 방향 ──
<DatePicker side="top" selected={date} onChange={setDate} />
```

#### 폼 연동 팁

- 값 타입은 **`Date | null`** (문자열 파싱이 필요하면 `date-fns` `format` / `parse`를 화면 단에서).  
- Field 와 쓸 때: Label은 FieldLabel, 컨트롤은 DatePicker를 Field 자식으로.  
- 제출 시 API가 ISO 문자열이면 `date?.toISOString()` 등 변환은 **feature 레이어**에서.

#### Public export (`index.ts`)

`DatePicker`, `DatePickerTrigger`, `emptyDuration`, `toDateFnsFormat`, `resolvePopperPlacement`,  
타입: `DatePickerProps`, `DatePickerTriggerProps`, `DatePickerView`, `DatePickerMode`, `DatePickerSide`, `DatePickerPlacement`, `DatePickerDuration`

### 4.2 Skeleton — 로딩 플레이스홀더 (에이전트 설계 가이드)

> 시안/스크린샷에 **회색 펄스 바·아바타 원·카드 자리 표시**가 보이거나, 데이터 로딩 중 UI를 유지해야 하면 **스피너/텍스트 “로딩…” 대신 `skeleton` 폴더를 복사**해 사용한다.

#### 언제 쓰는가

| 시안에서 보이는 것 | 사용 |
|--------------------|------|
| 텍스트 줄 여러 개의 회색 바 | `<Skeleton className="flex flex-col gap-2">` + `SkeletonItem` 높이·너비 |
| 원형 아바타 + 옆 텍스트 라인 | `SkeletonItem className="size-10 rounded-full"` + 라인 Item |
| 카드/리스트 그리드 자리 | `Skeleton className="grid …"` 안에 Item 조합 |
| 토큰·브랜드색·임의 hex로 바 색 변경 | Item에 `bg-[var(--token)]` / `bg-[#hex]` / `bg-[rgba(...)]` |

**쓰지 말 것**

- variant / size prop으로 레이아웃을 강제하는 API 기대 (없음 — **className만**)
- Storybook `catalog/skeleton` 코드 복사
- 기본 배경과 겹치는 `surface-neutral-subtle` 등을 기본값으로 가정 (기본은 **`surface-neutral`**)

#### 설치 (소비 앱)

```bash
mkdir -p src/components/ui
cp -R "$SB/src/components/skeleton" src/components/ui/
# peer 추가 없음 (clsx만 — 이미 Step 3에서 설치)
```

#### API 요약

| export | 역할 |
|--------|------|
| `Skeleton` | 래퍼. `role="status"` · `aria-busy`. 레이아웃은 `className` (flex/grid/gap) |
| `SkeletonItem` | 펄스 칸. 기본: `animate-pulse` + `bg surface-neutral` + `radius-8` + `h-4` |

- `className`에 **`bg-*`가 있으면** 기본 배경을 빼고 그대로 적용 (토큰 · hex · rgba 포함).  
- 형제 의존·Radix peer 없음.

#### 코드 패턴

```tsx
import { Skeleton, SkeletonItem } from "@/components/ui/skeleton";

// 텍스트 라인
<Skeleton className="flex w-full max-w-sm flex-col gap-2">
  <SkeletonItem className="h-4 w-3/4" />
  <SkeletonItem className="h-4 w-full" />
  <SkeletonItem className="h-4 w-5/6" />
</Skeleton>

// 아바타 + 라인
<Skeleton className="flex w-full max-w-sm items-center gap-3">
  <SkeletonItem className="size-10 shrink-0 rounded-full" />
  <div className="flex min-w-0 flex-1 flex-col gap-2">
    <SkeletonItem className="h-4 w-1/2" />
    <SkeletonItem className="h-3 w-full" />
  </div>
</Skeleton>

// 색 커스텀
<SkeletonItem className="h-4 w-full bg-[var(--surface-disabled)]" />
<SkeletonItem className="h-4 w-full bg-[#22c55e]" />
<SkeletonItem className="h-4 w-full bg-[rgba(59,130,246,0.45)]" />
```

#### Public export (`index.ts`)

`Skeleton`, `SkeletonItem`, `skeletonItemVariants`, `skeletonWrapperVariants`  
타입: `SkeletonProps`, `SkeletonItemProps`

---

## 5. Screen Build Playbook (화면 만들기)

사용자가 “화면/페이지/폼/모달 UI 만들어줘”라고 하면:

### 5.1 요구 분해

1. 필요한 UI 블록 나열 (버튼, 입력, 날짜, 테이블, 피드백…)  
2. §4 표에서 **folder 목록** 확정  
   - 날짜/기간 필드 → `datepicker` (§4.1)  
   - 로딩 플레이스홀더 → `skeleton` (§4.2)  
3. 형제 의존 + peer(Radix · datepicker) 자동 확장  
4. 디자인 토큰 색이 브랜드를 바꾸면 → globals `:root`만 수정 (컴포넌트·tokens 파일 X)

### 5.2 미설치 시 설치

§3 Step 2~6 실행.  

이미 복사된 컴포넌트는 덮어쓸지 사용자 확인.

**토큰 갱신 여부**

- Storybook `main` 최신 `tokens.css`를 반영할 것인가? → Step 2 **선택 A** (`tokens.css` 재복사)
- 또는 팀이 준 새 `tokens.json` / Storybook `tokens/tokens.json` 기준으로 다시 빌드할 것인가? → Step 2 **선택 B**
- 둘 다 아니면 현 `tokens.css` 유지
- 버전 메모: Storybook `tokens/VERSION` · `package.json` `config.clabiDsTag` (참고용, clabi_ds 클론 불필요)


### 5.3 페이지 작성 규칙

- className은 Tailwind v4 + **토큰 매핑 유틸** 사용: `bg-surface-primary`, `text-text-default`, `border-border-default` 등  
- 임의의 hex로 팔레트 만들지 말 것. 필요 시 토큰 변수 재정의  
- 폼: Field + Label/Input/Error 조합  
- **날짜·기간**: DatePicker (§4.1). `type="date"` 네이티브 입력으로 DS 룩 대체 금지  
- **로딩 UI**: Skeleton (§4.2). children으로 Item 매핑 + className. 스피너/“로딩…” 텍스트로 DS 룩 대체 금지  
- 피드백: Alert 인라인 / ToastProvider 래핑 후 useToast  
- 오버레이: Modal + Button CTA  
- `"use client"`는 훅·이벤트·Radix·DatePicker 합성에 필요하면 사용  

### 5.4 최소 작업 단위 예시 — “로그인 화면”

복사:

```bash
mkdir -p src/components/ui
cp -R "$SB/src/components/button" src/components/ui/
cp -R "$SB/src/components/field" src/components/ui/
cp -R "$SB/src/components/label" src/components/ui/
cp -R "$SB/src/components/input" src/components/ui/
cp -R "$SB/src/components/badge" src/components/ui/
cp -R "$SB/src/components/icon" src/components/ui/
# 복사 후 @/components/* → @/components/ui/* path 교체 (Step 5 참고)
```

구현 골격:


```tsx

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Alert, AlertTitle, AlertContents } from "@/components/ui/alert";

export default function LoginPage() {

  const [error, setError] = useState<string | null>(null);

  return (
    <main className="min-h-dvh bg-background text-foreground flex items-center justify-center p-6">
      <form className="w-full max-w-sm space-y-6" onSubmit={…}>
        {error ? (
          <Alert variant="fail" position="inline" onClose={() => setError(null)}>
            <AlertTitle>로그인 실패</AlertTitle>
            <AlertContents>{error}</AlertContents>
          </Alert>
        ) : null}
        <Field>
          <FieldLabel htmlFor="email">이메일</FieldLabel>
          <Input id="email" type="email" autoComplete="email" />
        </Field>
        <Field>
          <FieldLabel htmlFor="password">비밀번호</FieldLabel>
          <Input id="password" type="password" autoComplete="current-password" />
        </Field>
        <Button type="submit" className="w-full">로그인</Button>
      </form>
    </main>
  );
}

```

### 5.5 토큰 · 컴포넌트 갱신 명령

```bash
# Storybook 최신 컴포넌트 한 개 → 항상 ui/ 아래
cp -R "$SB/src/components/button" src/components/ui/
# path alias 교체 재실행 (Step 5)

# 선택 A — Storybook 최신 tokens.css
cp "$SB/src/styles/tokens.css" src/styles/
cp "$SB/src/styles/clabi-theme.css" src/styles/

# 선택 B — tokens.json 기준 재빌드 (파일 교체 후)
# cp "$SB/tokens/tokens.json" tokens/tokens.json
# 또는: cp ~/Downloads/tokens.json tokens/tokens.json
# node scripts/build-tokens.mjs --build
```

---

## 6. Do / Don’t (한눈에)

| Do | Don’t |
|----|--------|
| `git clone` Storybook 만 | `clabi_ds` clone · `../clabi_ds` 가정 |
| Storybook `tokens/tokens.json` 또는 다운로드한 JSON 사용 | 토큰 때문에 DS 전체 클론 |
| **`src/components/ui/<name>` 에 폴더 통째 복사** | 카탈로그 · `shared/` · `src/components/button`(ui 생략) |
| 내부 import `@/components/ui/…` 로 교체 | 복사본에 `@/components/field` 그대로 두기 |
| `npm i clsx` (+ Radix) | 컴포넌트에 `cn` / shared 유틸 경로 |
| globals에서 동일 변수명 재정의 | `tokens.css` / `tokens.json` 값을 앱에서 손편집 |
| export/types 읽고 구현 | props 추측·구 버전 API 가정 |
| 형제 의존도 `ui/` 아래 | Alert만 복사하고 Button 누락 |

---

## 7. Agent Self-Check (작업 종료 전)

- [ ] Storybook: `gitlab.clabi.co.kr/clabi/projects/storybook` 기준 소스 참고
- [ ] **clabi_ds 미클론**. 토큰은 Storybook `tokens/tokens.json` 또는 다운로드 JSON
- [ ] `src/styles/tokens.css` + `clabi-theme.css` 존재, globals import 순서 정상
- [ ] DS 컴포넌트는 **`src/components/ui/<name>`** 에만 있음 (`ui` 생략 경로 없음)
- [ ] 컴포넌트 내부 import가 `@/components/ui/…` (형제 참조 포함)
- [ ] 앱 코드 import도 `@/components/ui/…`, `@/shared` 없음
- [ ] 토큰 갱신 시 A(CSS 재복사) 또는 B(JSON 교체 후 `--build`)
- [ ] 형제 의존·Radix peer·`clsx` 충족
- [ ] 브랜드 색은 globals 재정의 (또는 기본 토큰 유지)
- [ ] `npm run build` 통과
- [ ] 화면이 토큰 기반 유틸 사용 (필요 시 인스턴스 className 예외만)

---


## 8. Quick Reference

```text
# Storybook (UI · tokens 기준 — clone 이 저장소만)
https://gitlab.clabi.co.kr/clabi/projects/storybook.git
git@gitlab.clabi.co.kr:clabi/projects/storybook.git

# 토큰 경로 (Storybook 안)
tokens/tokens.json          # 원본 JSON
src/styles/tokens.css       # 빌드 산출
scripts/build-tokens.mjs    # --build 로 JSON → CSS

# Suggested local layout (clabi_ds 없음)
<path>/storybook
<path>/<consumer-app>      # 이 가이드로 작업. UI_GUIDE.md 는 Storybook 레포에 두고 복사해 써도 됨
```

### tokens.json 업데이트 요약 (소비 앱)

```bash
# 경로 준비 (최초 1회)
mkdir -p tokens scripts
cp "$SB/scripts/build-tokens.mjs" scripts/
cp "$SB/src/styles/clabi-theme.css" src/styles/

# JSON 확보 (둘 중 하나)
cp "$SB/tokens/tokens.json" tokens/tokens.json
# 또는: 팀에서 받은 파일로 덮어쓰기
# cp ~/Downloads/tokens.json tokens/tokens.json

# 재빌드
node scripts/build-tokens.mjs --build
```

---

*소비 앱 에이전트 컨텍스트용 가이드. Storybook 레포에 유지하고, 킥오프 시 이 파일을 포함한다.*