@t3q-ui/design-system 0.0.0-stage → 1.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1424 @@
1
+ # TDS 컴포넌트·패턴 참조 (생성물)
2
+
3
+ `scripts/mcp/build-skill-reference.mjs` 가 Storybook 매니페스트에서 만든다 — **손으로 고치지 않는다**. 64개(패턴 7 · 컴포넌트 57).
4
+ MCP `docs-list` / `docs-show <id>` / `docs-show-story` 와 같은 내용이다. 패턴은 `@t3q-ui/design-system/patterns`, 컴포넌트는 `@t3q-ui/design-system` 에서 import 한다. props 표에는 설명이 있는 prop 만 있다 — 전체 타입은 `node_modules/@t3q-ui/design-system/dist/*.d.ts`.
5
+
6
+ ## 목차
7
+
8
+ - **patterns**: DashboardLayout `patterns-dashboardlayout` · DetailPage `patterns-detailpage` · FormDialog `patterns-formdialog` · FormDrawer `patterns-formdrawer` · FormPage `patterns-formpage` · ListPage `patterns-listpage` · EmptyState `patterns-states`
9
+ - **actions**: Button `components-actions-button` · DropdownMenu `components-actions-dropdownmenu` · IconButton `components-actions-iconbutton` · ToggleGroup `components-actions-togglegroup`
10
+ - **data-display**: Accordion `components-data-display-accordion` · Alert `components-data-display-alert` · Badge `components-data-display-badge` · EmptyBoxIllustration `components-data-display-emptyboxillustration` · EventList `components-data-display-eventlist` · LucideIcon `components-data-display-lucideicon` · MetaItem `components-data-display-metaitem` · Progress `components-data-display-progress` · Skeleton `components-data-display-skeleton` · Spinner `components-data-display-spinner` · Tag `components-data-display-tag`
11
+ - **inputs**: Calendar `components-inputs-calendar` · Checkbox `components-inputs-checkbox` · CheckboxSection `components-inputs-checkboxsection` · DatePicker `components-inputs-datepicker` · FormItem `components-inputs-form` · IconSelect `components-inputs-iconselect` · Label `components-inputs-label` · MultiSelect `components-inputs-multiselect` · Radio `components-inputs-radio` · RadioGroup `components-inputs-radiogroup` · RadioSection `components-inputs-radiosection` · Range `components-inputs-range` · RangeSlider `components-inputs-rangeslider` · Select `components-inputs-select` · Switch `components-inputs-switch` · SwitchSection `components-inputs-switchsection` · Textarea `components-inputs-textarea` · TextField `components-inputs-textfield` · TimePicker `components-inputs-timepicker`
12
+ - **layout**: Card `components-layout-card` · Panel `components-layout-panel` · Separator `components-layout-separator` · Stack `components-layout-stack`
13
+ - **navigation**: Breadcrumb `components-navigation-breadcrumb` · PageHeader `components-navigation-pageheader` · PageHeaderRefreshButton `components-navigation-pageheaderrefreshbutton` · Pagination `components-navigation-pagination` · Sidebar `components-navigation-sidebar` · Tabs `components-navigation-tabs`
14
+ - **overlay**: AlertDialogContent `components-overlay-alertdialog` · Command `components-overlay-command` · DialogContent `components-overlay-dialog` · DrawerContent `components-overlay-drawer` · PopoverContent `components-overlay-popover` · Toaster `components-overlay-toast` · Tooltip `components-overlay-tooltip`
15
+ - **table**: AgGridTable `components-table-aggridtable` · ColumnFilter `components-table-columnfilter` · ColumnVisibilityFilter `components-table-columnvisibilityfilter` · ParameterTable `components-table-parametertable` · Table `components-table-table` · TableFilterMultiSelect `components-table-tablefilter`
16
+
17
+ # 패턴 (화면 단위 — 먼저 고른다)
18
+
19
+ ## DashboardLayout (`patterns-dashboardlayout`)
20
+
21
+ 대시보드 화면 패턴. PageHeader → 요약 타일(`stats`) → DashboardSection(제목 + 패널 그리드 또는 표) 을 세로로 배치한다.
22
+
23
+ 언제 쓰나: 프로젝트 홈·리소스 모니터링처럼 "지금 상태를 숫자·차트로 한눈에" 보여 주고 각 섹션에서 세부 화면으로 들어가는 화면. 값·시계열·갱신 주기는 소비 앱이 가진다.
24
+ 언제 안 쓰나: 한 리소스의 목록(→ ListPage)·상세(→ DetailPage), 설정 입력(→ FormPage). 섹션 하나뿐이면 PageHeader + Card 로 충분하다.
25
+ 대신/함께: 패널 내용(차트·표)은 `DashboardPanel` 의 children 으로 넣고 차트 라이브러리는 소비 앱이 고른다. 폴링·갱신 시각 표시는 `titleRight` 슬롯에 `PageHeaderRefreshButton` 으로. 표 섹션은 DashboardSection 안에 Table 또는 AgGridTable 을 그대로 넣는다. 사용률 타일은 `ratio`(진행 막대)·`tone`(80% 초과면 warning) 을 준다. 전체를 못 불러왔으면 `error`(+`onRetry`), 데이터가 전혀 없으면 `empty` — 빨간 Alert·`<div>` 로 직접 만들지 않는다.
26
+
27
+ | prop | 타입 | 기본값 | 설명 |
28
+ |---|---|---|---|
29
+ | `titleRight` | `ReactNode` | | Right-aligned slot next to the title (e.g. a secondary title action). |
30
+ | `actions` | `ReactNode` | | 주요 행동 버튼. 제목 줄 오른쪽 (ListPage·DetailPage 와 같은 자리, 2026-10-19 14차) |
31
+ | `stats` | `DashboardStat[]` | | 맨 위 요약 타일. 2열, 넓은 화면(xl)에서 4열, 간격 12px |
32
+ | `loading` | `boolean` | | true 면 타일 숫자 자리에 Skeleton. 패널 로딩은 각 DashboardPanel 의 `loading` |
33
+ | `error` | `ReactNode` | | 주면 타일·섹션 대신 오류 상자(ErrorState). `onRetry` 가 있으면 "다시 시도" 버튼 |
34
+ | `onRetry` | `(() => void)` | | error 상태의 "다시 시도" 클릭 |
35
+ | `empty` | `ReactNode` | | 보여 줄 데이터가 전혀 없을 때 타일·섹션 대신 없음 상자. true 면 기본 문구 "데이터가 없습니다." 패널 하나만 비었으면 DashboardPanel 의 `empty` |
36
+ | `children` | `ReactNode` | | DashboardSection 들. 세로로 32px 간격 |
37
+
38
+ 예시 — story "Default — 타일 4 + 패널 4 + 최근 표":
39
+
40
+ ```tsx
41
+ const Default = () => <DashboardDemo />;
42
+ ```
43
+
44
+ ## DetailPage (`patterns-detailpage`)
45
+
46
+ | prop | 타입 | 기본값 | 설명 |
47
+ |---|---|---|---|
48
+ | `meta` | `ReactNode` | | 제목 아래 메타. 상세 화면의 핵심 정보는 여기 한 곳 — 여러 개면 `MetaGroup columns={3}` + `MetaItem`(2026-10-30 사용자 결정: 메타 자리를 두 개로 나누지 않는다) |
49
+ | `titleRight` | `ReactNode` | | Right-aligned slot next to the title (e.g. a secondary title action). |
50
+ | `primaryAction` | `DetailPageAction` | | 주 행동 1개 (예: 배포·새 추론 서비스). `primaryBold` 로 맨 오른쪽에 그린다 |
51
+ | `secondaryActions` | `DetailPageAction[]` | | 보조 행동들 (예: 다운로드·복제·새 버전). `secondary` 로 주 행동 왼쪽에 준 순서대로 — 2개까지만 보이고 3번째부터는 "더보기" 메뉴로 접힌다(2026-10-19 14차) |
52
+ | `deleteAction` | `DetailPageAction` | | 삭제. 항상 "더보기" 메뉴 안(구분선 아래 danger 항목)에 둔다 — 빨간 버튼을 첫 화면에 펼치지 않는다(2026-10-19 14차). 확인 Dialog 는 소비 앱이 onClick 에서 연다 |
53
+ | `actions` | `ReactNode` | | 버튼 묶음을 직접 구성할 때만 (드롭다운·툴팁 등). 주면 위 세 슬롯 대신 이것을 그대로 그린다 |
54
+ | `actionsNote` | `ReactNode` | | **쓰지 않는다**(2026-10-18 11차, 사용자 결정) — 본문 위 안내 한 줄이 4라운드 연속 "떠 있다" 로 감점. 비활성 버튼은 비활성으로만 두고, 상태 자체의 안내가 필요하면 `banner`. 호환용으로 남긴 prop |
55
+ | `banner` | `ReactNode` | | 헤더 아래, 본문 위에 놓는 알림 슬롯 (예: 삭제 예정·권한 안내 Alert). 상태와 무관하게 항상 그린다 |
56
+ | `loading` | `boolean` | | true 면 본문 자리에 Skeleton. error·notFound 보다 우선 |
57
+ | `error` | `ReactNode` | | 주면 본문 대신 오류 상자(ErrorState: 아이콘·"불러오지 못했습니다"·이 내용). `onRetry` 가 있으면 "다시 시도" 버튼을 함께 그린다 |
58
+ | `onRetry` | `(() => void)` | | error 상태의 "다시 시도" 클릭 |
59
+ | `notFound` | `ReactNode` | | 항목이 없을 때 본문 대신 없음 상자(EmptyState — 상자 일러스트, 2026-10-13 부터 목록·대시보드와 같은 그림). true 면 기본 문구 "항목을 찾을 수 없습니다." + 안내 한 줄 |
60
+ | `aside` | `ReactNode` | | **호환용** — 새 화면은 핵심 정보를 헤더 `meta`(`MetaGroup columns={3}`)에 둔다(2026-10-27 사용자 결정 A: 오른쪽 패널은 내용이 짧아 첫 화면 아래가 비는 것이 홀드아웃 9~12 연속 지적, 2026-10-30 요약 상자 대신 메타 자리 하나로). 주면 넓은 화면(xl)에서 본문 2 : 패널 1, 좁은 화면에서는 본문 아래 |
61
+ | `asideTitle` | `ReactNode` | `기본 정보` | 패널 제목(Panel 머리글, 기본 "기본 정보"). 패턴이 그리므로 `aside` 안에 같은 제목을 다시 쓰지 않는다(2026-10-27 홀드아웃 12: "작업 정보" 가 연달아 두 번) |
62
+ | `width` | `enum` | `full` | 본문 폭. full = 가로 전체(정보 탭), panel = 가운데 900px (설정 탭). 기본 full |
63
+ | `children` | `ReactNode` | | 본문 (현재 탭 내용). 탭 상태와 라우팅은 소비 앱이 가진다 |
64
+
65
+ 예시 — story "Default — 헤더 요약 상자(키-값 3열) + 본문 전폭 (2026-10-27 A)":
66
+
67
+ ```tsx
68
+ const Default = () => <DetailPageDemo />;
69
+ ```
70
+
71
+ ## FormDialog (`patterns-formdialog`)
72
+
73
+ 모달 폼 패턴. 제목 → 필드 → 오류 → 취소/저장.
74
+
75
+ 언제 쓰나: 이름 변경, 태그 추가처럼 필드 1~4개로 끝나는 짧은 입력.
76
+ 언제 안 쓰나: 목록 맥락이 필요한 한 화면짜리 폼은 FormDrawer, 스크롤이 생기는 긴 폼은 FormPage, 되돌릴 수 없는 확인만 받을 때는 AlertDialog.
77
+ 대신/함께: 제출 중에는 닫기 버튼이 사라지고 Esc·오버레이·취소로도 닫히지 않는다.
78
+
79
+ | prop | 타입 | 기본값 | 설명 |
80
+ |---|---|---|---|
81
+ | `title` (필수) | `string` | | 제목 |
82
+ | `description` | `ReactNode` | | 제목 아래 설명 |
83
+ | `form` (필수) | `UseFormReturn<TValues>` | | react-hook-form `useForm()` 반환값. 검증 스키마·기본값은 소비 앱이 소유한다 |
84
+ | `onSubmit` (필수) | `(values: TValues) => void \| Promise<void>` | | 검증 통과 후 호출. Promise 를 돌려주면 끝날 때까지 제출 중 상태가 된다 |
85
+ | `submitLabel` | `string` | | 제출 버튼 라벨. 기본 "저장" |
86
+ | `cancelLabel` | `string` | | 취소 버튼 라벨. 기본 "취소" |
87
+ | `submitting` | `boolean` | | 외부 저장 중 상태(뮤테이션 등). 버튼 loading·disabled, 취소·닫기 차단 |
88
+ | `error` | `ReactNode` | | 서버 오류 등 폼 전체 오류. 버튼 위에 Alert(danger) 로 표시 |
89
+ | `submitMode` | `enum` | | 제출 버튼 활성 규칙. `always`(기본) = 제출 중이 아니면 항상 눌리고, 누르면 react-hook-form 이 검증해 틀린 항목마다 FormMessage 를 보인다. `whenValid` = 모든 입력이 유효해질 때까지 잠근다(2026-10-12 전 기본). "누르면 검증해 이유 표시" 가 흔한 폼 관례라 기본을 바꿨다 |
90
+ | `children` (필수) | `ReactNode` | | FormSection 으로 묶은 FormField 들 |
91
+ | `open` (필수) | `boolean` | | 열림 상태 (controlled) |
92
+ | `onOpenChange` (필수) | `(open: boolean) => void` | | 열림 상태 변경. 제출 중에는 닫기 요청(false)이 전달되지 않는다 |
93
+ | `width` | `"medium" \| "small" \| "large" \| "xlarge" \| null` | | 모달 너비. 기본 small |
94
+
95
+ 예시 — story "Default — 열기 버튼 → 모달":
96
+
97
+ ```tsx
98
+ const Default = () => <FormDialogDemo />;
99
+ ```
100
+
101
+ ## FormDrawer (`patterns-formdrawer`)
102
+
103
+ 사이드 패널 폼 패턴. 목록 화면 위에 오른쪽 Drawer 로 생성·수정 폼을 띄운다. 제목 → FormSection 들(스크롤) → 오류 → 취소/저장.
104
+
105
+ 언제 쓰나: 목록 맥락을 유지해야 하고 **Drawer 한 화면 안에 들어가는** 폼(섹션 1~2개 — 예: 태그·설명·담당자 수정, 짧은 생성).
106
+ 언제 안 쓰나: 필드 1~4개의 짧은 입력은 FormDialog. 스크롤이 생기거나 섹션이 셋 이상이거나 여러 단계인 긴 폼(실행환경·파이프라인·모델 생성)은 FormPage — 긴 폼을 Drawer 에 넣지 않는다(2026-10-13, Carbon·Shopify 관례).
107
+ 대신/함께: 제출 중에는 Esc·오버레이·취소·닫기 버튼으로 닫히지 않는다. 닫을 때 폼을 reset 할지는 소비 앱이 `onOpenChange` 에서 정한다(초안을 보존하려면 reset 하지 않는다).
108
+
109
+ | prop | 타입 | 기본값 | 설명 |
110
+ |---|---|---|---|
111
+ | `title` (필수) | `string` | | 제목 |
112
+ | `description` | `ReactNode` | | 제목 아래 설명 |
113
+ | `form` (필수) | `UseFormReturn<TValues>` | | react-hook-form `useForm()` 반환값. 검증 스키마·기본값은 소비 앱이 소유한다 |
114
+ | `onSubmit` (필수) | `(values: TValues) => void \| Promise<void>` | | 검증 통과 후 호출. Promise 를 돌려주면 끝날 때까지 제출 중 상태가 된다 |
115
+ | `submitLabel` | `string` | | 제출 버튼 라벨. 기본 "저장" |
116
+ | `cancelLabel` | `string` | | 취소 버튼 라벨. 기본 "취소" |
117
+ | `submitting` | `boolean` | | 외부 저장 중 상태(뮤테이션 등). 버튼 loading·disabled, 취소·닫기 차단 |
118
+ | `error` | `ReactNode` | | 서버 오류 등 폼 전체 오류. 버튼 위에 Alert(danger) 로 표시 |
119
+ | `submitMode` | `enum` | | 제출 버튼 활성 규칙. `always`(기본) = 제출 중이 아니면 항상 눌리고, 누르면 react-hook-form 이 검증해 틀린 항목마다 FormMessage 를 보인다. `whenValid` = 모든 입력이 유효해질 때까지 잠근다(2026-10-12 전 기본). "누르면 검증해 이유 표시" 가 흔한 폼 관례라 기본을 바꿨다 |
120
+ | `children` (필수) | `ReactNode` | | FormSection 으로 묶은 FormField 들 |
121
+ | `open` (필수) | `boolean` | | 열림 상태 (controlled) |
122
+ | `onOpenChange` (필수) | `(open: boolean) => void` | | 열림 상태 변경. 제출 중에는 닫기 요청(false)이 전달되지 않는다 |
123
+ | `side` | `"left" \| "right" \| "top" \| "bottom" \| null` | | 패널이 나오는 방향. 기본 right |
124
+ | `size` | `"medium" \| "small" \| "large" \| "xLarge" \| null` | | 패널 너비. 기본 large |
125
+
126
+ 예시 — story "Default — 열기 버튼 → 패널":
127
+
128
+ ```tsx
129
+ const Default = () => <FormDrawerDemo />;
130
+ ```
131
+
132
+ ## FormPage (`patterns-formpage`)
133
+
134
+ 독립 화면 폼 패턴. PageHeader(제목·설명) → FormSection 들(사이에 연한 구분선, 왼쪽 축 720px) → 오류 → 취소/저장(FormFooter; `aside` 가 있으면 패널 바닥). **헤더 내용과 본문은 같은 가운데 폭 컨테이너**에 둔다 — 검토 패널이 있으면 `--max-w-page-content`(1152 = 720 + 24 + 408), 없으면 폼 폭 720. 띠(헤더 배경·아래 선)는 전폭 그대로(2026-10-26 22차: 전폭 왼쪽 정렬에서는 긴 폼 옆 짧은 패널 아래가 "unused space" 로 감점됐고, 내용을 가운데 폭에 가둔 변형만 원문 심판 layout 3.5 → 4.0 — 기준선 폼들과 같은 구조). 검토·요약 패널(`aside`)은 **항상** 폼 옆에 둔다 — 입력값 요약·예상 비용처럼 제출 전에 확인할 것(2026-10-17 8차 C2, 예제 form-page.tsx).
135
+
136
+ 언제 쓰나: 스크롤이 생기는 긴 폼 — 섹션 3개 이상, 한 화면을 넘기거나, 여러 단계. 전용 URL 을 가진 생성·수정 화면의 기본. 검증 스키마·기본값·payload 는 소비 앱의 `useForm` 이 소유한다.
137
+ 언제 안 쓰나: 목록 맥락을 유지해야 하고 Drawer 한 화면 안에 들어가는 폼(섹션 1~2개)은 FormDrawer, 필드 1~4개의 짧은 입력은 FormDialog. 긴 폼을 Drawer·Dialog 에 넣지 않는다.
138
+ 대신/함께: 머리는 목록·상세·대시보드와 같은 PageHeader(h1)라 FormSection 제목은 h2 가 된다. 바탕은 흰색이 기본이고 `canvas='subtle'` 은 측정용 옵트인(회색 캔버스 + 카드)이다. 저장 버튼은 기본(`submitMode='always'`)으로 항상 눌리고 누르면 검증해 틀린 항목마다 FormMessage 를 보인다 — 유효해질 때까지 잠그려면 `submitMode='whenValid'`. `onSubmit` 이 Promise 면 끝날 때까지 loading — 다만 제출 중 상태는 앱이 `submitting` 으로도 **코드에 드러내라**(접근성·심사 모두 코드에서 보여야 한다, 2026-10-16). 숫자 입력의 단위는 `TextField elemAfterInput='개'`. 선택지 목록을 못 불러왔으면 폼을 지우지 말고 그 필드 자리에 ErrorState 를 둔다.
139
+
140
+ | prop | 타입 | 기본값 | 설명 |
141
+ |---|---|---|---|
142
+ | `title` (필수) | `string` | | 제목 |
143
+ | `description` | `ReactNode` | | 제목 아래 설명 |
144
+ | `form` (필수) | `UseFormReturn<TValues>` | | react-hook-form `useForm()` 반환값. 검증 스키마·기본값은 소비 앱이 소유한다 |
145
+ | `onSubmit` (필수) | `(values: TValues) => void \| Promise<void>` | | 검증 통과 후 호출. Promise 를 돌려주면 끝날 때까지 제출 중 상태가 된다 |
146
+ | `submitLabel` | `string` | | 제출 버튼 라벨. 기본 "저장" |
147
+ | `cancelLabel` | `string` | | 취소 버튼 라벨. 기본 "취소" |
148
+ | `submitting` | `boolean` | | 외부 저장 중 상태(뮤테이션 등). 버튼 loading·disabled, 취소·닫기 차단 |
149
+ | `error` | `ReactNode` | | 서버 오류 등 폼 전체 오류. 버튼 위에 Alert(danger) 로 표시 |
150
+ | `submitMode` | `enum` | | 제출 버튼 활성 규칙. `always`(기본) = 제출 중이 아니면 항상 눌리고, 누르면 react-hook-form 이 검증해 틀린 항목마다 FormMessage 를 보인다. `whenValid` = 모든 입력이 유효해질 때까지 잠근다(2026-10-12 전 기본). "누르면 검증해 이유 표시" 가 흔한 폼 관례라 기본을 바꿨다 |
151
+ | `children` (필수) | `ReactNode` | | FormSection 으로 묶은 FormField 들 |
152
+ | `titleRight` | `ReactNode` | | Right-aligned slot next to the title (e.g. a secondary title action). |
153
+ | `onCancel` | `(() => void)` | | 취소 버튼 핸들러. 없으면 취소 버튼을 그리지 않는다 |
154
+ | `requiredLegend` | `boolean` | | true 면 폼 맨 위에 "* 표시는 필수 입력 항목입니다" 안내 한 줄 |
155
+ | `canvas` | `enum` | `plain` | 본문 바탕. 'plain'(기본)은 흰 캔버스에 섹션 구분선. 'subtle' 은 연한 회색 캔버스 위에 폼 전체를 흰 Card 하나로 — UIClip 폼 진단(evals/metrics-spec §16.7)용 옵트인이며 캔버스 정책(DESIGN.md)은 흰색 그대로다 |
156
+ | `align` | `enum` | `start` | 폼 열(720px)의 자리. 'start'(기본)는 헤더 제목과 같은 왼쪽 축에서 시작, 'center' 는 본문 가운데(2026-10-16 이슈 47 전의 모양 — 제목은 왼쪽, 폼은 가운데라 축이 어긋난다고 원문 심판 10회 감점). 2026-10-26 22차부터 헤더 내용과 본문이 같은 가운데 폭 컨테이너(패널 있으면 1152, 없으면 720) 안에 들어가므로 이 값의 효과는 거의 없다(호환용). `aside` 가 있으면 폼 열은 왼쪽 |
157
+ | `aside` | `ReactNode` | | 폼 오른쪽의 검토·요약 패널(입력값 요약, 비용 미리보기 등). 넓은 화면에서 폼 열 옆에 세로선으로 구분돼 화면을 따라 내려오고(sticky), 좁으면 폼 아래로. **취소·제출 버튼은 이 패널 바닥**에 놓인다(폼 끝에는 없음 — 2026-10-20 16차 D1, 긴 폼 옆에서 패널 아래가 비고 제출이 멀다는 두 심판 지적). FormPage 를 flex 로 감싸 옆에 패널을 두면 헤더가 화면 폭을 다 못 써 구분선이 끊긴다 — 반드시 이 슬롯으로 |
158
+ | `asideTitle` | `ReactNode` | `검토` | 검토 패널 제목(h2). 기본 '검토' |
159
+
160
+ 예시 — story "Invalid — 검증 실패 표시 (빈 채 저장을 누른 뒤와 같은 상태)":
161
+
162
+ ```tsx
163
+ const Invalid = () => <FormPageDemo invalid />;
164
+ ```
165
+
166
+ ## ListPage (`patterns-listpage`)
167
+
168
+ 목록 화면 패턴. PageHeader(제목·설명·주요 버튼·필터 줄) → AgGridTable → Pagination 을 한 번에 배치한다.
169
+
170
+ 언제 쓰나: 실행환경·모델·파이프라인·데이터셋처럼 "검색/필터 → 표 → 상세 진입" 흐름이 있는 리소스 목록. 컬럼·행·필터 컨트롤·데이터 조회는 소비 앱이 넘기고, 패턴은 배치와 로딩·빈 결과·오류 상태만 책임진다.
171
+ 언제 안 쓰나: 차트·요약 카드 중심의 대시보드, 한 항목의 상세 화면, 표가 없는 설정 화면.
172
+ 대신/함께: 행 클릭 뒤 상세는 `onRowClick` 에서 상세 페이지로 이동하는 것이 기본이고, 목록 맥락을 유지해야 할 때는 `detail` 슬롯으로 목록 오른쪽에 **나란히** 연다(표가 좁아지고 목록은 계속 보인다, 닫기 버튼·Escape, 비활성 사유는 `detail.note`). 표를 좁히면 안 될 때만 `<Drawer modal={false}>`(덮는 옆 패널)로(어느 쪽인지는 화면 설계가 정한다). 필터가 바뀌면 소비 앱이 `pagination.page` 를 1로 되돌린다. `onResetFilters` 는 항상 넘기고 `filtersActive` 로 보일지를 정한다(필터가 걸린 채 0건이면 빈 결과 가운데에도 "필터 초기화" 가 나온다). 상세를 Drawer 로 열면 `activeRowId` 로 그 행을 표시한다. 주 식별자 열은 `primaryField`(행 클릭이 있으면 기본 `'name'`), 표 위 제목·건수는 `tableTitle`·`pagination.total`. 상태처럼 선택지가 3~6개인 다중 선택 필터는 `quickFilter` 로 표 왼쪽 위에 세그먼트로 둔다(드롭다운에 숨기지 않는다). 본문은 제목을 이름으로 가진 `section` landmark 다(소비 앱의 `<main>` 안에 둔다). 표는 한 페이지의 행을 다 보이도록 행 수만큼 자란다(`domLayout='autoHeight'` 기본) — 화면 높이에 맞춘 안쪽 스크롤이 필요하면 `tableProps` 로 `domLayout='normal'` 과 `containerStyle` 을 넘긴다.
173
+
174
+ | prop | 타입 | 기본값 | 설명 |
175
+ |---|---|---|---|
176
+ | `title` (필수) | `string` | | 페이지 제목 (h1) |
177
+ | `description` | `ReactNode` | | 제목 아래 설명 |
178
+ | `titleRight` | `ReactNode` | | 제목 오른쪽 슬롯 (예: 새로고침 버튼·마지막 갱신 시각) |
179
+ | `actions` | `ReactNode` | | 주요 행동 버튼 (보통 1개). 제목 줄 오른쪽(PageHeader `actions`) — 2026-10-19 14차: 페이지 수준 행동은 제목 줄(Polaris 인덱스 페이지·GitHub Issues·기준선과 같은 자리). 1.22.0~1.47.4 의 필터 줄 왼쪽(SharePoint 계열)은 2026-10-13 UIClip(폐기된 자) 근거였다 |
180
+ | `filters` | `ReactNode` | | 검색·필터 컨트롤. 헤더 오른쪽 아래 (TableFilter*, ColumnFilter 등) |
181
+ | `onResetFilters` | `(() => void)` | | 필터를 되돌리는 핸들러. 주면 빠른 필터 줄(표 바로 위) 오른쪽 끝에 "필터 초기화" 외곽선 버튼이 **항상** 자리를 잡는다(빠른 필터·건수가 없으면 filters 줄 끝). 검색창은 그래서 표 오른쪽 끝까지 닿는다(2026-10-18 10차). 필터가 걸렸을 때만 조건부로 넘기지 말고(빈 결과에서 버튼이 튀어나와 배치가 바뀐다) 항상 넘기고 `filtersActive` 로 보일지를 정한다 |
182
+ | `filtersActive` | `boolean` | | 필터가 하나라도 걸려 있는가. false 면 "필터 초기화" 버튼이 자리만 지킨 채 보이지 않는다(invisible) |
183
+ | `primaryField` | `string \| null` | | 주 식별자 열의 field. 그 열에 `type: 'primary'`(굵게, 행 hover 시 밑줄)를 붙인다. 기본값: `onRowClick` 이 있으면 `'name'`, 없으면 없음. 끄려면 `null` |
184
+ | `secondaryField` | `string` | | 주 식별자 열 아래 **보조 줄**에 보일 행 필드(id·이미지·경로·설명, 점 표기 가능). 열을 하나 더 늘리는 대신 이름 아래 12px 회색 한 줄로 — dl-ui-v3 목록(이름 / 설명)과 같은 모양이고 열이 적어 잘림도 준다(2026-10-18 13차 ⑥). 주 열(`primaryField`, 기본 `name`)에 `cellRenderer` 가 이미 있으면 적용하지 않는다. 주면 행 높이 56(`tableProps.rowHeight` 가 우선) |
185
+ | `tableTitle` | `ReactNode` | | 표 위 제목. **쓰지 않는다** — 헤더 제목(h1)과 중복이고 빠른 필터 옆에서 "tightly packed" 로 감점(2026-10-18 10차, 사용자 결정). 남겨 둔 건 호환용. `pagination.total` 이 있으면 같은 줄 오른쪽에 "총 N개" 가 붙는다(제목·빠른 필터 둘 다 없으면 윗줄 자체를 안 그린다 — 2026-10-27) — 단 `quickFilter` 가 항목마다 `count` 를 보여 주면 "전체 N" 과 같은 숫자라 생략한다 |
186
+ | `quickFilter` | `ListPageQuickFilter` | | 표 왼쪽 위(제목 줄)의 빠른 필터 — 상태처럼 선택지 3~6개의 다중 선택. ToggleGroup(붙어있는 세그먼트)으로 그려 드롭다운을 열지 않아도 선택지가 보인다. 선택지가 많거나 검색이 필요한 축은 `filters` 의 TableFilterMultiSelect 로. `tableTitle` 이 없으면 이 줄을 세그먼트가 대신한다(줄이 늘지 않음) |
187
+ | `activeRowId` | `string \| number \| null` | | 상세가 열린 행의 id (`row.id` 와 비교). 그 행을 표의 선택 색으로 칠해 "지금 어느 행을 보고 있나" 를 보인다. 상세를 닫으면 null |
188
+ | `detail` | `ListPageDetail` | | 행을 눌러 보는 상세 패널. 주면 목록 **오른쪽에 나란히** 그린다(표가 좁아지고 목록은 계속 보인다, 닫기 버튼·Escape). 닫으면 undefined 로. 2026-10-16(이슈 45) — 덮는 `<Drawer modal={false}>` 는 표를 좁히면 안 될 때의 대안 |
189
+ | `columns` (필수) | `ColDef<TRow, any>[]` | | 표 컬럼 정의 (ag-grid ColDef) |
190
+ | `rows` | `TRow[]` | | 현재 페이지 행. undefined 면 표가 로딩 오버레이를 보인다 |
191
+ | `loading` | `boolean` | | true 면 표 자리에 TableSkeleton |
192
+ | `error` | `ReactNode` | | 주면 표 대신 Alert(danger) 로 표시한다 |
193
+ | `emptyState` | `ReactNode \| ListPageEmptyContent` | | 0건일 때 표 안에 그릴 안내. 패턴이 **한 줄 문장**(+ 필터가 걸려 있으면 링크형 "필터 초기화")으로 그린다 — 아이콘·상자는 그리지 않는다(1.29.0 의 상자는 2026-10-13 되돌림). 문자열 또는 `{ title, description }`(이어 붙여 한 줄). 버튼·아이콘은 넣지 않는다. 기본 "데이터가 없습니다" |
194
+ | `pagination` | `{ total: number; page: number; pageSize: number; onChange: (page: number) => vo…` | | 페이지네이션. 없으면 그리지 않는다 |
195
+ | `onSortChange` | `((sort: string) => void)` | | 정렬 변경. 값은 getSortedColumnState 규칙 ('name' \| '-name' \| '') |
196
+ | `onRowClick` | `((row: TRow) => void)` | | 행 클릭 |
197
+ | `tableProps` | `Omit<AgGridTableType, "loading" \| "columnDefs" \| "noRowsOverlayComponent" \| …` | | AgGridTable 에 그대로 넘길 나머지 props (rowHeight, rowSelection 등). 표 높이는 기본 `domLayout='autoHeight'`(행 수만큼) — 고정 높이는 `domLayout='normal'` + `containerStyle` |
198
+
199
+ 예시 — story "Loading — TableSkeleton":
200
+
201
+ ```tsx
202
+ const Loading = () => <ListPageDemo
203
+ title="실행환경"
204
+ description="모델 학습·추론에 쓰는 컨테이너 실행환경 목록입니다."
205
+ columns={[
206
+ {
207
+ field: 'status',
208
+ headerName: '상태',
209
+ width: 110,
210
+ cellRenderer: (props: ICellRendererParams<Env>) => {
211
+ const running = props.data?.status === 'running'
212
+ return (
213
+ <Badge variant={running ? 'primary' : 'danger'}>
214
+ {running ? <Play /> : <XCircle />}
215
+ {running ? '실행 중' : '중지'}
216
+ </Badge>
217
+ )
218
+ },
219
+ },
220
+ // 남는 폭은 이름 열(flex 2, 상한 480)과 글자 열 전부(flex 1)가 나눈다 — 글자 열이 하나뿐이면 2:1 로 나눠도 이름 열이 비어 보인다(2026-10-20 17차·18차).
221
+ // maxWidth 를 주면 넓은 화면에서 표 오른쪽이 빈다. 이미지는 열이 아니라 이름 아래 보조 줄(secondaryField) — 열이 줄어 잘림도 준다(2026-10-18 13차 ⑥)
222
+ { field: 'name', headerName: '이름', flex: 2, minWidth: 220, maxWidth: 480, sortable: true },
223
+ { field: 'owner', headerName: '등록자', flex: 1, minWidth: 120 },
224
+ { field: 'updatedAt', headerName: '수정일시', width: 170, sortable: true },
225
+ ]}
226
+ rows={undefined}
227
+ secondaryField="image"
228
+ loading />;
229
+ ```
230
+
231
+ ## EmptyState (`patterns-states`)
232
+
233
+ 패턴 공통 빈 결과·없음 안내. 가운데 정렬. 아이콘 → 제목 → 설명 → 행동 버튼 순.
234
+ 제목만 있으면 한 줄 안내, 설명까지 있으면 "무엇이 없고 다음에 뭘 하나" 를 두 줄로 — 2026-10-10 진단에서 두 평가자가 모두 더 낫다고 본 구성.
235
+
236
+ 언제 쓰나: 목록이 0건, 선택지가 없음, 항목이 삭제됨처럼 "보여 줄 것이 없다" 를 알릴 때. 패턴(ListPage·DetailPage·DashboardLayout·DashboardPanel)은 `emptyState`·`notFound`·`empty` prop 으로 이것을 그린다.
237
+ 언제 안 쓰나: 불러오기 실패(→ ErrorState), 아직 불러오는 중(→ LoadingState), 입력 검증 실패(→ FormMessage).
238
+ 대신/함께: `<div>` 에 문장만 넣어 직접 만들지 않는다. 다음 행동이 있으면 `action` 에 버튼 하나(예: 필터 초기화·새로 만들기).
239
+
240
+ | prop | 타입 | 기본값 | 설명 |
241
+ |---|---|---|---|
242
+ | `children` | `ReactNode` | | 제목 자리(`title` 과 같다). 기존 호출부 호환용 |
243
+ | `icon` | `ReactNode` | | 제목 위 그림. 기본은 상자 일러스트 `EmptyBoxIllustration`(라이트·다크 토큰, 2026-10-13 — dl-ui-v3 사용자 정보 화면의 빈 패널과 같은 그림). 장식이라 스크린리더에는 읽히지 않는다 |
244
+ | `title` | `ReactNode` | | 무엇이 없는지 한 줄 (예: "조건에 맞는 모델이 없습니다") |
245
+ | `description` | `ReactNode` | | 다음에 할 수 있는 일 (예: "검색어나 필터를 바꾸거나 초기화해 보세요") |
246
+ | `action` | `ReactNode` | | 설명 아래 행동 버튼 (예: 필터 초기화). ag-grid 오버레이 안에서도 눌리게 pointer-events 를 연다 |
247
+
248
+ 예시 — story "Loading — 본문 로딩 자리":
249
+
250
+ ```tsx
251
+ const Loading = () => <LoadingState lines={4} />;
252
+ ```
253
+
254
+ # 컴포넌트 — actions
255
+
256
+ ## Button (`components-actions-button`)
257
+
258
+ 클릭으로 동작을 실행하는 기본 버튼. 텍스트가 있는 모든 행동(생성·저장·취소·링크형 보조 행동)에 쓴다.
259
+
260
+ 언제 쓰나: 화면의 주요 행동은 `primaryBold` 1개, 보조 행동은 `secondaryOutline`·`secondaryLink`, 파괴적 행동은 `danger*`. 비동기 처리 중에는 `loading` 으로 잠근다.
261
+ 언제 안 쓰나: 아이콘만 있는 버튼(→ IconButton), 페이지 이동(→ 링크. 버튼 모양이 꼭 필요하면 `asChild` 로 `<a>` 를 감싼다), 표 안의 상태 표시(→ Badge).
262
+ 대신/함께: 같은 줄에 주요 버튼을 두 개 두지 않는다. 폼 푸터에서는 취소(secondaryOutline) 왼쪽, 저장(primaryBold) 오른쪽.
263
+
264
+ 예시 — story "Danger — Subtle":
265
+
266
+ ```tsx
267
+ const Danger = () => <Button variant="danger">위험 버튼</Button>;
268
+ ```
269
+
270
+ ## DropdownMenu (`components-actions-dropdownmenu`)
271
+
272
+ 예시 — story "Variants — default / danger":
273
+
274
+ ```tsx
275
+ const Variants = () => (
276
+ <DropdownMenu>
277
+ <DropdownMenuTrigger>액션</DropdownMenuTrigger>
278
+ <DropdownMenuContent>
279
+ <DropdownMenuItem variant='default'>편집</DropdownMenuItem>
280
+ <DropdownMenuItem variant='default' disabled>
281
+ 비활성 항목
282
+ </DropdownMenuItem>
283
+ <DropdownMenuSeparator />
284
+ <DropdownMenuItem variant='danger'>삭제</DropdownMenuItem>
285
+ </DropdownMenuContent>
286
+ </DropdownMenu>
287
+ );
288
+ ```
289
+
290
+ ## IconButton (`components-actions-iconbutton`)
291
+
292
+ 아이콘만 있는 버튼. 좁은 자리(툴바·표 행·카드 모서리)의 보조 행동에 쓴다.
293
+
294
+ 언제 쓰나: 새로고침·편집·삭제·닫기처럼 아이콘만으로 뜻이 통하는 행동. 항상 `aria-label` 로 이름을 주고, 필요하면 Tooltip 을 함께 쓴다.
295
+ 언제 안 쓰나: 화면의 주요 행동(→ Button 텍스트), 아이콘 뜻이 모호한 행동(→ Button 에 아이콘+텍스트).
296
+ 대신/함께: Dialog·Drawer 의 닫기 버튼은 각 컴포넌트가 내장하므로 따로 두지 않는다.
297
+
298
+ 예시 — story "Playground":
299
+
300
+ ```tsx
301
+ const Playground = () => <IconButton
302
+ variant="primary"
303
+ shape="normal"
304
+ spacing="normal"
305
+ disabled={false}
306
+ aria-label="추가"><Plus /></IconButton>;
307
+ ```
308
+
309
+ ## ToggleGroup (`components-actions-togglegroup`)
310
+
311
+ 선택지가 모두 보이는 버튼 묶음(세그먼트). 단일 선택(`type='single'`, 기본) 또는 다중 선택(`type='multiple'`).
312
+
313
+ 언제 쓰나: 선택지가 2~6개이고 항상 보여야 할 때 — 상태·심각도 같은 빠른 필터, 보기 전환(목록/카드), 기간(일/주/월). `count` 로 항목별 건수를 보일 수 있다.
314
+ 언제 안 쓰나: 선택지가 많거나(7개 이상) 검색이 필요할 때 → `TableFilterSingleSelect`/`TableFilterMultiSelect`·`Select`. 화면(내용 영역)을 전환할 때 → `Tabs`. 폼의 단일 선택 입력 → `RadioGroup`.
315
+ 대신/함께: 목록 화면의 상태 필터는 `ListPage quickFilter` 가 이 컴포넌트로 표 왼쪽 위에 그린다. 그룹 이름은 `aria-label` 로 준다(기본 이름 없음). 붙어있는 모양은 `variant='outline' spacing={false}`.
316
+
317
+ | prop | 타입 | 기본값 | 설명 |
318
+ |---|---|---|---|
319
+ | `type` | `enum` | `single` | 미지정 시 'single' — 기존 호출부 무수정 호환 (radix와 달리 optional인 이유) |
320
+ | `onValueChange` | `((value: string) => void) \| ((value: string[]) => void)` | | 선택 해제 시 빈 문자열이 전달된다 전체 해제 시 빈 배열이 전달된다 |
321
+ | `variant` | `enum` | `default` | 아이템 스타일. default: 테두리 없음 / outline: 테두리 있음 |
322
+ | `spacing` | `boolean` | `false` | true일 때 아이템 간 gap-1 간격 추가 (outline variant와 조합 가능) |
323
+ | `disabled` | `boolean` | | Whether the group is disabled from user interaction. @defaultValue false |
324
+ | `rovingFocus` | `boolean` | | Whether the group should maintain roving focus of its buttons. @defaultValue true |
325
+
326
+ 예시 — story "Single — 단일 선택 (controlled)":
327
+
328
+ ```tsx
329
+ const Single = () => <SingleExample />;
330
+ ```
331
+
332
+ # 컴포넌트 — data-display
333
+
334
+ ## Accordion (`components-data-display-accordion`)
335
+
336
+ | prop | 타입 | 기본값 | 설명 |
337
+ |---|---|---|---|
338
+ | `value` | `string \| string[]` | | The controlled stateful value of the accordion item whose content is expanded. The controlled stateful value of the accordion items whose contents are expanded. |
339
+ | `defaultValue` | `string \| string[]` | | The value of the item whose content is expanded when the accordion is initially rendered. Use `defaultValue` if you do not need to control the state of an accordion. The value of the items whose contents are expanded when the accordion is initially rendered. Use `defaultValue` if you do not need to control the state of an accordion. |
340
+ | `onValueChange` | `((value: string) => void) \| ((value: string[]) => void)` | | The callback that fires when the state of the accordion changes. |
341
+ | `collapsible` | `boolean` | `false` | Whether an accordion item can be collapsed after it has been opened. |
342
+ | `disabled` | `boolean` | | Whether or not an accordion is disabled from user interaction. @defaultValue false |
343
+ | `orientation` | `enum` | `vertical` | The layout in which the Accordion operates. |
344
+ | `dir` | `enum` | | The language read direction. |
345
+
346
+ 예시 — story "WithoutIcon — 아이콘 없이 텍스트만":
347
+
348
+ ```tsx
349
+ const WithoutIcon = () => (
350
+ <Accordion type='single' collapsible className='w-[527px]'>
351
+ <AccordionItem value='1'>
352
+ <AccordionTrigger>Title</AccordionTrigger>
353
+ <AccordionContent>{DESCRIPTION}</AccordionContent>
354
+ </AccordionItem>
355
+ <AccordionItem value='2'>
356
+ <AccordionTrigger>Title</AccordionTrigger>
357
+ <AccordionContent>{DESCRIPTION}</AccordionContent>
358
+ </AccordionItem>
359
+ </Accordion>
360
+ );
361
+ ```
362
+
363
+ ## Alert (`components-data-display-alert`)
364
+
365
+ 화면 안에 남아 있는 상태·안내 박스. 섹션 상단의 경고·오류·정보 안내에 쓴다.
366
+
367
+ 언제 쓰나: 폼 전체 오류(서버 거부), 권한·만료 같은 지속 상태, 주의가 필요한 설명, **폼 섹션 안의 부분 실패**(선택지 목록을 못 불러옴 — `variant='danger'` 한 줄 제목 + 설명 + `AlertActions` 의 "다시 시도" 링크, 2026-10-13). `variant` 로 의미를 주고 `AlertTitle`·`AlertDescription` 으로 구성한다.
368
+ 언제 안 쓰나: 잠깐 보이면 되는 결과(→ Toast), 필드 하나의 오류(→ FormMessage), 상태 한 단어(→ Badge), 화면·패널 본문을 통째로 못 불러온 것(→ ErrorState 상자).
369
+ 대신/함께: 아이콘을 첫 자식으로 넣으면 자동으로 왼쪽 열에 놓인다. 행동 버튼은 `AlertActions` 에(링크형 `Button variant='secondaryLink'` 기본, 버튼 사이 8px).
370
+
371
+ 예시 — story "WithoutIcon — 아이콘 없이":
372
+
373
+ ```tsx
374
+ const WithoutIcon = () => (
375
+ <Alert variant='info'>
376
+ <AlertTitle>안내</AlertTitle>
377
+ <AlertDescription>아이콘이 없으면 첫 번째 열 폭이 0 으로 접힙니다.</AlertDescription>
378
+ </Alert>
379
+ );
380
+ ```
381
+
382
+ ## Badge (`components-data-display-badge`)
383
+
384
+ 상태·분류를 색으로 보여 주는 읽기 전용 표시. 표의 상태 열, 제목 옆 상태에 쓴다.
385
+
386
+ 언제 쓰나: 실행 중/중지/오류 같은 상태(`success`·`danger`·`warning`·`info`), 개수·등급 표시. 의미 variant 를 쓰고 색을 직접 바꾸지 않는다. `appearance='outline'` 은 흰 바탕 + 1px accent 테두리.
387
+ 언제 안 쓰나: 사용자가 붙이고 뗄 수 있는 라벨(→ Tag), 클릭해 동작하는 것(→ Button), 지나가는 알림(→ Toast).
388
+ 대신/함께: **상태 표시는 Badge 에 아이콘 + 글자** — `<Badge variant='success'><LucideIcon iconComponentName='circle-check' />완료</Badge>`(아이콘은 자식으로 바로, 16px·글자색). 별도 상태 컴포넌트는 없다(2026-10-30). 크기는 기본(`size` 를 주지 않는다). 상태값 → variant·아이콘 매핑은 소비 앱의 상수 한 곳에 둔다.
389
+
390
+ | prop | 타입 | 기본값 | 설명 |
391
+ |---|---|---|---|
392
+ | `size` | `"medium" \| "small" \| null` | | small(기본): 20px·12px — 상태·건수·탭·개수 뱃지 모두 이 크기. medium: 24px·14px — 특별히 크게 보여야 할 때만 |
393
+ | `appearance` | `"tint" \| "outline" \| null` | | tint(기본): 연한 채움. outline: 흰 바탕 + 1px accent 테두리(subtle 변형에만 — 2026-10-17 이슈 47 6차, 상태 뱃지용) |
394
+
395
+ 예시 — story "Etc (분류·태그성 정보)":
396
+
397
+ ```tsx
398
+ const Etc = () => <Badge variant="etc">Etc</Badge>;
399
+ ```
400
+
401
+ ## EmptyBoxIllustration (`components-data-display-emptyboxillustration`)
402
+
403
+ 빈 상태 일러스트 — 열린 상자와 구겨진 종이. dl-ui-v3 의 `empty_box.svg`(사용자 정보 화면의 빈 패널)를 토큰 색으로 옮겨 라이트·다크 테마를 따른다.
404
+
405
+ 언제 쓰나: EmptyState 의 기본 그림(목록·상세·대시보드의 "없음" 상자). 소비 앱의 빈 패널(dl-ui-v3 `CEmptyBox`)도 이것으로 대체한다.
406
+ 언제 안 쓰나: 오류 안내(→ ErrorState 의 경고 아이콘), 로딩(→ LoadingState), 표 안의 한 줄 빈 결과(ListPage 는 문장만 쓴다).
407
+ 대신/함께: 색은 `fill-primary10/20`·`fill-sys-background`·`fill-current`(종이 = `text-sys-text-subtler`, 원본의 grayscale60) 로 토큰을 따르므로 `className` 으로는 크기·여백만 바꾼다(기본 110×81). 장식이라 `aria-hidden`.
408
+
409
+ 예시 — story "Default — 110×81":
410
+
411
+ ```tsx
412
+ const Default = () => <EmptyBoxIllustration />;
413
+ ```
414
+
415
+ ## EventList (`components-data-display-eventlist`)
416
+
417
+ 최근 이벤트·활동 목록. 항목마다 왼쪽 레벨 색 띠 → 레벨 Badge → 시각 → 메시지 한 줄.
418
+
419
+ 언제 쓰나: 대시보드·상세 화면의 "최근 이벤트/활동" 섹션처럼 최신순 몇 개(보통 5~10개)를 한눈에 보여 줄 때. 레벨(info·success·warning·error)을 색 띠와 Badge 로 구분한다.
420
+ 언제 안 쓰나: 검색·필터·페이지네이션이 필요한 로그(→ ListPage·AgGridTable), 단계가 있는 진행 상황(→ 소비 앱의 스텝 표시), 사용자가 답해야 하는 알림(→ Alert·Toast).
421
+ 대신/함께: 시각은 소비 앱이 포맷해 `time` 에 넘긴다. 항목이 없으면 `emptyText` 를 status 로 보여 준다. 레벨 → 색은 고정이고 `className` 으로 색을 바꾸지 않는다.
422
+
423
+ | prop | 타입 | 기본값 | 설명 |
424
+ |---|---|---|---|
425
+ | `items` (필수) | `EventListItem[]` | | 최신순으로 정렬해 넘긴다 |
426
+ | `emptyText` | `ReactNode` | `최근 이벤트가 없습니다.` | 항목이 없을 때 문구. 기본 "최근 이벤트가 없습니다." |
427
+ | `levelLabels` | `Partial<Record<EventLevel, ReactNode>>` | | 레벨 Badge 글자. 기본 정보·성공·경고·오류 |
428
+
429
+ 예시 — story "Empty — 항목 없음":
430
+
431
+ ```tsx
432
+ const Empty = () => <EventList items={[]} />;
433
+ ```
434
+
435
+ ## LucideIcon (`components-data-display-lucideicon`)
436
+
437
+ 이름으로 그리는 lucide 아이콘. 버튼·뱃지·메뉴의 작은 아이콘에 쓴다.
438
+
439
+ 언제 쓰나: 아이콘 이름이 데이터(메뉴 설정, 상태 상수)에서 올 때, 또는 lucide 를 직접 import 하지 않고 이름으로 쓰고 싶을 때. 기본 크기 16px(버튼 글자 옆·뱃지 안 크기).
440
+ 언제 안 쓰나: 이름이 코드에 고정돼 있고 lucide 를 직접 import 할 수 있으면 `import { Plus } from 'lucide-react'` 가 더 가볍다.
441
+ 대신/함께: 상태 뱃지 아이콘은 `Badge` 의 자식으로 넣는다(16px·글자색). 로딩은 `Spinner`.
442
+
443
+ | prop | 타입 | 기본값 | 설명 |
444
+ |---|---|---|---|
445
+ | `iconComponentName` | `string \| null` | | lucide 아이콘 이름 — kebab(`circle-pause`) 또는 Pascal(`CirclePause`). 옛 이름(alias, `pause-circle`)도 받는다. 없는 이름이면 아무것도 그리지 않는다 |
446
+
447
+ 예시 — story "Playground":
448
+
449
+ ```tsx
450
+ const Playground = () => <LucideIcon iconComponentName="calendar-days" size={24} strokeWidth={2} />;
451
+ ```
452
+
453
+ ## MetaItem (`components-data-display-metaitem`)
454
+
455
+ 라벨·값 한 쌍의 메타 정보. 가로 나열(`MetaItem`+`MetaGroup`, 헤더용)과 세로 쌓기(`MetaRow`+`MetaList`, 상세 패널용) 두 모양이 있다.
456
+ 언제 쓰나: PageHeader 아래 등록자·일시 같은 메타(MetaItem). 상세 화면의 핵심 정보도 여기 한 곳 — 여러 개면 `MetaGroup columns={3}` 격자(2026-10-30 사용자 결정: 헤더의 메타 자리를 두 개로 나누지 않는다). 본문 카드의 속성 묶음은 MetaList + MetaRow align='start'.
457
+ 언제 안 쓰나: 편집 가능한 값(폼), 열이 여러 개인 표(Table).
458
+ 대신/함께: MetaRow 값은 14px·라벨 12px — 행마다 값 크기를 바꾸지 않는다(패널 안 크기 차이는 심판 감점, 2026-10-18). 상태는 value 에 Badge.
459
+
460
+ 예시 — story "Playground":
461
+
462
+ ```tsx
463
+ const Playground = () => <MetaItem label="등록자" value="smkim1" />;
464
+ ```
465
+
466
+ ## Progress (`components-data-display-progress`)
467
+
468
+ 가로 진행 막대(role=progressbar). 표의 진행률 열, 자원 사용량, 작업 진행에 쓴다.
469
+
470
+ 언제 쓰나: 0~100 의 비율을 길이로 보여 줄 때. 이름은 `aria-label` 로 준다 — 표에서는 행마다 `'GPU 사용률 46%'` 처럼 **무엇의 비율인지**.
471
+ 이름을 안 주면 `'46%'`(값이 없으면 `'진행 중'`)를 기본 이름으로 붙인다 — 이름 없는 progressbar 는 axe serious(aria-progressbar-name, 2026-10-20 17차). 기본 이름은 숫자뿐이라 뜻이 약하니 호출자가 주는 것이 맞다.
472
+ 언제 안 쓰나: 단계가 있는 진행(→ Stepper), 불확정 대기(→ Spinner).
473
+ 대신/함께: 막대 옆에 % 글자를 같이 둔다(`tabular-nums`). 색은 `variant` 로 상태와 맞춘다.
474
+
475
+ | prop | 타입 | 기본값 | 설명 |
476
+ |---|---|---|---|
477
+ | `value` | `number` | | 진행률 (0–100). 없으면 indeterminate |
478
+ | `variant` | `enum` | `primary` | 막대 색. 기본 `primary`. 상태 Badge 의 tone 과 맞춘다 — 실패 `danger`, 경고 `warning`, 완료 `success` |
479
+
480
+ 예시 — story "Playground":
481
+
482
+ ```tsx
483
+ const Playground = () => <Progress value={40} aria-label="진행률" />;
484
+ ```
485
+
486
+ ## Skeleton (`components-data-display-skeleton`)
487
+
488
+ 예시 — story "Playground":
489
+
490
+ ```tsx
491
+ const Playground = () => <Skeleton className="h-[20px] w-[240px]" />;
492
+ ```
493
+
494
+ ## Spinner (`components-data-display-spinner`)
495
+
496
+ 예시 — story "Auto — 어두운 배경 위 (variant="auto")":
497
+
498
+ ```tsx
499
+ const AutoOnDark = () => <Spinner size="large" shape="spinner" variant="auto" />;
500
+ ```
501
+
502
+ ## Tag (`components-data-display-tag`)
503
+
504
+ 붙이고 뗄 수 있는 라벨. 키워드·분류·선택된 항목을 나타낸다.
505
+
506
+ 언제 쓰나: 태그 목록, MultiSelect 가 보여 주는 선택 항목, 필터 조건 칩. 제거가 가능하면 `onRemove`, 링크면 `asChild`.
507
+ 언제 안 쓰나: 시스템이 정하는 상태 표시(→ Badge), 행동 버튼(→ Button).
508
+ 대신/함께: `asChild` 와 `onRemove` 를 함께 쓰면 제거 버튼이 링크 밖 형제로 렌더된다(버튼 중첩 금지).
509
+
510
+ | prop | 타입 | 기본값 | 설명 |
511
+ |---|---|---|---|
512
+ | `asChild` | `boolean` | `false` | 다른 요소로 렌더링 (예: `<a>` 링크). Radix Slot 패턴 |
513
+ | `elemBefore` | `ReactNode` | | 좌측 아이콘 영역. 16x16 크기로 렌더링됨 |
514
+ | `onRemove` | `(() => void)` | | 닫기 버튼 콜백. 전달 시 우측에 닫기 아이콘 표시 |
515
+
516
+ 예시 — story "Removable — onRemove 로 삭제":
517
+
518
+ ```tsx
519
+ const Removable = () => <RemovableTags />;
520
+ ```
521
+
522
+ # 컴포넌트 — inputs
523
+
524
+ ## Calendar (`components-inputs-calendar`)
525
+
526
+ | prop | 타입 | 기본값 | 설명 |
527
+ |---|---|---|---|
528
+ | `disableViewSwitching` | `boolean` | `false` | 뷰 전환 비활성화 (항상 days view만 표시) |
529
+
530
+ 예시 — story "Single — 단일 선택 (제어)":
531
+
532
+ ```tsx
533
+ const Single = () => <SingleCalendar />;
534
+ ```
535
+
536
+ ## Checkbox (`components-inputs-checkbox`)
537
+
538
+ 켜기/끄기 또는 여러 항목 중 복수 선택. 폼의 동의·옵션 플래그와 표의 행 선택에 쓴다.
539
+
540
+ 언제 쓰나: 서로 독립인 옵션을 여러 개 고를 때. 표의 행 선택은 AgGridTable 이 rowSelection 을 켜면 이 Checkbox 를 쓴 HeaderCheckboxRenderer·RowCheckboxRenderer 를 자동으로 넣으므로 셀에 직접 넣지 않는다(바꾸려면 그 렌더러를 감싼다).
541
+ 언제 안 쓰나: 하나만 골라야 할 때(→ Radio·RadioGroup), 켜기/끄기 하나의 설정(→ SwitchSection·Switch — 폼 제출값이어도), 선택지마다 제목·설명이 붙는 복수 선택(→ CheckboxSectionGroup), 선택지가 많을 때(→ MultiSelect).
542
+ 대신/함께: 라벨은 `Label` 또는 `FormLabel` 로 연결해 클릭 영역을 넓힌다.
543
+
544
+ 예시 — story "WithLabel — Label 조합 (controlled)":
545
+
546
+ ```tsx
547
+ const WithLabel = () => <WithLabelExample />;
548
+ ```
549
+
550
+ ## CheckboxSection (`components-inputs-checkboxsection`)
551
+
552
+ 카드형 체크박스 — 제목 + 설명이 있는 카드 안에 체크 표시. `CheckboxSectionGroup` 안에서 `value` 로 쓴다.
553
+
554
+ 언제 쓰나: 설명이 붙는 복수 선택의 한 항목(RadioSection 의 복수 선택판). 카드 전체가 눌리고, 선택되면 2px 주조색 테두리(padding 으로 보정해 layout shift 없음).
555
+ 언제 안 쓰나: 그룹 밖 단독(→ Checkbox + Label), 하나만 고를 때(→ RadioSection).
556
+ 대신/함께: 네이티브 `<button role='checkbox'>` 라 Tab 으로 이동, Space·Enter 로 켜고 끈다. 안의 체크 표시는 표시 전용이다.
557
+
558
+ | prop | 타입 | 기본값 | 설명 |
559
+ |---|---|---|---|
560
+ | `value` (필수) | `string` | | 그룹의 value 배열과 맞출 값 |
561
+ | `title` (필수) | `ReactNode` | | 카드 제목 |
562
+ | `description` | `ReactNode` | | 한 줄 설명 |
563
+
564
+ 예시 — story "Group — 세로 + disabled 항목":
565
+
566
+ ```tsx
567
+ const GroupVertical = () => <VerticalExample />;
568
+ ```
569
+
570
+ ## DatePicker (`components-inputs-datepicker`)
571
+
572
+ 날짜 하나를 고르는 입력. 트리거를 누르면 달력 팝오버가 열린다.
573
+
574
+ 언제 쓰나: 폼의 날짜 필드(시작일·만료일·예약일). `FormField` 안에서 `FormControl` 로 감싸 오류 상태(`isInvalid`)를 받는다. 시각까지 필요하면 옆에 `TimePicker` 를 둔다.
575
+ 언제 안 쓰나: 기간(→ DatePickerRange), 시각만(→ TimePicker).
576
+ 대신/함께: 기본 폭은 250px 이다. 폼 칸을 채우려면 `classNames={{ button: 'w-full' }}`(TimePicker 는 `classNames={{ trigger: 'w-full' }}`) — 좁은 고정 폭이 두 칸 줄의 절반을 비워 감점됐다(2026-10-28 홀드아웃 14).
577
+
578
+ | prop | 타입 | 기본값 | 설명 |
579
+ |---|---|---|---|
580
+ | `classNames` | `{ button?: string; PopoverContent?: string; } \| undefined` | | 클래스 오버라이드. `button` = 트리거 틀(기본 폭 250px — 폼 칸을 채우려면 `'w-full'`), `PopoverContent` = 달력 팝오버 |
581
+ | `label` | `string` | | Floating label 텍스트 |
582
+ | `required` | `boolean` | | 라벨 필수 표시 (*) |
583
+
584
+ 예시 — story "Single — 기본 (제어)":
585
+
586
+ ```tsx
587
+ const Basic = () => <ControlledSingle />;
588
+ ```
589
+
590
+ ## FormItem (`components-inputs-form`)
591
+
592
+ 예시 — story "Login — FormFooter align="stretch"":
593
+
594
+ ```tsx
595
+ const Login = () => <LoginForm />;
596
+ ```
597
+
598
+ ## IconSelect (`components-inputs-iconselect`)
599
+
600
+ | prop | 타입 | 기본값 | 설명 |
601
+ |---|---|---|---|
602
+ | `value` | `string \| null` | | 선택된 아이콘 이름 (lucide kebab-case, 예: 'calendar-days') |
603
+ | `onValueChange` | `((value: string) => void)` | | 아이콘을 클릭하면 그 아이콘 이름으로 호출된다 |
604
+ | `iconColumnCount` | `number` | `8` | 한 줄에 보여줄 아이콘 수 |
605
+ | `hasCategory` | `boolean` | `false` | 왼쪽에 카테고리 목록 표시 |
606
+ | `hiddenSearch` | `boolean` | `false` | 검색 입력 숨김 |
607
+
608
+ 예시 — story "WithCategory — 카테고리 목록 표시":
609
+
610
+ ```tsx
611
+ const WithCategory = () => <ControlledExample hasCategory hiddenSearch={false} iconColumnCount={8} />;
612
+ ```
613
+
614
+ ## Label (`components-inputs-label`)
615
+
616
+ 예시 — story "Playground":
617
+
618
+ ```tsx
619
+ const Playground = () => <Label>데이터셋 이름</Label>;
620
+ ```
621
+
622
+ ## MultiSelect (`components-inputs-multiselect`)
623
+
624
+ 드롭다운에서 여러 개를 고르는 폼 입력. 선택한 항목은 칩으로 보여 준다.
625
+
626
+ 언제 쓰나: 태그·권한·대상 목록처럼 0개 이상을 고르는 폼 필드. 구성은 Select 와 같다(MultiSelectTrigger·Content·Item).
627
+ 언제 안 쓰나: 하나만 고르는 경우(→ Select), 선택지가 적어 한눈에 보여야 하는 경우(→ Checkbox 묶음), 목록 화면 필터(→ TableFilterMultiSelect).
628
+ 대신/함께: 칩 라벨도 마운트된 아이템에서 얻으므로 미로드 값은 `useMultiSelectContext().registerLabel` 로 등록한다.
629
+
630
+ | prop | 타입 | 기본값 | 설명 |
631
+ |---|---|---|---|
632
+ | `onSearchChange` | `((value: string) => void)` | | 트리거 input / Content 의 검색어가 변경될 때 호출. typing 외에도 토글/popup close 시 빈 문자열로 reset 될 때도 호출됨. |
633
+ | `className` | `string` | | root container 에 적용할 추가 className. FormControl 의 maxWidth 가 여기에 주입된다. |
634
+
635
+ 예시 — story "Basic — controlled":
636
+
637
+ ```tsx
638
+ const Basic = () => <BasicExample />;
639
+ ```
640
+
641
+ ## Radio (`components-inputs-radio`)
642
+
643
+ 시각적 라디오 인디케이터 컴포넌트
644
+ RadioGroup 없이 독립적으로 사용 가능 (AG Grid 셀 렌더러 등)
645
+
646
+ 단일 선택 라디오 버튼 하나. 보통 RadioGroup 안에서 쓴다.
647
+
648
+ 언제 쓰나: 2~5개 선택지 중 하나를 고르고, 선택지를 한눈에 보여야 할 때.
649
+ 언제 안 쓰나: 선택지가 많을 때(→ Select), 복수 선택(→ Checkbox), 제목·설명이 붙은 큰 선택 카드(→ RadioSection).
650
+ 대신/함께: 단독으로 쓰지 말고 RadioGroup 으로 묶어 키보드 이동과 그룹 이름을 받는다.
651
+
652
+ 예시 — story "Playground":
653
+
654
+ ```tsx
655
+ const Playground = () => <Radio checked={false} disabled={false} aria-label="옵션" />;
656
+ ```
657
+
658
+ ## RadioGroup (`components-inputs-radiogroup`)
659
+
660
+ Radio 묶음. 폼에서 2~5개 선택지 중 하나를 고를 때 쓴다.
661
+
662
+ 언제 쓰나: 유형·방식 선택처럼 선택지가 적고 항상 보여야 할 때. `FormField` 안에서 `onValueChange` 를 연결한다.
663
+ 언제 안 쓰나: 선택지가 6개 이상(→ Select), 복수 선택(→ Checkbox), 선택지마다 제목·설명이 필요한 카드형(→ RadioSectionGroup).
664
+ 대신/함께: 선택에 따라 아래 필드가 바뀌면 반대 분기의 값은 `reset` 으로 비운다(`trigger` 는 빈 필수 필드를 즉시 오류로 띄운다).
665
+
666
+ 예시 — story "제어 — 선택 값 표시":
667
+
668
+ ```tsx
669
+ const Controlled = () => <ControlledRadioGroup />;
670
+ ```
671
+
672
+ ## RadioSection (`components-inputs-radiosection`)
673
+
674
+ 카드형 라디오 — 제목 + 설명이 있는 카드 안에 Radio 인디케이터. 유형·방식 선택의 기본.
675
+
676
+ 언제 쓰나: 소스 유형·공개 범위·입력 방식처럼 선택지가 2개 이상이고 선택지마다 제목과 한 줄 설명이 붙을 때 — `RadioSectionGroup` 안에 `value`·`title`·`description` 으로(dl-ui-v3 의 유형 선택 관례). 배치는 그룹이 개수로 정한다(1~3개 한 줄, 4개 2×2, 5개 이상 세로).
677
+ 언제 안 쓰나: 설명이 없는 짧은 선택(예·아니오, 켬·끔)은 RadioGroup, 선택지 7개 이상·검색이 필요하면 Select, 복수 선택은 Checkbox.
678
+ 대신/함께: 폼에서는 `FormField` 의 `field.value`/`field.onChange` 를 `RadioSectionGroup` 의 `value`/`onValueChange` 에 연결하고, 그룹에 `aria-labelledby` 로 FormLabel 의 id 를 건다(예제 form-page.tsx 의 스케줄 선택).
679
+
680
+ 두 가지 사용 방식:
681
+ - **그룹 모드**: `<RadioSectionGroup>` 안에서 `value` 와 함께 사용 → Radix Item 으로 동작하여
682
+ role=radio / roving tabindex / aria-checked / 화살표·Space 키를 그룹이 관리한다.
683
+ - **단독 모드(하위호환)**: `selected` / `onSelect` 로 부모가 직접 제어. (기존 동작 그대로)
684
+
685
+ | prop | 타입 | 기본값 | 설명 |
686
+ |---|---|---|---|
687
+ | `value` | `string` | | RadioSectionGroup 안에서 사용할 때 필수 — 그룹의 선택 value 와 매칭된다. |
688
+
689
+ 예시 — story "Standalone — selected / onSelect (하위호환)":
690
+
691
+ ```tsx
692
+ const Standalone = () => <StandaloneExample />;
693
+ ```
694
+
695
+ ## Range (`components-inputs-range`)
696
+
697
+ Figma "Range" 합성 컴포넌트 — ± 스테퍼(IconButton) + 슬라이더(RangeSlider) + 숫자 입력박스(DS TextField).
698
+ 숫자 입력 검증은 useNumberInput 훅이 담당. 단일값 전용 — 두 thumb 범위는 저수준 `RangeSlider`를 직접 쓴다.
699
+
700
+ | prop | 타입 | 기본값 | 설명 |
701
+ |---|---|---|---|
702
+ | `value` | `number` | | controlled 값 |
703
+ | `defaultValue` | `number` | | uncontrolled 초기값 |
704
+ | `shape` | `enum` | `rounded` | ±버튼·숫자입력 모서리 형태 (트랙/썸/눈금엔 영향 없음) |
705
+ | `type` | `enum` | `discrete` | discrete: step 눈금/숫자, continuous: 양끝 라벨만 |
706
+ | `hasButtonControl` | `boolean` | `true` | ± 스테퍼 버튼 표시 (기본 true) |
707
+ | `hasCustomInput` | `boolean` | `true` | 숫자 입력박스 표시 (기본 true) |
708
+ | `showLabels` | `boolean` | `true` | 눈금 숫자/양끝 라벨 표시 (기본 true) |
709
+ | `marks` | `RangeMark[]` | | 명시적 눈금/라벨 위치. 지정 시 step 기반 기본 눈금을 대체 |
710
+ | `aria-label` | `string` | | 슬라이더 thumb 과 숫자 입력의 접근 가능한 이름 (예: '학습률') |
711
+ | `aria-labelledby` | `string` | | 외부 라벨 요소의 id. 지정 시 thumb 과 숫자 입력이 그 요소로 이름을 얻는다 |
712
+ | `name` | `string` | | 숫자 입력의 name (폼 전송용) |
713
+
714
+ 예시 — story "Disabled":
715
+
716
+ ```tsx
717
+ const Disabled = () => <Range
718
+ min={0}
719
+ max={10}
720
+ step={1}
721
+ defaultValue={5}
722
+ shape="rounded"
723
+ type="discrete"
724
+ hasButtonControl
725
+ hasCustomInput
726
+ disabled
727
+ aria-label="값" />;
728
+ ```
729
+
730
+ ## RangeSlider (`components-inputs-rangeslider`)
731
+
732
+ 저수준 슬라이더 파트(Radix 기반). Track + 채움(Range) + thumb 상태머신 + 선택적 discrete/continuous 라벨.
733
+ thumb 상태: default(primary30) → hover(primary40) → press(primary50) → focus(+ring) → disabled(secondary50).
734
+ 단일값/범위([lo,hi]) 모두 지원(value 배열 길이로 thumb 개수 결정).
735
+
736
+ | prop | 타입 | 기본값 | 설명 |
737
+ |---|---|---|---|
738
+ | `type` | `enum` | `discrete` | discrete: step마다 눈금/숫자, continuous: 양끝 라벨만 |
739
+ | `showLabels` | `boolean` | `true` | 눈금 숫자/양끝 라벨 표시 여부 |
740
+ | `marks` | `RangeMark[]` | | 명시적 눈금/라벨 위치. 지정 시 step 기반 기본 눈금을 대체(discrete/continuous 공통) |
741
+
742
+ 예시 — story "Disabled":
743
+
744
+ ```tsx
745
+ const Disabled = () => <RangeSlider min={0} max={100} step={10} defaultValue={[40]} type="discrete" disabled />;
746
+ ```
747
+
748
+ ## Select (`components-inputs-select`)
749
+
750
+ 드롭다운에서 하나를 고르는 폼 입력. 선택지가 정해져 있고 5개 이상일 때 쓴다.
751
+
752
+ 언제 쓰나: 폼 안에서 `FormControl` 아래 `Select` > `SelectTrigger`(label·required) + `SelectContent` > `SelectItem`. 항목이 많으면 `SelectInput` 으로 검색, 비동기 목록은 `SelectLoading`·`SelectEmpty`.
753
+ 언제 안 쓰나: 선택지 2~4개(→ RadioGroup·RadioSection), 여러 개 선택(→ MultiSelect), 목록 화면 상단 필터(→ TableFilterSingleSelect), 켜고 끄기(→ Switch).
754
+ 대신/함께: 트리거 라벨은 `SelectItem` 에서 얻는다 — 닫혀 있어도 항목이 이름을 등록하므로 미리 정한 값도 펼치기 전에 이름으로 보인다. 목록에 아직 없는 값(비동기로 오는 항목)만 `useSelectContext().registerLabel` 로 미리 등록한다.
755
+
756
+ | prop | 타입 | 기본값 | 설명 |
757
+ |---|---|---|---|
758
+ | `className` | `string` | | root container 에 적용할 추가 className. FormControl 의 maxWidth 가 여기에 주입된다. |
759
+
760
+ 예시 — story "Searchable — SelectInput + SelectEmpty":
761
+
762
+ ```tsx
763
+ const Searchable = () => (
764
+ <Select>
765
+ <SelectTrigger className='w-[240px]'>
766
+ <SelectValue placeholder='검색하여 선택' />
767
+ </SelectTrigger>
768
+ <SelectContent>
769
+ <SelectInput placeholder='검색' />
770
+ {fruits.map((item) => (
771
+ <SelectItem key={item.value} value={item.value}>
772
+ {item.label}
773
+ </SelectItem>
774
+ ))}
775
+ <SelectEmpty />
776
+ </SelectContent>
777
+ </Select>
778
+ );
779
+ ```
780
+
781
+ ## Switch (`components-inputs-switch`)
782
+
783
+ 켜고 끄는 토글. 두 상태뿐인 설정에 쓴다 — 즉시 반영이든 폼 제출값이든.
784
+
785
+ 언제 쓰나: 알림 켜기, 공개/비공개, 규칙 사용처럼 두 상태뿐인 설정. 폼 안에서는 `SwitchSection`(박스 + 제목·설명 + Switch)으로 둔다(2026-10-14, dl-ui-v3 ToggleField 관례 — 이전 "폼 제출값은 Checkbox" 규칙은 폐기).
786
+ 언제 안 쓰나: 여러 옵션 중 복수 선택(→ Checkbox·CheckboxSectionGroup), 세 가지 이상 상태(→ RadioGroup·RadioSectionGroup·Select).
787
+ 대신/함께: 라벨은 `Label` 로 연결하고, 상태 텍스트("켜짐")를 옆에 따로 두지 않는다.
788
+
789
+ 예시 — story "WithLabel — Label 조합 (controlled)":
790
+
791
+ ```tsx
792
+ const WithLabel = () => <WithLabelExample />;
793
+ ```
794
+
795
+ ## SwitchSection (`components-inputs-switchsection`)
796
+
797
+ 토글 박스 — 테두리 상자 안에 제목·설명(왼쪽)과 Switch(오른쪽). dl-ui-v3 의 ToggleField(노트북 지원, 자동 갱신 등) 관례를 TDS 로 올린 것. 테두리·패딩·글자(제목 h5·설명 base·간격 4)는 RadioSection·CheckboxSection 카드와 같다 — 한 폼에 나란히 와도 한 가족으로 보이게.
798
+
799
+ 언제 쓰나: 켜기/끄기 하나의 설정 — 즉시 반영이든 폼 제출값이든(규칙 사용, 실패 시 알림, 노트북 지원). 폼에서는 `FormField` 의 `field.value`·`field.onChange` 를 `checked`·`onCheckedChange` 에 연결하고 `FormControl` 로 감싼다(id·aria 가 Switch 에 간다).
800
+ 언제 안 쓰나: 여러 옵션 중 복수 선택(→ Checkbox·CheckboxSectionGroup), 세 가지 이상 상태(→ RadioSectionGroup·Select), 라벨만 있는 짧은 동의(→ Checkbox + Label).
801
+ 대신/함께: 제목을 누르면 Switch 가 눌린다(label htmlFor). 바깥 FormLabel 은 필드 이름("알림"), 박스 제목은 무엇을 켜는지("실패 시 이메일 알림 받기").
802
+
803
+ | prop | 타입 | 기본값 | 설명 |
804
+ |---|---|---|---|
805
+ | `title` (필수) | `ReactNode` | | 박스 안 왼쪽의 굵은 제목 (예: "규칙 사용") |
806
+ | `description` | `ReactNode` | | 제목 아래 한 줄 설명 (예: "끄면 규칙은 저장되지만 알림을 보내지 않습니다") |
807
+ | `className` | `string` | | 박스(바깥 div)의 className. Switch 자체는 `switchClassName` |
808
+
809
+ 예시 — story "TitleOnly — 설명 없이":
810
+
811
+ ```tsx
812
+ const TitleOnly = () => <Controlled title='노트북 지원' />;
813
+ ```
814
+
815
+ ## Textarea (`components-inputs-textarea`)
816
+
817
+ | prop | 타입 | 기본값 | 설명 |
818
+ |---|---|---|---|
819
+ | `label` | `string` | | Floating label 텍스트 |
820
+ | `isInvalid` | `boolean` | | 에러 상태 |
821
+ | `containerClassName` | `string` | | 컨테이너 div에 적용할 추가 className |
822
+ | `resize` | `boolean` | `false` | 수동 리사이즈 허용 |
823
+
824
+ 예시 — story "Controlled — 제어 컴포넌트":
825
+
826
+ ```tsx
827
+ const Controlled = () => <ControlledExample />;
828
+ ```
829
+
830
+ ## TextField (`components-inputs-textfield`)
831
+
832
+ 한 줄 텍스트 입력. 폼의 이름·코드·URL·숫자 같은 짧은 값에 쓴다.
833
+
834
+ 언제 쓰나: `FormField` 안에서 `FormControl` 로 감싸 오류 상태를 받는다. 라벨은 `FormLabel` 로 두고, 독립 사용 시에만 `label`(floating) 을 쓴다.
835
+ 언제 안 쓰나: 여러 줄 입력(→ Textarea), 정해진 선택지(→ Select·RadioGroup), 목록 검색·필터(→ ColumnFilter·TableFilter*).
836
+ 대신/함께: 지우기 버튼은 `hasClearButton`, 단위·아이콘은 `elemBeforeInput`/`elemAfterInput`.
837
+
838
+ | prop | 타입 | 기본값 | 설명 |
839
+ |---|---|---|---|
840
+ | `label` | `string` | | Floating label 텍스트 |
841
+ | `hasClearButton` | `boolean` | | Clear 버튼 표시 여부 |
842
+ | `elemBeforeInput` | `ReactNode` | | 입력 필드 왼쪽 아이콘/요소 슬롯 |
843
+ | `elemAfterInput` | `ReactNode` | | 입력 필드 오른쪽 아이콘/요소 슬롯 |
844
+ | `isInvalid` | `boolean` | | 에러 상태 |
845
+ | `containerClassName` | `string` | | 컨테이너 div에 적용할 추가 className |
846
+
847
+ 예시 — story "Controlled — 제어 컴포넌트":
848
+
849
+ ```tsx
850
+ const Controlled = () => <ControlledExample />;
851
+ ```
852
+
853
+ ## TimePicker (`components-inputs-timepicker`)
854
+
855
+ 시각(시·분)을 고르는 입력. 트리거를 누르면 시·분 목록 팝오버가 열린다. `hourType` 으로 24시간·12시간.
856
+
857
+ 언제 쓰나: 폼의 시각 필드(예약 시작·종료 시각, 실행 시각). 날짜와 함께면 `DatePicker` 옆에 둔다.
858
+ 언제 안 쓰나: 날짜만(→ DatePicker), 기간(→ DatePickerRange), 분 단위 지속 시간(→ TextField + 단위).
859
+ 대신/함께: 기본 폭은 160px(12시간 180px)이다. 폼 칸을 채우려면 `classNames={{ trigger: 'w-full' }}`(DatePicker 는 `classNames={{ button: 'w-full' }}`) — 좁은 고정 폭이 줄의 절반을 비워 감점됐다(2026-10-28 홀드아웃 14).
860
+
861
+ | prop | 타입 | 기본값 | 설명 |
862
+ |---|---|---|---|
863
+ | `placeholder` | `string` | | 플레이스홀더 텍스트 |
864
+ | `disabled` | `boolean` | | 비활성 상태 |
865
+ | `isInvalid` | `boolean` | | 에러/유효하지 않은 상태 |
866
+ | `label` | `string` | | Floating label 텍스트 |
867
+ | `required` | `boolean` | | 라벨 필수 표시 (*) |
868
+ | `minuteStep` | `number` | `5` | 분 간격. |
869
+ | `classNames` | `{ trigger?: string; popoverContent?: string; } \| undefined` | | 클래스 오버라이드. `trigger` = 트리거 틀(기본 폭 160px, 12시간 180px — 폼 칸을 채우려면 `'w-full'`), `popoverContent` = 시·분 팝오버 |
870
+
871
+ 예시 — story "24hrs (기본)":
872
+
873
+ ```tsx
874
+ const Hours24 = () => (
875
+ <div className='flex flex-wrap items-start gap-[24px]'>
876
+ <Example label='기본'>
877
+ <TimePicker24Example showValue />
878
+ </Example>
879
+ <Example label='미리 선택된 시간'>
880
+ <TimePicker24Example defaultValue='14:35' showValue />
881
+ </Example>
882
+ </div>
883
+ );
884
+ ```
885
+
886
+ # 컴포넌트 — layout
887
+
888
+ ## Card (`components-layout-card`)
889
+
890
+ 예시 — story "Simple — 헤더와 본문만":
891
+
892
+ ```tsx
893
+ const Simple = () => (
894
+ <Card className='w-[320px] gap-[8px] py-[16px]'>
895
+ <CardHeader>
896
+ <CardTitle>학습 작업</CardTitle>
897
+ </CardHeader>
898
+ <CardContent>진행 중인 학습 작업이 3건 있습니다.</CardContent>
899
+ </Card>
900
+ );
901
+ ```
902
+
903
+ ## Panel (`components-layout-panel`)
904
+
905
+ 1px 테두리의 둥근 상자. 상세 화면의 본문 구역·옆 패널, 목록의 상세 패널, 폼의 검토 패널이 전부 이 상자다.
906
+
907
+ 언제 쓰나: 제목 + 내용 묶음을 본문과 구분해 보일 때(상세 본문의 "주요 정보"·"설명", DetailPage `aside`, FormPage `aside`, ListPage `detail`). 그림자 없이 선 하나로 나눈다 — 화면의 다른 선(표·구분선)과 같은 토큰.
908
+ 언제 안 쓰나: 대시보드 요약 타일(→ Card), 알림(→ Alert), 겹쳐 뜨는 것(→ Dialog·Drawer·Popover).
909
+ 대신/함께: 안의 필드는 `MetaList` + `MetaRow align='start'`, 표는 `Table`·`AgGridTable`(자기 상자가 있으니 Panel 로 또 감싸지 않는다). 패턴 슬롯(`aside`·`detail`)은 패턴이 Panel 로 그리므로 소비 앱이 상자를 또 두르지 않는다.
910
+
911
+ | prop | 타입 | 기본값 | 설명 |
912
+ |---|---|---|---|
913
+ | `title` | `ReactNode` | | 상자 제목. 주면 이름 있는 `section` landmark 가 되고(aria-labelledby), 없으면 `div` |
914
+ | `description` | `ReactNode` | | 제목 아래 한 줄 설명 |
915
+ | `level` | `enum` | `2` | 제목의 heading 레벨. 페이지 제목이 h1 이면 2(기본), 상자 안의 하위 구역이면 3 |
916
+ | `titleRight` | `ReactNode` | | 제목 줄 오른쪽(닫기 버튼 등) |
917
+ | `note` | `ReactNode` | | 본문 아래 안내 한 줄(비활성 사유 등) |
918
+ | `footer` | `ReactNode` | | 맨 아래 버튼 줄(오른쪽 정렬, 8px 간격) |
919
+ | `padding` | `enum` | `medium` | 안쪽 여백. medium 16px(옆 패널·본문 구역, 기본) · large 24px(목록의 상세 패널) |
920
+
921
+ 예시 — story "NoTitle — 제목 없는 상자 (div)":
922
+
923
+ ```tsx
924
+ const NoTitle = () => <Panel>{rows}</Panel>;
925
+ ```
926
+
927
+ ## Separator (`components-layout-separator`)
928
+
929
+ | prop | 타입 | 기본값 | 설명 |
930
+ |---|---|---|---|
931
+ | `orientation` | `enum` | `horizontal` | Either `vertical` or `horizontal`. Defaults to `horizontal`. |
932
+ | `decorative` | `boolean` | `true` | Whether or not the component is purely decorative. When true, accessibility-related attributes are updated so that that the rendered element is removed from the accessibility tree. |
933
+
934
+ 예시 — story "Horizontal — 가로 구분선":
935
+
936
+ ```tsx
937
+ const Horizontal = () => <div
938
+ className='flex w-[360px] flex-col gap-[12px] text-base-medium text-sys-text-main'>
939
+ <span>데이터셋</span>
940
+ <Separator orientation='horizontal' />
941
+ <span>학습</span>
942
+ <Separator orientation='horizontal' />
943
+ <span>추론 서비스</span>
944
+ </div>;
945
+ ```
946
+
947
+ ## Stack (`components-layout-stack`)
948
+
949
+ 예시 — story "Playground":
950
+
951
+ ```tsx
952
+ const Playground = () => <Stack
953
+ direction="row"
954
+ spacing={8}
955
+ className='min-h-[160px] rounded-small border border-dashed border-sys-border p-[12px]'>
956
+ <Box label='1' />
957
+ <Box label='2' />
958
+ <Box label='3' />
959
+ </Stack>;
960
+ ```
961
+
962
+ # 컴포넌트 — navigation
963
+
964
+ ## Breadcrumb (`components-navigation-breadcrumb`)
965
+
966
+ | prop | 타입 | 기본값 | 설명 |
967
+ |---|---|---|---|
968
+ | `separator` | `ReactNode` | | 하위 `BreadcrumbSeparator` 의 기본 구분자 (children 을 넘기지 않은 Separator 에 적용) |
969
+
970
+ 예시 — story "Basic — 3 depth":
971
+
972
+ ```tsx
973
+ const Basic = () => (
974
+ <Breadcrumb>
975
+ <BreadcrumbList>
976
+ <BreadcrumbItem>
977
+ <BreadcrumbLink href='#'>홈</BreadcrumbLink>
978
+ </BreadcrumbItem>
979
+ <BreadcrumbSeparator />
980
+ <BreadcrumbItem>
981
+ <BreadcrumbLink href='#'>카테고리</BreadcrumbLink>
982
+ </BreadcrumbItem>
983
+ <BreadcrumbSeparator />
984
+ <BreadcrumbItem>
985
+ <BreadcrumbPage>현재 페이지</BreadcrumbPage>
986
+ </BreadcrumbItem>
987
+ </BreadcrumbList>
988
+ </Breadcrumb>
989
+ );
990
+ ```
991
+
992
+ ## PageHeader (`components-navigation-pageheader`)
993
+
994
+ 페이지 최상단 헤더. 제목·설명·상태·메타·주요 행동·필터 줄·탭을 한 곳에 둔다.
995
+
996
+ 언제 쓰나: 모든 목록·상세 화면의 맨 위에 한 번. 주요 행동은 `actions`(제목 줄 오른쪽 — 탭이 있어도 같고, 제목이 길어 자리가 모자라면 버튼 묶음이 제목 아래 줄 오른쪽으로 내려간다), 검색·필터는 `bottomRight`, 새로고침은 `titleRight` 에 PageHeaderRefreshButton(actions 와 같은 묶음), 페이지 이동 탭은 `tabs`(제목 아래 별도 줄). 버튼이 나란히 놓이는 슬롯(`actions`·`titleRight`·`bottomLeft`)은 버튼 사이 8px 로 그린다 — 직접 Stack 으로 감싸지 않는다.
997
+ 언제 안 쓰나: 섹션 제목(→ `FormSection`·`CardHeader`), 모달·패널 제목(→ DialogHeader·DrawerHeader), 한 페이지에 두 번.
998
+ 대신/함께: 레이아웃이 헤더를 소유하는 앱은 `PageHeaderProvider` + `usePageHeader` 로 내용만 밀어 넣는다. 목록 화면 전체는 ListPage 패턴이 이를 포함한다.
999
+
1000
+ | prop | 타입 | 기본값 | 설명 |
1001
+ |---|---|---|---|
1002
+ | `breadcrumbTitle` | `string \| null` | | Optional override for the breadcrumb tail (last item). - `undefined` (default): consuming apps may fall back to `title`. - `null`: explicitly suppress the auto-appended breadcrumb tail. - `string`: use the given text as the breadcrumb tail regardless of `title`. design-system itself does not render breadcrumbs (they are passed in as the `breadcrumb` slot) — this field is a contract for the consuming app's breadcrumb wrapper to read. |
1003
+ | `meta` | `ReactNode` | | 제목 아래 메타. 상세 화면의 핵심 정보는 여기 한 곳 — 여러 개면 `MetaGroup columns={3}` + `MetaItem`(2026-10-30 사용자 결정: 메타 자리를 두 개로 나누지 않는다) |
1004
+ | `titleRight` | `ReactNode` | | Right-aligned slot next to the title (e.g. a secondary title action). |
1005
+ | `contentMaxWidth` | `string` | | 내용(빵부스러기·제목·행동·설명·탭)의 최대 폭 — CSS 길이(예: `var(--max-w-page-content)`). 주면 그 폭 안에 가운데 두고 띠(배경·아래 선)는 전폭 그대로. FormPage 가 폼 폭에 맞춰 넘긴다(2026-10-26 22차). 기본 없음 = 전폭 |
1006
+
1007
+ 예시 — story "WithTabs — 탭 + 배지 (controlled)":
1008
+
1009
+ ```tsx
1010
+ const WithTabs = () => <WithTabsExample />;
1011
+ ```
1012
+
1013
+ ## PageHeaderRefreshButton (`components-navigation-pageheaderrefreshbutton`)
1014
+
1015
+ PageHeader `titleRight` 에 놓는 새로고침 묶음 — "N분 전 업데이트" 라벨 + 아이콘 버튼. 갱신 중에는 아이콘이 돌고 버튼이 잠긴다.
1016
+
1017
+ 언제 쓰나: 목록·상세·대시보드처럼 서버 데이터를 다시 불러올 수 있는 화면의 헤더. `lastUpdatedAt` 에 조회 시각(react-query `dataUpdatedAt`)을, `refreshing` 에 재조회 중 여부를 넘긴다.
1018
+ 언제 안 쓰나: 폼의 "다시 시도"(→ Button secondary), 표 안의 행 단위 새로고침(→ IconButton secondaryLink), 자동 폴링만 있고 사용자가 누를 일이 없는 화면.
1019
+ 대신/함께: 텍스트 버튼으로 만들지 않는다(dl-ui-v3 헤더 관례와 통일). 라벨은 1분마다 다시 계산한다.
1020
+
1021
+ | prop | 타입 | 기본값 | 설명 |
1022
+ |---|---|---|---|
1023
+ | `onClick` (필수) | `() => void` | | 새로고침 클릭 |
1024
+ | `lastUpdatedAt` | `string \| number \| Date` | | 마지막 갱신 시각. 없으면 "업데이트 전", 1분 안이면 "방금 업데이트", 그 뒤로는 "N분 전 업데이트" (1분마다 갱신) |
1025
+ | `refreshing` | `boolean` | `false` | true 면 아이콘이 돌고 버튼이 비활성 (react-query 의 isRefetching 등) |
1026
+ | `size` | `enum` | `default` | 버튼 크기. default 36px, compact 28px (compact PageHeader 용) |
1027
+
1028
+ 예시 — story "Default — 5분 전 업데이트, 누르면 갱신":
1029
+
1030
+ ```tsx
1031
+ const Default = () => <RefreshDemo />;
1032
+ ```
1033
+
1034
+ ## Pagination (`components-navigation-pagination`)
1035
+
1036
+ 페이지 번호 이동. 서버 페이징 목록의 표 아래에 쓴다.
1037
+
1038
+ 언제 쓰나: `total`·`page`·`rowsPerPage` 를 알 수 있는 목록. `showLabel` 로 "전체 N개 중 X-Y" 를 보여 준다. 필터·검색이 바뀌면 1페이지로.
1039
+ 언제 안 쓰나: 무한 스크롤·더 보기 방식, 커서 기반이라 전체 개수를 모르는 목록(→ 이전/다음 버튼만).
1040
+ 대신/함께: `TablePagination` 으로 감싸 표 아래 가운데에 둔다. ListPage 패턴은 이를 내장한다.
1041
+
1042
+ | prop | 타입 | 기본값 | 설명 |
1043
+ |---|---|---|---|
1044
+ | `total` (필수) | `number` | | 총 row(데이터)의 개수 |
1045
+ | `rowsPerPage` (필수) | `number` | | 한 페이지에 보여줄 row(데이터)의 개수 |
1046
+ | `page` (필수) | `number` | | 현재 페이지 번호 |
1047
+ | `onChange` (필수) | `Dispatch<number>` | | 페이지 번호 설정 함수 |
1048
+ | `count` | `number` | `5` | 컴포넌트에서 보여줄 페이지 수 (기본값 : 5) |
1049
+ | `disabled` | `boolean` | `false` | disabled |
1050
+ | `showLabel` | `boolean` | `false` | 라벨 표시 여부 (전체 N개 중 X - Y번째) |
1051
+
1052
+ 예시 — story "Composed — 하위 파트로 직접 조립":
1053
+
1054
+ ```tsx
1055
+ const Composed = () => <ComposedExample />;
1056
+ ```
1057
+
1058
+ ## Sidebar (`components-navigation-sidebar`)
1059
+
1060
+ 예시 — story "Default — 펼침":
1061
+
1062
+ ```tsx
1063
+ const Default = () => <SidebarProvider defaultOpen>
1064
+ <SidebarExample openWidth={240} closedWidth={72} />
1065
+ </SidebarProvider>;
1066
+ ```
1067
+
1068
+ ## Tabs (`components-navigation-tabs`)
1069
+
1070
+ 같은 대상의 여러 뷰를 전환하는 탭. 상세 화면의 섹션 전환에 쓴다.
1071
+
1072
+ 언제 쓰나: 한 리소스의 개요·설정·로그처럼 같은 대상을 다른 관점으로 보는 2~6개 뷰. `TabsList` > `TabsTrigger` 와 `TabsContent` 로 조립한다.
1073
+ 언제 안 쓰나: URL 이 바뀌는 페이지 이동(→ PageHeader 의 `tabs` 또는 내비게이션), 단계가 있는 흐름(→ 스텝 UI), 선택 입력(→ RadioGroup).
1074
+ 대신/함께: 페이지 상단 탭은 PageHeader 가 그리므로 둘을 겹쳐 쓰지 않는다.
1075
+
1076
+ | prop | 타입 | 기본값 | 설명 |
1077
+ |---|---|---|---|
1078
+ | `value` | `string` | | The value for the selected tab, if controlled |
1079
+ | `defaultValue` | `string` | | The value of the tab to select by default, if uncontrolled |
1080
+ | `onValueChange` | `((value: string) => void)` | | A function called when a new tab is selected |
1081
+ | `orientation` | `enum` | | The orientation the tabs are layed out. Mainly so arrow navigation is done accordingly (left & right vs. up & down) @defaultValue horizontal |
1082
+ | `dir` | `enum` | | The direction of navigation between toolbar items. |
1083
+ | `activationMode` | `enum` | | Whether a tab is activated automatically or manually. @defaultValue automatic |
1084
+
1085
+ 예시 — story "Box — 기본":
1086
+
1087
+ ```tsx
1088
+ const Box = () => <TabsExample defaultValue='overview' />;
1089
+ ```
1090
+
1091
+ # 컴포넌트 — overlay
1092
+
1093
+ ## AlertDialogContent (`components-overlay-alertdialog`)
1094
+
1095
+ 확인 대화상자 본체. 되돌릴 수 없는 행동 직전의 최종 확인에만 쓴다.
1096
+
1097
+ 언제 쓰나: 삭제·중지·초기화처럼 결과가 사라지는 행동. 제목에 대상 이름을 넣고, 확인 버튼은 `danger*` variant 로 행동 이름("삭제")을 쓴다.
1098
+ 언제 안 쓰나: 입력이 필요한 작업(→ Dialog), 단순 정보 안내(→ Alert·Toast), 되돌릴 수 있는 행동(확인 없이 실행 후 Toast).
1099
+ 대신/함께: 실수 방지가 더 필요하면 이름을 입력해야 삭제되는 방식을 소비 앱에서 더한다.
1100
+
1101
+ | prop | 타입 | 기본값 | 설명 |
1102
+ |---|---|---|---|
1103
+ | `forceMount` | `true` | | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
1104
+ | `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` | | Event handler called when the escape key is down. Can be prevented. |
1105
+ | `onFocusOutside` | `((event: FocusOutsideEvent) => void)` | | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
1106
+ | `onOpenAutoFocus` | `((event: Event) => void)` | | Event handler called when auto-focusing on open. Can be prevented. |
1107
+ | `onCloseAutoFocus` | `((event: Event) => void)` | | Event handler called when auto-focusing on close. Can be prevented. |
1108
+
1109
+ 예시 — story "LoadingAction — 비동기 확인 (loading)":
1110
+
1111
+ ```tsx
1112
+ const LoadingAction = () => <LoadingActionExample />;
1113
+ ```
1114
+
1115
+ ## Command (`components-overlay-command`)
1116
+
1117
+ | prop | 타입 | 기본값 | 설명 |
1118
+ |---|---|---|---|
1119
+ | `value` | `string` | | Optional controlled state of the selected command menu item. |
1120
+ | `filter` | `CommandFilter` | | Custom filter function for whether each command menu item should matches the given search query. It should return a number between 0 and 1, with 1 being the best match and 0 being hidden entirely. By default, uses the `command-score` library. |
1121
+ | `label` | `string` | | Accessible label for this command menu. Not shown visibly. |
1122
+ | `loop` | `boolean` | | Optionally set to `true` to turn on looping around when using the arrow keys. |
1123
+ | `shouldFilter` | `boolean` | | Optionally set to `false` to turn off the automatic filtering and sorting. If `false`, you must conditionally render valid items based on the search query yourself. |
1124
+ | `onValueChange` | `((value: string) => void)` | | Event handler called when the selected item of the menu changes. |
1125
+ | `disablePointerSelection` | `boolean` | | Optionally set to `true` to disable selection via pointer events. |
1126
+ | `vimBindings` | `boolean` | | Set to `false` to disable ctrl+n/j/p/k shortcuts. Defaults to `true`. |
1127
+
1128
+ 예시 — story "Dialog — CommandDialog":
1129
+
1130
+ ```tsx
1131
+ const Dialog = () => <DialogExample />;
1132
+ ```
1133
+
1134
+ ## DialogContent (`components-overlay-dialog`)
1135
+
1136
+ 모달 대화상자 본체. 짧은 입력이나 복합 상호작용을 현재 화면 위에서 처리할 때 쓴다.
1137
+
1138
+ 언제 쓰나: 이름 바꾸기·태그 추가처럼 필드 서너 개의 입력, 미리보기·선택기처럼 맥락을 잠시 가리는 작업. `DialogTitle` 은 항상 넣는다.
1139
+ 언제 안 쓰나: 되돌릴 수 없는 행동의 최종 확인(→ AlertDialog), 섹션이 여럿인 긴 폼·상세 보기(→ Drawer), 지나가는 결과 알림(→ Toast).
1140
+ 대신/함께: 폼이면 `@t3q-ui/design-system/patterns` 의 FormDialog 가 제출 중 닫힘 차단까지 포함한다.
1141
+
1142
+ | prop | 타입 | 기본값 | 설명 |
1143
+ |---|---|---|---|
1144
+ | `forceMount` | `true` | | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
1145
+ | `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` | | Event handler called when the escape key is down. Can be prevented. |
1146
+ | `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` | | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
1147
+ | `onFocusOutside` | `((event: FocusOutsideEvent) => void)` | | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
1148
+ | `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` | | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prevented. |
1149
+ | `onOpenAutoFocus` | `((event: Event) => void)` | | Event handler called when auto-focusing on open. Can be prevented. |
1150
+ | `onCloseAutoFocus` | `((event: Event) => void)` | | Event handler called when auto-focusing on close. Can be prevented. |
1151
+
1152
+ 예시 — story "OpenByDefault — 본문이 있는 열린 상태":
1153
+
1154
+ ```tsx
1155
+ const OpenByDefault = () => (
1156
+ <Dialog defaultOpen>
1157
+ <DialogContent width='medium'>
1158
+ <DialogHeader>
1159
+ <DialogTitle variant='info'>시스템 점검 안내</DialogTitle>
1160
+ <DialogDescription>
1161
+ 2026년 10월 10일 오전 2시 ~ 4시 동안 정기 점검이 예정되어 있습니다.
1162
+ </DialogDescription>
1163
+ </DialogHeader>
1164
+ <div className='flex flex-col gap-[8px] text-base-regular text-sys-text-main'>
1165
+ <p>점검 시간 동안 서비스 이용이 일시 중단됩니다.</p>
1166
+ <p>점검이 끝나면 서비스가 자동으로 재개되고, 진행 중인 작업은 자동 저장됩니다.</p>
1167
+ </div>
1168
+ <DialogFooter>
1169
+ <DialogClose asChild>
1170
+ <Button variant='primary'>확인했습니다</Button>
1171
+ </DialogClose>
1172
+ </DialogFooter>
1173
+ </DialogContent>
1174
+ </Dialog>
1175
+ );
1176
+ ```
1177
+
1178
+ ## DrawerContent (`components-overlay-drawer`)
1179
+
1180
+ 화면 가장자리에서 미끄러져 나오는 패널 본체. 목록 맥락을 유지한 채 긴 폼·상세를 볼 때 쓴다.
1181
+
1182
+ 언제 쓰나: 목록에서 "생성" 을 눌러 여는 섹션 여럿의 폼(side right, size large), 행을 눌러 보는 상세(루트 `Drawer` 에 `modal={false}` — 옆 패널). `DrawerTitle` 은 항상 넣는다.
1183
+ 언제 안 쓰나: 필드 서너 개의 짧은 입력(→ Dialog), 되돌릴 수 없는 확인(→ AlertDialog), 전용 URL 이 필요한 화면(→ 페이지).
1184
+ 대신/함께: 폼이면 `@t3q-ui/design-system/patterns` 의 FormDrawer 가 제출 중 닫힘 차단까지 포함한다. 본문은 `DrawerBody` 로 스크롤시킨다.
1185
+
1186
+ | prop | 타입 | 기본값 | 설명 |
1187
+ |---|---|---|---|
1188
+ | `forceMount` | `true` | | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
1189
+ | `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` | | Event handler called when the escape key is down. Can be prevented. |
1190
+ | `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` | | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
1191
+ | `onFocusOutside` | `((event: FocusOutsideEvent) => void)` | | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
1192
+ | `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` | | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prevented. |
1193
+ | `onOpenAutoFocus` | `((event: Event) => void)` | | Event handler called when auto-focusing on open. Can be prevented. |
1194
+ | `onCloseAutoFocus` | `((event: Event) => void)` | | Event handler called when auto-focusing on close. Can be prevented. |
1195
+
1196
+ 예시 — story "Sizes — small·medium·large·xLarge":
1197
+
1198
+ ```tsx
1199
+ const Sizes = () => (
1200
+ <div className='flex gap-[8px]'>
1201
+ {(['small', 'medium', 'large', 'xLarge'] as const).map((size) => (
1202
+ <Drawer key={size}>
1203
+ <DrawerTrigger asChild>
1204
+ <Button variant='secondaryOutline'>{size}</Button>
1205
+ </DrawerTrigger>
1206
+ <DrawerContent size={size}>
1207
+ <DrawerHeader>
1208
+ <DrawerTitle>{size} 크기</DrawerTitle>
1209
+ <DrawerDescription>size=&quot;{size}&quot;</DrawerDescription>
1210
+ </DrawerHeader>
1211
+ </DrawerContent>
1212
+ </Drawer>
1213
+ ))}
1214
+ </div>
1215
+ );
1216
+ ```
1217
+
1218
+ ## PopoverContent (`components-overlay-popover`)
1219
+
1220
+ | prop | 타입 | 기본값 | 설명 |
1221
+ |---|---|---|---|
1222
+ | `forceMount` | `true` | | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
1223
+ | `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` | | Event handler called when the escape key is down. Can be prevented. |
1224
+ | `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` | | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
1225
+ | `onFocusOutside` | `((event: FocusOutsideEvent) => void)` | | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
1226
+ | `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` | | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prevented. |
1227
+ | `onOpenAutoFocus` | `((event: Event) => void)` | | Event handler called when auto-focusing on open. Can be prevented. |
1228
+ | `onCloseAutoFocus` | `((event: Event) => void)` | | Event handler called when auto-focusing on close. Can be prevented. |
1229
+
1230
+ 예시 — story "WithAnchor — 트리거와 다른 기준 요소":
1231
+
1232
+ ```tsx
1233
+ const WithAnchor = () => <AnchorExample />;
1234
+ ```
1235
+
1236
+ ## Toaster (`components-overlay-toast`)
1237
+
1238
+ 화면 모서리에 잠깐 떴다 사라지는 알림(sonner). 앱 루트에 한 번 두고 `toast()` 로 띄운다.
1239
+
1240
+ 언제 쓰나: 저장·삭제 완료, 서버 오류처럼 지나가도 되는 결과 알림. 성공은 짧게, 오류는 원인 한 줄.
1241
+ 언제 안 쓰나: 화면에 남아 있어야 하는 안내·오류(→ Alert), 사용자의 확인이 필요한 것(→ AlertDialog), 폼 필드 오류(→ FormMessage).
1242
+ 대신/함께: `ThemeProvider` 안에 두어야 다크 모드를 따라간다.
1243
+
1244
+ | prop | 타입 | 기본값 | 설명 |
1245
+ |---|---|---|---|
1246
+ | `loadingIcon` | `ReactNode` | | @deprecated Please use the `icons` prop instead: ```jsx <Toaster icons={{ loading: <LoadingIcon /> }} /> ``` |
1247
+
1248
+ 예시 — story "Variants":
1249
+
1250
+ ```tsx
1251
+ const Variants = () => (
1252
+ <>
1253
+ <Toaster />
1254
+ <div className='flex flex-wrap gap-[12px]'>
1255
+ {VARIANTS.map((variant) => (
1256
+ <Button
1257
+ key={variant}
1258
+ variant='secondaryOutline'
1259
+ onClick={() => toast({ title: variant[0].toUpperCase() + variant.slice(1), variant })}
1260
+ >
1261
+ {variant} 토스트
1262
+ </Button>
1263
+ ))}
1264
+ </div>
1265
+ </>
1266
+ );
1267
+ ```
1268
+
1269
+ ## Tooltip (`components-overlay-tooltip`)
1270
+
1271
+ | prop | 타입 | 기본값 | 설명 |
1272
+ |---|---|---|---|
1273
+ | `delayDuration` | `number` | | The duration from when the pointer enters the trigger until the tooltip gets opened. This will override the prop with the same name passed to Provider. @defaultValue 700 |
1274
+ | `disableHoverableContent` | `boolean` | | When `true`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. @defaultValue false |
1275
+
1276
+ 예시 — story "Playground":
1277
+
1278
+ ```tsx
1279
+ const Playground = ({ content, side, align, delayDuration }) => (
1280
+ <Tooltip delayDuration={delayDuration}>
1281
+ <TooltipTrigger asChild>
1282
+ <Button variant='secondary'>마우스를 올려보세요</Button>
1283
+ </TooltipTrigger>
1284
+ <TooltipContent side={side} align={align}>
1285
+ {content}
1286
+ </TooltipContent>
1287
+ </Tooltip>
1288
+ );
1289
+ ```
1290
+
1291
+ # 컴포넌트 — table
1292
+
1293
+ ## AgGridTable (`components-table-aggridtable`)
1294
+
1295
+ ag-grid 기반 데이터 표. 목록 화면의 본 표에 쓴다.
1296
+
1297
+ 언제 쓰나: 서버 페이징 목록, 정렬·컬럼 숨김·행 선택·셀 렌더러가 필요한 표. `loading` 이면 TableSkeleton, 0건이면 `noRowsOverlayComponent`. 주 식별자 열(보통 이름)에는 `type: 'primary'` 를 주면 굵게 강조되고 클릭 가능한 행에서 hover 시 밑줄이 생긴다. `sortable: true` 로 명시한 열은 정렬 전에도 정렬 아이콘이 보인다(그 열에 `unSortIcon: false` 를 주면 끔. 2026-10-16 다시 켬 — 원문 심판이 "정렬 가능 표시 없음" 을 반복 감점). 숫자 열(건수·크기·비율·시간)은 `type: 'number'` — 오른쪽 정렬·숫자 폭이고, width·flex 를 안 주면 폭 140(ag-grid 기본 200 은 비어 보인다). 숫자 열에 `flex` 를 주지 않는다.
1298
+ 언제 안 쓰나: 수십 행 이하의 단순 나열(→ Table), 키-값 표(→ MetaItem), 편집 가능한 파라미터 표(→ ParameterTable).
1299
+ 대신/함께: 목록 화면 전체 골격은 `@t3q-ui/design-system/patterns` 의 ListPage 가 이 표를 포함한다. 정렬 값은 `getSortedColumnState` 로 읽는다. `onRowClicked` 를 주면 포커스된 셀에서 Enter 로도 호출된다(키보드 상세 진입). 열 제목이 들어가는 최소 폭은 표가 보정하고(`width: 110` 같은 좁은 폭을 줘도 "크기(M…" 로 잘리지 않음) 넘치는 셀 글자는 말줄임표로 끝난다. 보조 정보(id·이미지·경로·설명)는 열을 늘리지 말고 `TwoLineCellRenderer`(`cellRendererParams.secondaryField`)로 이름 아래 줄에 — ListPage 는 `secondaryField` 한 줄로 된다. 이름 열만 `flex: 1, minWidth: 200`(남는 폭을 가진다 — maxWidth 를 주면 표 오른쪽이 빈다), 숫자·날짜 열은 `width` 로.
1300
+
1301
+ | prop | 타입 | 기본값 | 설명 |
1302
+ |---|---|---|---|
1303
+ | `modules` | `Module[]` | | Used to register AG Grid Modules directly with this instance of the grid. See [Providing Modules To Individual Grids](https://www.ag-grid.com/react-data-grid/modules/#providing-modules-to-individual-grids) for more information. |
1304
+ | `containerStyle` | `any` | | The CSS style to be applied to the grid's outermost div element. |
1305
+ | `className` | `string` | | The CSS class to be applied to the grid's outermost div element. |
1306
+ | `componentWrappingElement` | `string` | | Default: div |
1307
+ | `maxComponentCreationTimeMs` | `number` | | @deprecated v33.3 It is expected that this property is no longer required and so will be removed in a future version. If you require this property please contact support. |
1308
+ | `setGridApi` | `((gridApi: GridApi<any>) => void)` | | @deprecated v33.3 This method is not called and will be removed. To see how to access the GridApi visit: https://ag-grid.com/react-data-grid/grid-interface/#grid-api |
1309
+ | `children` | `any` | | @deprecated v33.3 AgGridReact does not accept children so this property will be removed. |
1310
+
1311
+ 예시 — story "LongPlainText — 평문 셀 말줄임(…) · renderer 셀과 나란히":
1312
+
1313
+ ```tsx
1314
+ const LongPlainText = () => <AgGridTable
1315
+ rowData={ROWS.map((r) => ({
1316
+ ...r,
1317
+ regr_nm: `${r.regr_nm} · GPU 클러스터 · a100-prod-cluster-seoul-01`,
1318
+ }))}
1319
+ columnDefs={[
1320
+ { field: 'regr_nm', headerName: '감시 대상 (평문, 160px)', width: 160 },
1321
+ {
1322
+ field: 'algorithm.name',
1323
+ headerName: '알고리즘 (평문, valueFormatter)',
1324
+ width: 170,
1325
+ valueFormatter: (p: ValueFormatterParams<OpermodelRow>) =>
1326
+ `${p.value} · ${p.data?.cpu} vCPU · ${p.data?.memory} GB · ${p.data?.reg_dt}`,
1327
+ },
1328
+ ...COLUMN_DEFS.filter((c) => c.field === 'use_yn' || c.field === 'name'),
1329
+ { field: 'memory', headerName: '메모리(GB)', width: 130, type: 'number' },
1330
+ ]}
1331
+ defaultColDef={{
1332
+ resizable: false,
1333
+ sortable: false,
1334
+ filter: false,
1335
+ suppressMovable: true,
1336
+ }}
1337
+ rowHeight={56}
1338
+ containerStyle={{ height: 360 }} />;
1339
+ ```
1340
+
1341
+ ## ColumnFilter (`components-table-columnfilter`)
1342
+
1343
+ | prop | 타입 | 기본값 | 설명 |
1344
+ |---|---|---|---|
1345
+ | `columns` (필수) | `ColumnDefinition[]` | | 필터링할 컬럼 정의 |
1346
+ | `value` | `FilterCondition[]` | | 현재 필터 조건 (controlled) |
1347
+ | `onChange` | `((filters: FilterCondition[]) => void)` | | 필터 변경 콜백 |
1348
+ | `onSearch` | `((query: string) => void)` | | 통합 검색 쿼리 변경 콜백. 제공 여부가 검색 팝오버의 Enter 동작을 가른다. - 제공: 첫 제안을 선점하지 않는다. 선택 없는 Enter는 입력 텍스트로 통합 검색을 확정한다. - 미제공: 팝오버가 열리면 첫 제안이 하이라이트(`aria-activedescendant`)되어 ArrowDown 없는 Enter가 그 컬럼의 편집으로 진입한다. 자유 텍스트를 받을 곳이 없기 때문이다. |
1349
+ | `placeholder` | `string` | | 플레이스홀더 텍스트 |
1350
+ | `className` | `string` | | 추가 CSS 클래스 |
1351
+ | `disabled` | `boolean` | | 비활성화 여부 |
1352
+ | `getValueSuggestions` | `((columnKey: string, query: string) => string[] \| Promise<string[]>)` | | 값 자동완성 콜백. select 타입에 options가 있으면 무시됨 |
1353
+ | `operatorsByType` | `Partial<Record<ColumnType, Operator[]>>` | | 컬럼 타입별 사용 가능 연산자 override. 지정하지 않으면 기본 연산자 목록을 사용한다. |
1354
+ | `operatorsByColumn` | `Partial<Record<string, Operator[]>>` | | 컬럼 key별 사용 가능 연산자 override. operatorsByType보다 우선한다. |
1355
+
1356
+ 예시 — story "Controlled — 외부 state 와 onSearch":
1357
+
1358
+ ```tsx
1359
+ const Controlled = () => <ControlledExample />;
1360
+ ```
1361
+
1362
+ ## ColumnVisibilityFilter (`components-table-columnvisibilityfilter`)
1363
+
1364
+ 예시 — story "Controlled — 외부 state 로 표시 컬럼 관리":
1365
+
1366
+ ```tsx
1367
+ const Controlled = () => <Example options={OPTIONS} initial={['project', 'author']} searchable={false} />;
1368
+ ```
1369
+
1370
+ ## ParameterTable (`components-table-parametertable`)
1371
+
1372
+ | prop | 타입 | 기본값 | 설명 |
1373
+ |---|---|---|---|
1374
+ | `cellErrors` | `ParameterTableCellErrors` | | 셀별 invalid 표시. cellErrors[rowId][colId] === true 인 셀에 빨간 강조. trailing placeholder 행은 자동으로 제외된다. |
1375
+
1376
+ 예시 — story "Default":
1377
+
1378
+ ```tsx
1379
+ const Default = () => <ParameterTable caption="Parameter Table" />;
1380
+ ```
1381
+
1382
+ ## Table (`components-table-table`)
1383
+
1384
+ HTML table 기반의 가벼운 표. 정렬·컬럼 조작이 없는 소량 데이터 나열에 쓴다.
1385
+
1386
+ 언제 쓰나: 상세 화면의 속성 표, 수십 행 이하의 고정 목록, 셀 안에 자유로운 마크업이 필요할 때. `TableHeader`·`TableRow`·`TableCell` 로 조립한다. 숫자 열(건수·비율·크기)은 `TableHead align='right'` — 폭 140 으로 고정되고 글자 열이 남는 폭을 가진다(표는 `table-fixed`, 다른 폭은 `className='w-…'`).
1387
+ 언제 안 쓰나: 서버 페이징·정렬·컬럼 숨김·가상 스크롤이 필요한 목록(→ AgGridTable), 키-값 몇 줄(→ MetaItem).
1388
+ 대신/함께: 페이지네이션은 `TablePagination` 안에 `Pagination`, 빈 상태는 행 하나로 직접 그린다.
1389
+
1390
+ 예시 — story "Default — 기본 테이블":
1391
+
1392
+ ```tsx
1393
+ const Default = () => <BasicTable />;
1394
+ ```
1395
+
1396
+ ## TableFilterMultiSelect (`components-table-tablefilter`)
1397
+
1398
+ 목록 화면 상단의 필터 드롭다운(복수 선택). 표의 결과를 거르는 조건에 쓴다.
1399
+
1400
+ 언제 쓰나: PageHeader 의 필터 줄에서 상태·유형처럼 값 집합이 정해진 조건. 바뀌면 소비 앱이 1페이지로 되돌린다.
1401
+ 언제 안 쓰나: 폼 입력값(→ Select·MultiSelect), 자유 텍스트 검색·컬럼별 조건(→ ColumnFilter), 날짜 범위(→ TableFilterDateRangeSelect).
1402
+ 대신/함께: 하나만 고르는 조건은 TableFilterSingleSelect. 여러 필터를 한 번에 비우는 "필터 초기화" 링크를 같이 둔다.
1403
+
1404
+ | prop | 타입 | 기본값 | 설명 |
1405
+ |---|---|---|---|
1406
+ | `options` (필수) | `Option[]` | | 선택 드롭다운에 표시될 옵션 배열 |
1407
+ | `onChange` (필수) | `(selected: string[]) => void` | | 선택이 변경될 때 호출되는 콜백 함수 |
1408
+ | `defaultValue` | `string[]` | `[]` | 초기 선택된 값들 |
1409
+ | `value` | `string[]` | | 현재 선택된 값들 (제어 컴포넌트로 사용 시) |
1410
+ | `placeholder` | `string` | | 아무것도 고르지 않았을 때의 **값** 문구. `title` 이 있으면 트리거는 항상 "{title} · {값}"(예: 상태 · 전체)으로 그려지고, 이 값의 기본은 "전체". title 이 없으면 기본 "선택" |
1411
+ | `emptyText` | `string` | `조회 가능한 옵션이 없습니다.` | 사용 가능한 옵션이 없을 때 표시되는 텍스트 |
1412
+ | `allText` | `string` | `전체 선택` | 목록 안 '전체 선택' 항목의 텍스트. 트리거의 전체 상태 문구는 `placeholder`(기본 "전체", title 이 있으면 "{title} · 전체") 가 맡는다 |
1413
+ | `className` | `{ button?: string; PopoverContent?: string; Command?: string \| undefined; Comm…` | | 컴포넌트의 각 부분에 대한 className 오버라이드 |
1414
+ | `searchPlaceholder` | `string` | `검색` | 검색 입력창의 placeholder 텍스트 |
1415
+ | `searchable` | `boolean` | `true` | 검색 입력창 표시 여부 |
1416
+ | `allowEmpty` | `boolean` | `false` | 전체 선택 해제 허용 여부 (기본값: false) |
1417
+ | `loading` | `boolean` | `false` | 옵션 로딩 상태 |
1418
+ | `loadingText` | `string` | `로드 중` | 로딩 상태에 표시할 텍스트 |
1419
+
1420
+ 예시 — story "DateSelect — 단일 날짜":
1421
+
1422
+ ```tsx
1423
+ const DateSelect = () => <DateSelectExample />;
1424
+ ```