sellmate-design-system-react 8.1.0 → 9.0.0-beta.10

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.
Files changed (76) hide show
  1. package/AGENTS.md +279 -124
  2. package/README.md +101 -8
  3. package/dist/components/SChipFilter/README.md +70 -0
  4. package/dist/components/SChipFilter/SChipFilter.d.ts +167 -0
  5. package/dist/components/SChipFilter/index.d.ts +1 -0
  6. package/dist/components/SCircleProgress/README.md +3 -0
  7. package/dist/components/SCircleProgress/SCircleProgress.d.ts +7 -1
  8. package/dist/components/SDatePicker/README.md +43 -3
  9. package/dist/components/SDatePicker/SDatePicker.d.ts +36 -3
  10. package/dist/components/SDatePicker/index.d.ts +1 -1
  11. package/dist/components/SDateRangePicker/README.md +21 -0
  12. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +16 -0
  13. package/dist/components/SDateRangePicker/index.d.ts +1 -1
  14. package/dist/components/SField/README.md +3 -0
  15. package/dist/components/SField/SField.d.ts +12 -0
  16. package/dist/components/SGhostButton/README.md +6 -0
  17. package/dist/components/SGnb/gnb.config.d.ts +2 -0
  18. package/dist/components/SGuide/README.md +1 -1
  19. package/dist/components/SGuide/SGuide.d.ts +1 -1
  20. package/dist/components/SIcon/README.md +6 -0
  21. package/dist/components/SIcon/icons.gen.d.ts +1 -0
  22. package/dist/components/SImage/README.md +44 -0
  23. package/dist/components/SImage/SImage.d.ts +41 -0
  24. package/dist/components/SImage/index.d.ts +1 -0
  25. package/dist/components/SKeyValueTable/README.md +1 -0
  26. package/dist/components/SKeyValueTable/SKeyValueTable.d.ts +8 -1
  27. package/dist/components/SListItem/README.md +2 -2
  28. package/dist/components/SListItem/SListItem.d.ts +2 -2
  29. package/dist/components/SPage/README.md +23 -0
  30. package/dist/components/SPage/SPage.d.ts +27 -1
  31. package/dist/components/SPage/SPageHeader.d.ts +32 -0
  32. package/dist/components/SPage/index.d.ts +2 -1
  33. package/dist/components/SPage/page.config.d.ts +8 -0
  34. package/dist/components/SPortal/README.md +1 -1
  35. package/dist/components/SPortal/SPortal.d.ts +7 -1
  36. package/dist/components/SRadio/README.md +2 -0
  37. package/dist/components/SRadioButton/README.md +13 -0
  38. package/dist/components/SSearchInput/README.md +48 -0
  39. package/dist/components/SSearchInput/SSearchInput.d.ts +46 -0
  40. package/dist/components/SSearchInput/index.d.ts +1 -0
  41. package/dist/components/SSectionHeaderCard/README.md +10 -23
  42. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.d.ts +21 -14
  43. package/dist/components/SSectionHeaderCard/index.d.ts +1 -1
  44. package/dist/components/SSelect/README.md +4 -2
  45. package/dist/components/SSelect/SSelect.d.ts +2 -2
  46. package/dist/components/STable/README.md +1 -0
  47. package/dist/components/STable/STable.d.ts +20 -0
  48. package/dist/components/STabs/README.md +0 -1
  49. package/dist/components/STabs/STabs.d.ts +2 -4
  50. package/dist/components/STabs/index.d.ts +1 -1
  51. package/dist/components/STabs/tabs.config.d.ts +3 -4
  52. package/dist/components/STag/README.md +2 -0
  53. package/dist/components/STextLink/README.md +2 -0
  54. package/dist/components/STimePicker/timepicker.config.d.ts +5 -0
  55. package/dist/components/SToggle/README.md +1 -0
  56. package/dist/components/SToggle/SToggle.d.ts +4 -1
  57. package/dist/components/STooltip/README.md +2 -0
  58. package/dist/components/STree/README.md +3 -0
  59. package/dist/components/STree/STree.d.ts +8 -0
  60. package/dist/components/STree/index.d.ts +1 -1
  61. package/dist/index.cjs +2988 -393
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.d.ts +3 -0
  64. package/dist/index.js +2981 -394
  65. package/dist/index.js.map +1 -1
  66. package/dist/lib/floating-width.d.ts +22 -0
  67. package/dist/lib/truncated-value-tooltip.d.ts +18 -0
  68. package/dist/llms-full.txt +545 -152
  69. package/dist/llms.txt +283 -127
  70. package/dist/styles.css +397 -36
  71. package/dist/theme.css +34 -11
  72. package/eslint/index.mjs +5 -0
  73. package/eslint/rules/field-width-grade.d.mts +41 -0
  74. package/eslint/rules/field-width-grade.mjs +294 -0
  75. package/eslint/scale.gen.mjs +1 -1
  76. package/package.json +7 -4
@@ -35,12 +35,12 @@
35
35
  | 분류 | 컴포넌트 |
36
36
  | --- | --- |
37
37
  | **버튼·링크** | `SButton` `SGhostButton` `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
38
- | **입력 (폼)** | `SForm` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
39
- | **날짜·시간** | `SCalendar` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
40
- | **표·목록** | `STable` `STableBar` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
41
- | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
38
+ | **입력 (폼)** | `SForm` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
39
+ | **날짜·시간** | `SCalendar` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
40
+ | **표·목록** | `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
41
+ | **레이아웃** | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
42
42
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
43
- | **표시·상태** | `STag` `SBadge` `SIcon` `SCallout` `SGuide` |
43
+ | **표시·상태** | `STag` `SBadge` `SIcon` `SImage` `SCallout` `SGuide` |
44
44
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
45
45
  | **오버레이** | `STooltip` `SPopover` `SPopup` `SDrawer` `SPortal` |
46
46
  | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` `SModalOutlet`(앱 루트 1회) |
@@ -68,6 +68,7 @@ AI 에이전트는 코드를 생성하기 전에 이 목록을 반드시 지킨
68
68
  | --- | --- |
69
69
  | `<button>` | `SButton`, `SGhostButton`, `SDropdownButton`, `STextLink` |
70
70
  | `<input type="text/password/...">` | `SInput` |
71
+ | `<input type="search">` | `SSearchInput` |
71
72
  | `<input type="number">` | `SNumberInput` |
72
73
  | `<input type="checkbox">` | `SCheckbox`, `SToggle`, `SSwitch` |
73
74
  | `<input type="radio">` | `SRadio`, `SRadioButton` |
@@ -80,7 +81,7 @@ AI 에이전트는 코드를 생성하기 전에 이 목록을 반드시 지킨
80
81
  | `alert()`, `confirm()` | `SToast`, `SModal.confirm(...)` |
81
82
  | 직접 만든 탭/페이지네이션/스텝퍼 | `STabs`, `SPagination`, `SStepper` |
82
83
  | `<ul>`/`<li>` 로 만든 목록 UI | `SList` + `SListItem` (드래그 정렬은 `SDraggableItem`) |
83
- | 직접 만든 섹션 카드(제목 바 + 본문 박스) | `SSectionHeaderCard` + `.Header` / `.Body` |
84
+ | 직접 만든 섹션 카드(제목 바 + 본문 박스) | `SSectionHeaderCard` 의 `title` / `padding` props |
84
85
  | `<svg>` 직접 삽입, 이모지 아이콘 | `SIcon` |
85
86
  | `<hr>` | `SDivider` |
86
87
  | `<details>` / `<summary>` | `SExpansionItem` |
@@ -144,9 +145,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
144
145
 
145
146
  | 층 | 무엇인가 | 컴포넌트 |
146
147
  | --- | --- | --- |
147
- | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage` |
148
- | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
149
- | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SLinearProgress` `SCircleProgress` |
148
+ | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) |
149
+ | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
150
+ | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SImage` `SLinearProgress` `SCircleProgress` |
150
151
  | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
151
152
  | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
152
153
 
@@ -166,7 +167,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
166
167
  | 담는 것 | 올 수 있는 것 | 오면 안 되는 것 |
167
168
  | --- | --- | --- |
168
169
  | `SPage` | **블록만** | **요소를 직접** — 버튼 하나도 블록에 담아 놓는다 |
169
- | `SSectionHeaderCard.Body` | 그리는 블록 · 요소 | `SSectionHeaderCard` · `SCard` (카드 겹침, §3-7-8) |
170
+ | `SSectionHeaderCard` 의 `children` | 그리는 블록 · 요소 | `SSectionHeaderCard` · `SCard` (카드 겹침, §3-7-8) |
170
171
  | `SCard` | 그리는 블록 · 요소 | `SCard` · `SSectionHeaderCard` |
171
172
  | `SForm` | 블록 (보통 `SKeyValueTable` + 하단 액션) | — |
172
173
  | `SSplitter.Before` / `.After` | 블록 | — |
@@ -194,9 +195,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
194
195
  블록 순서는 화면 종류와 무관하게 같다. **필요한 것만 남기되 순서를 바꾸지 않는다.**
195
196
 
196
197
  ```text
197
- 1. 페이지 제목 (+ 가이드·매뉴얼 링크. 액션 버튼은 오지 않는다 §4-2)
198
+ 1. 페이지 제목 `SPage` 의 `header` prop (+ 가이드·매뉴얼 링크는 slot. 액션 버튼은 오지 않는다 §4-2)
198
199
  2. 상시 안내 SCallout
199
- 3. 필터 SKeyValueTable
200
+ 3. 필터 SKeyValueTable · SChipFilter (§3-7-11)
200
201
  4. 툴바 STableBar (건수 요약 + 액션)
201
202
  5. 본문 STable · 섹션 카드들 · SList …
202
203
  6. 페이지네이션 SPagination (STable 이 pagination prop 으로 직접 그린다)
@@ -222,7 +223,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
222
223
 
223
224
  | 층 (§2-0) | 역할 | 클래스 | 크기 |
224
225
  | --- | --- | --- | --- |
225
- | 셸 | 페이지 제목 (h1) | `typo-heading-lg` | 18px |
226
+ | 셸 | 페이지 제목 (`SPage` 의 `header.title`) | `typo-heading-lg` | 18px |
226
227
  | 블록 | 섹션 제목 | `typo-heading-sm` | 14px |
227
228
  | 블록 내부 | 하위 제목 (섹션 안을 더 나눌 때) | `typo-heading-xs` | 12px |
228
229
  | — | 본문 | `typo-body-sm-default` | 12px |
@@ -230,7 +231,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
230
231
 
231
232
  페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
232
233
 
233
- - **섹션 제목의 타이포를 직접 주지 않는다.** `SSectionHeaderCard.Header` 가 `title` 에 이미 넣는다 — 그 위에 `typo-heading-sm` 을 또 씌우지 않는다. 직접 쓰는 경우는 섹션 카드 없이 제목만 세울 때뿐이다.
234
+ - **페이지·섹션 제목의 타이포를 직접 주지 않는다.** `SPage` 의 `header.title`, `SSectionHeaderCard` 의 `header.title` 이 이미 넣는다 — 그 위에 `typo-heading-lg`/`typo-heading-sm` 을 또 씌우지 않는다.
234
235
  - **하위 제목이 필요하면 먼저 섹션을 나눌 수 없는지 본다.** 한 섹션 안에서 제목이 두 단으로 갈린다는 것은 대개 섹션이 둘이라는 뜻이다 (§3-7-8).
235
236
  - 본문 안에서 한 단어를 강조할 때는 `typo-body-sm-medium` 을 쓴다. `typo-body-sm-bold` 는 제목 성격의 짧은 라벨에만 쓴다. <!-- TODO(디자인): 강조 굵기 기준 확정 -->
236
237
 
@@ -300,7 +301,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
300
301
 
301
302
  **페이지 프레임은 예외 없이 `SPage` 가 넣는다.** 아래 규칙은 그 안의 **섹션·패널 레벨에만** 적용된다.
302
303
 
303
- **컴포넌트가 자체 여백을 가지면 컴포넌트 기준이 우선한다.** `SKeyValueTable`·`STable` 처럼 자기 여백을 토큰으로 갖고 있는 컴포넌트에는 이 판정을 적용하지 않는다 — 손댈 것이 없다. 아래 판정이 필요한 자리는 **직접 만든 컨테이너**와 **`SSectionHeaderCard.Body`** 두 곳뿐이다.
304
+ **컴포넌트가 자체 여백을 가지면 컴포넌트 기준이 우선한다.** `SKeyValueTable`·`STable` 처럼 자기 여백을 토큰으로 갖고 있는 컴포넌트에는 이 판정을 적용하지 않는다 — 손댈 것이 없다. 아래 판정이 필요한 자리는 **직접 만든 컨테이너**와 **`SSectionHeaderCard` 의 `padding`** 두 곳뿐이다.
304
305
 
305
306
  판정은 **그 영역이 담고 있는 콘텐츠 덩어리의 종류 수**로 한다.
306
307
 
@@ -335,14 +336,47 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
335
336
 
336
337
  **중첩되면 안쪽 여백을 주지 않는다.** 24 영역 안에 또 여백을 주면 가장자리가 40 으로 벌어져 한 면적처럼 읽힌다. 안쪽 카드·목록이 **배경색이 다르거나 테두리가 있어** 경계가 스스로 보이는 경우에만 자기 여백을 유지한다.
337
338
 
338
- `SSectionHeaderCard.Body` 는 이 규칙을 **prop 으로 받는다** — 직접 `p-sd-*` 를 주지 않는다.
339
+ `SSectionHeaderCard` 는 이 규칙을 **`padding` prop 으로 받는다** — 직접 `p-sd-*` 를 주지 않는다.
339
340
 
340
341
  ```tsx
341
- <SSectionHeaderCard.Body>…</SSectionHeaderCard.Body> {/* 기본 = 16 */}
342
- <SSectionHeaderCard.Body padding="wide">…</SSectionHeaderCard.Body> {/* 3종류 이상 */}
343
- <SSectionHeaderCard.Body padding="none">…</SSectionHeaderCard.Body> {/* 표를 가장자리까지 */}
342
+ <SSectionHeaderCard title="기본 정보">…</SSectionHeaderCard> {/* 기본 = 16 */}
343
+ <SSectionHeaderCard title="기본 정보" padding="wide">…</SSectionHeaderCard> {/* 3종류 이상 */}
344
+ <SSectionHeaderCard title="기본 정보" padding="none">…</SSectionHeaderCard> {/* 표를 가장자리까지 */}
344
345
  ```
345
346
 
347
+ ##### 카드 가장자리까지 채우는 표는 자기 테두리를 끈다
348
+
349
+ 여기서 "표"는 **`STable` 과 `SKeyValueTable` 둘 다**다. 두 컴포넌트 모두 자기 바깥 테두리를 그리는데, 카드도 바깥 테두리를 그린다. `padding="none"` 으로 붙이면 **1px 두 개가 나란히 놓여 그 변만 2px** 로 보인다(카드의 다른 변은 1px 그대로라 굵기가 어긋난다).
350
+
351
+ ```tsx
352
+ <SSectionHeaderCard title="발주 내역" padding="none">
353
+ <SKeyValueTable fields={…} bordered={false} radius="useTop" />
354
+ <STable columns={…} rows={…} bordered={false} radius="useTop" />
355
+ </SSectionHeaderCard>
356
+ ```
357
+
358
+ - **`bordered={false}`** 로 표의 테두리를 끈다. 카드가 이미 그린다.
359
+ - **`radius="useTop"`** 으로 위쪽 모서리를 죽인다. 아래쪽 라운드는 카드가 처리한다.
360
+ - `STable` 은 **페이지네이션 바의 테두리와 `-mt-px` 겹침도 함께 꺼진다** — 본문 테두리가 없으면 겹칠 대상이 없어, 그대로 두면 페이지네이션만 테두리를 갖고 1px 어긋난다.
361
+
362
+ #### 본문 바탕 눌러앉히기 (선택)
363
+
364
+ `SSectionHeaderCard` 는 **`background="neutral"`** 로 본문 바탕을 한 단계 눌러앉힐 수 있다. 흰 면 덩어리(표·리스트)가 여럿일 때 그 덩어리들이 **"면 위에 놓인 객체"로 읽혀 묶음이 더 강하게 보인다.**
365
+
366
+ ```tsx
367
+ <SSectionHeaderCard title="발주 상세" background="neutral">…흰 면 표 여럿…</SSectionHeaderCard>
368
+ ```
369
+
370
+ **기본값(`frame`, 흰 면)이 틀린 것이 아니다.** 이 저장소의 표·리스트는 테두리·라운드·헤더 줄과 `gap-sd-12` 를 이미 갖고 있어 흰 바탕에서도 경계가 읽힌다. 위계를 한 단계 더 주고 싶을 때 고르는 수단이지, 덩어리가 둘 이상이면 반드시 깔아야 하는 규칙이 아니다.
371
+
372
+ 깔아도 **효과가 없는** 자리는 있다.
373
+
374
+ - **덩어리가 가장자리까지 차는 경우** — `padding="none"` 으로 표를 채우면 깐 바탕이 표에 완전히 가려 보이지 않는다.
375
+ - **덩어리에 회색 면이 섞인 경우** — 그 덩어리가 바탕과 같은 색이 되어 묻힌다.
376
+ - **맨 텍스트·폼 컨트롤만 있는 본문** — 떠오를 흰 면이 없다.
377
+
378
+ 바탕을 깐 경우, 표 사이 구분선(`SDivider`)은 대개 불필요해진다 — 색이 이미 경계를 만든다.
379
+
346
380
  #### 스크롤 영역의 하단 여백
347
381
 
348
382
  스크롤을 끝까지 내렸을 때 마지막 항목이 화면 경계에 붙으면 **목록이 끝난 것인지 더 있는 것인지** 읽히지 않는다. 그래서 스크롤 영역은 **하단만** 넓게 둔다. 나머지 세 방향은 위 16 / 24 규칙 그대로다.
@@ -352,7 +386,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
352
386
  | **패널 자체 스크롤** (좌측 목록, 중앙 대화 등) | 그 패널 안쪽 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` — `SPage` 와 같은 토큰이라 값이 바뀌어도 함께 따라간다 |
353
387
  | **페이지 단위 스크롤** | **`SPage` 가 넣는다. 직접 주지 않는다** |
354
388
 
355
- `SPage` 는 기본으로 넣으므로 **아무것도 하지 않으면 맞다.** 끄는 경우는 하나뿐이다 — **페이지네이션이 붙은 테이블.** 페이지네이션이 이미 "여기서 끝"을 알려주므로 `scrollEndSpacing={false}` 로 끈다 (§4-2 목록 페이지).
389
+ `SPage` 는 기본으로 넣으므로 **아무것도 하지 않으면 맞다.** 직접 끌 일은 없다 — **페이지네이션이 붙은 테이블**은 `contentHeight="fill"` 을 쓰고, 이 모드에서는 페이지가 스크롤하지 않아 스크롤 끝 여백이 **자동으로 무시**된다 (§4-2 목록 페이지).
356
390
 
357
391
  ### 2-3. 색상
358
392
 
@@ -414,6 +448,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
414
448
  | --- | --- | --- |
415
449
  | 한 줄 텍스트를 받는다 | `SInput` | §3-7-1 |
416
450
  | 여러 줄 텍스트를 받는다 | `STextarea` | §3-7-1 |
451
+ | 목록·결과를 검색어로 좁힌다 | `SSearchInput` | §3-7-1 |
417
452
  | 숫자(수량·금액)를 받는다 | `SNumberInput` | |
418
453
  | 바코드를 스캔해 받는다 | `SBarcodeInput` | |
419
454
  | 목록에서 하나 고르게 한다 | `SSelect` | §3-7-2 |
@@ -427,6 +462,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
427
462
  | 입력된 값 하나를 지우거나 고치게 한다 | `SChip` | §3-1 |
428
463
  | 파일을 받는다 | `SFilePicker` | |
429
464
  | 날짜 하나를 받는다 | `SDatePicker` | §3-7-4 |
465
+ | 연도 선택 리스트만 커스텀 조합에 넣는다 | `SDatePickerYearListbox` | §3-7-4 |
466
+ | 연도+월 선택 리스트만 커스텀 조합에 넣는다 | `SDatePickerMonthListbox` | §3-7-4 |
430
467
  | 날짜 기간을 받는다 | `SDateRangePicker` | §3-7-4 |
431
468
  | 시각 하나를 받는다 | `STimePicker` | |
432
469
  | 시각 범위를 받는다 | `STimeRangePicker` | |
@@ -434,6 +471,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
434
471
  | 컨트롤에 라벨·필수·에러를 붙인다 | `SField` | §3-7-5 |
435
472
  | 입력 여러 개를 묶어 한 번에 검증한다 | `SForm` | §4-3 |
436
473
  | 폼·필터를 표 형태로 배치한다 | `SKeyValueTable` | §4 |
474
+ | 필요한 검색 조건만 칩으로 골라 붙이게 한다 | `SChipFilter` | §3-7-11 |
437
475
 
438
476
  #### B. 정보를 읽게 보여준다
439
477
 
@@ -449,6 +487,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
449
487
  | 상태·분류를 라벨로 찍는다 | `STag` | §3-1 |
450
488
  | 색 점만으로 상태를 찍는다 | `SBadge` | §3-1 |
451
489
  | 아이콘을 넣는다 | `SIcon` | |
490
+ | 사진·썸네일을 보여준다 (로딩·실패 상태 포함) | `SImage` | §3-7-12 |
452
491
  | 문장 안에서 다른 화면으로 보낸다 | `STextLink` | §3-5-6 |
453
492
 
454
493
  #### C. 동작을 실행시킨다
@@ -466,6 +505,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
466
505
  | 앱 셸(상단바 + 내비 + 본문)을 세운다 | `SLayout` | §4-1 |
467
506
  | 좌측 내비게이션을 만든다 | `SGnb` | §4-1 |
468
507
  | 페이지 본문을 담는다 (패딩·스크롤) | `SPage` | §4-1 |
508
+ | 페이지 제목(+ 서브 텍스트·뒤로가기·우측 슬롯)을 만든다 | `SPage` 의 `header` prop | §4-1 |
469
509
  | 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
470
510
  | 제목 없이 흰 면으로만 묶는다 | `SCard` | §3-7-8 |
471
511
  | 가로선으로 끊는다 | `SDivider` | §3-6 |
@@ -555,9 +595,26 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
555
595
  | | 무엇인가 | 크기 |
556
596
  | --- | --- | --- |
557
597
  | **SPopup** | **별도 브라우저 창** (`window.open` 으로 여는 전용 라우트) | 창 크기 = 콘텐츠 크기 |
558
- | **SActionModal** | 같은 창 위 오버레이 카드 | `width` / `height` prop |
598
+ | **SActionModal** | 같은 창 위 오버레이 카드 | `width` prop. **높이는 주지 않는다** (아래) |
559
599
  | **SModal.confirm** (`SConfirmModal`) | 같은 창 위 확인창 | 고정 |
560
600
 
601
+ ##### 모달 높이는 내용이 정하고, 상한은 시스템이 건다
602
+
603
+ **`height` 를 주지 않는다.** 높이는 내용이 정하고, 카드는 **뷰포트의 85%** 에서 멈춘다(시스템이 모든 모달에 건다). 데이터가 적으면 내용만큼 작아지고, 많으면 85% 에서 멈춘다.
604
+
605
+ 가로는 좌우 24px 씩을 뺀 값으로 클램핑하는데 **세로만 비율**인 이유는, 모달이 화면을 거의 다 덮으면 뒤 맥락이 사라져 "떠 있는 것"으로 읽히지 않기 때문이다.
606
+
607
+ **상한에 닿았을 때 스크롤되어야 하는 것은 모달 본문이 아니라 표다.**
608
+
609
+ ```tsx
610
+ <SActionModal modalTitle="발주 검토" button={{ label: '확정', onClick: submit }}>
611
+ {/* 표가 남은 높이를 먹고 자기 안에서 스크롤한다 — 헤더·합계·푸터는 늘 보인다 */}
612
+ <STable className="min-h-0 flex-1" columns={columns} rows={rows} />
613
+ </SActionModal>
614
+ ```
615
+
616
+ 본문(`overflow-auto` 영역)이 통째로 스크롤되면 표 헤더와 합계 줄이 위로 밀려 사라진다. `SActionModal` 의 본문은 이미 `min-h-0 flex-1` 이므로, 표에 `min-h-0 flex-1` 을 주면 세로 축이 이어져 표만 스크롤한다 (§4-2 목록 페이지와 같은 사슬이다).
617
+
561
618
  **성격이 먼저 둘로 갈린다.**
562
619
 
563
620
  | 성격 | 정의 | 컴포넌트 |
@@ -745,6 +802,7 @@ const columns: STableColumn[] = [
745
802
 
746
803
  컬럼 폭은 `width` 로 **고정**되고, `<td>` 는 그 폭을 넘는 내용을 잘라낸다(`overflow: hidden`). 텍스트라면 말줄임으로 끝나지만, 셀에 `STag` · `SButton` · `SGhostButton` · `SSelect` · `SInput` · `SNumberInput` 처럼 **자기 폭을 가진 요소**를 넣으면 요소 자체가 잘려 **누르거나 읽거나 입력할 수 없게 된다.** `width` 를 생략해도 내용에 맞춰 늘어나지 않고 `STable` 의 기본 폭이 될 뿐이므로, 컨트롤이 들어가는 컬럼은 폭을 직접 판단해서 준다.
747
804
 
805
+ - **컨트롤이 들어가는 컬럼에는 `contentType: 'control'` 을 함께 준다.** 좌우 패딩이 텍스트용(넓게)에서 컨트롤용(좁게)으로 바뀌어, 같은 컬럼 폭에서도 요소가 쓸 폭이 넓어진다. 기본값은 `text` 다.
748
806
  - 기준은 **요소가 온전히 보이는 폭 + 셀 좌우 패딩**이다. 좌우 패딩은 `STable` 이 토큰으로 넣으므로(직접 주지 않는다) 그만큼을 뺀 나머지가 요소 몫이라는 점을 계산에 넣는다.
749
807
  - 요소가 둘 이상이면 요소 폭의 합에 **`gap` 까지** 더한다 (행 내부 인라인 액션 간격은 `gap-sd-4` 고정, §3-5-5).
750
808
  - 값에 따라 폭이 달라지는 요소(`STag` 라벨, 라벨 있는 버튼)는 **가장 긴 값** 기준으로 잡는다. `판매중` 에 맞춰두면 `판매중지 요청` 에서 잘린다.
@@ -752,6 +810,13 @@ const columns: STableColumn[] = [
752
810
  - 폭을 넉넉히 줄 수 없는 자리는 폭을 줄이는 게 아니라 **요소를 바꾼다** — 라벨 버튼 대신 아이콘만 있는 `SGhostButton`, `size="xs"` (§3-5-2, §3-5-5).
753
811
  - **`autoWidth` 는 해법이 아니다.** 내용에 맞춰 늘어나는 게 아니라 고정폭 컬럼들이 가져가고 **남은 폭을 나눠 갖는 것**이라, 테이블이 좁으면 역시 잘린다. 컨트롤 컬럼은 `width` 로 직접 확보한다.
754
812
 
813
+ ```tsx
814
+ { name: 'normal', label: '정상', field: 'normal', width: '96px',
815
+ align: 'center', contentType: 'control', render: row => <SNumberInput … /> },
816
+ ```
817
+
818
+ **셀 좌우 여백은 내용이 정한다.** 텍스트는 넓게, 컨트롤은 좁게다 — `SKeyValueTable` 은 `field.type` 으로 이 판정을 스스로 하지만, `STable` 의 셀은 소비 앱이 넘긴 임의의 `render` 결과라 컴포넌트가 알 수 없다. 그래서 `contentType` 으로 알려준다. 여백 값 자체는 토큰이 정하므로 `tdClass` 로 패딩을 직접 덮어쓰지 않는다.
819
+
755
820
  **`resizable` 테이블이면 `minWidth` 를 함께 준다.** resize 하한 기본값은 어떤 컨트롤도 담지 못할 만큼 작아, 사용자가 끝까지 끌면 그대로 잘린다. `width` 를 정한 근거와 같은 값을 하한으로 둔다 — 텍스트 컬럼과 달리 여기서는 더 줄일 여지가 없다.
756
821
 
757
822
  ```tsx
@@ -964,16 +1029,19 @@ const columns: STableColumn[] = [
964
1029
 
965
1030
  > §3-0 라우팅에서 이 절을 가리키는 자리들이다. <!-- TODO(디자인): 전체 검수·확정 -->
966
1031
 
967
- #### 3-7-1. SInput vs STextarea
1032
+ #### 3-7-1. SInput vs STextarea vs SSearchInput
968
1033
 
969
- **줄 수가 아니라 값의 성격으로 고른다.** 값의 길이를 미리 알 수 있으면 `SInput`, 없으면 `STextarea` 다.
1034
+ **먼저 "그 값이 저장되는가"를 본다.** 저장되면 폼 필드(`SInput`·`STextarea`), 화면을 좁히기만 하고 사라지면 `SSearchInput` 이다.
970
1035
 
971
1036
  | 값 | 사용 |
972
1037
  | --- | --- |
973
1038
  | 이름·코드·전화번호·URL 처럼 형식이 정해진 값 | `SInput` |
974
1039
  | 메모·사유·설명처럼 길이가 예측되지 않는 문장 | `STextarea` |
1040
+ | 지금 보이는 목록·결과를 좁히는 검색어 | `SSearchInput` |
1041
+
1042
+ 폼 필드 둘은 **줄 수가 아니라 값의 성격으로** 갈린다. 값이 길어질 수 있는데 `SInput` 을 쓰면 사용자가 자기가 쓴 것을 다시 읽지 못한다 — 한 줄 안에서 좌우로 스크롤해야 하기 때문이다. 반대로 짧은 값에 `STextarea` 를 쓰면 빈 공간이 남아 입력량을 잘못 기대하게 한다.
975
1043
 
976
- 값이 길어질 수 있는데 `SInput` 을 쓰면 사용자가 자기가 쓴 것을 다시 읽지 못한다 — 한 줄 안에서 좌우로 스크롤해야 하기 때문이다. 반대로 짧은 값에 `STextarea` 를 쓰면 빈 공간이 남아 입력량을 잘못 기대하게 한다.
1044
+ `SSearchInput` 은 폼 필드가 아니다 — 라벨·힌트·유효성 규칙·에러 메시지를 받지 않고, `SForm` 의 제출 검증 대상에도 들어가지 않는다. 돋보기 아이콘이 항상 앞에 붙어 "여기는 검색"임을 스스로 밝히므로 라벨을 따로 붙이지 않는다. 검색 실행은 `onSearch`(Enter) 로 받고, 값이 바뀔 때마다 좁히는 실시간 필터라면 `onValueChange` 만 쓴다. 반대로 검색어를 **저장하거나 검증해야 한다면** 그것은 폼 값이므로 `SInput` 이다.
977
1045
 
978
1046
  #### 3-7-2. 하나를 고르게 하는 다섯 — SSelect vs SRadioGroup vs SRadioButton vs STabs vs SRadio
979
1047
 
@@ -1010,11 +1078,25 @@ const columns: STableColumn[] = [
1010
1078
  | 판별 | 사용 |
1011
1079
  | --- | --- |
1012
1080
  | 날짜 **하나**를 값으로 받는다 | `SDatePicker` |
1081
+ | 연도 선택 리스트만 필요하다 (트리거·팝오버는 직접 조합) | `SDatePickerYearListbox` |
1082
+ | 연도+월 선택 리스트만 필요하다 (트리거·팝오버는 직접 조합) | `SDatePickerMonthListbox` |
1013
1083
  | **시작~종료** 를 값으로 받는다 | `SDateRangePicker` |
1014
1084
  | 달력 격자 **자체가 화면 콘텐츠** 다 (일정·이벤트 보기) | `SCalendar` |
1015
1085
 
1016
1086
  - **기간을 `SDatePicker` 두 개로 만들지 않는다.** 시작이 종료보다 뒤인 입력을 막는 검증과 한쪽만 고른 중간 상태 처리가 `SDateRangePicker` 안에 이미 있다. 두 개로 쪼개면 그게 전부 앱 몫이 된다.
1017
1087
  - `SDatePicker`·`SDateRangePicker` 는 내부적으로 `SCalendar` 를 팝오버로 띄운다. 값을 받는 자리에 `SCalendar` 를 직접 쓰지 않는다.
1088
+ - `SDatePickerYearListbox`·`SDatePickerMonthListbox` 는 `SDatePicker` 의 mode listbox 조각만 떼어낸 컴포넌트다. 일반 폼 입력에는 `SDatePicker mode="year" | "month"` 를 우선 쓰고, 다른 트리거·팝오버 안에 리스트만 끼워 넣을 때만 직접 쓴다.
1089
+
1090
+ **날짜·시간 피커는 폭 상한을 스스로 갖는다 — `width` 를 주지 않는다.** 값 길이가 `YYYY-MM-DD` 처럼 정해져 있어 컴포넌트가 사이즈별 상한을 안다. `SKeyValueTable` 이 모든 컨트롤에 `width="100%"` 를 넘기지만, 이 상한 덕분에 행 전체로 늘어나지 않고 제 폭에서 멈춘다.
1091
+
1092
+ | 컴포넌트 | `size="sm"` | `size="md"` |
1093
+ | --- | --- | --- |
1094
+ | `SDatePicker` | md | lg |
1095
+ | `SDateRangePicker` | lg | xl |
1096
+ | `STimePicker` | md | lg |
1097
+ | `STimeRangePicker` | md | lg (오전/오후 표시는 두 사이즈 모두 lg) |
1098
+
1099
+ `SDateRangePicker` 가 한 등급씩 위인 것은 값이 `YYYY-MM-DD ~ YYYY-MM-DD` 로 두 배가 넘기 때문이다. 같은 이유로 `STimeRangePicker` 의 오전/오후 모드도 sm 에서 한 등급 위를 쓴다 — 그 모드의 최소 폭이 md 등급을 이미 넘어, 그대로 두면 하한이 상한을 넘어 상한이 무력해진다.
1018
1100
 
1019
1101
  #### 3-7-5. SField 를 직접 쓰는 경우
1020
1102
 
@@ -1032,6 +1114,16 @@ const columns: STableColumn[] = [
1032
1114
 
1033
1115
  - **항목 하나의 속성을 나열하는 것은 목록이 아니다.** `라벨: 값` 이 세로로 쌓이는 것은 `SKeyValueTable` 이다 (§4-4).
1034
1116
  - `SList` 는 레이아웃만 담당한다. 펼침·단일 선택 동작이 필요하면 `SExpansionList` 다 (§3-7-7).
1117
+ - **항목 사이 구분선은 리스트가 그린다.** `SListItem`·`SExpansionItem`·`SDraggableItem` 셋 다 스스로 구분선을 그리지 않으므로, 목록을 감싸는 `SList`·`SExpansionList`·`SDraggableList` 에 `separator` 를 준다 — 아이템에 `border-b` 를 직접 붙이지 않는다. 테두리형(`bordered`)은 테두리가 구분 역할을 하므로 `separator` 대신 `useGap` 으로 띄운다.
1118
+ - **`SListItem` 은 기본이 표시 전용이다.** 눌러서 이동·선택하게 하려면 `clickable` 을 준다 — hover·`selected`·`interaction="chevron"` 표현이 전부 여기에 딸려 있어서, `clickable` 없이 `selected` 만 주면 선택 표시가 나오지 않는다. `SExpansionList` 는 선택을 자기가 관리하므로 자식 아이템을 알아서 클릭 가능하게 만든다.
1119
+
1120
+ ```tsx
1121
+ ✅ <SList separator><SListItem title="일반 문의" />…</SList> {/* 줄로 구분되는 목록 */}
1122
+ ✅ <SList useGap><SListItem title="일반 문의" bordered />…</SList> {/* 카드처럼 떨어진 목록 */}
1123
+ ✅ <SListItem title="일반 문의" clickable selected onClick={…} /> {/* 눌러서 고르는 목록 */}
1124
+ ❌ <SList><SListItem title="일반 문의" className="border-b" />…</SList>
1125
+ ❌ <SListItem title="일반 문의" selected /> {/* clickable 없으면 선택 표시가 안 나온다 */}
1126
+ ```
1035
1127
 
1036
1128
  #### 3-7-7. 펼치는 셋 — SExpansionItem vs SExpansionList vs STree
1037
1129
 
@@ -1041,7 +1133,7 @@ const columns: STableColumn[] = [
1041
1133
  | **한 번에 하나만** 열려야 한다 (아코디언) | `SExpansionList` + `SExpansionItem` |
1042
1134
  | **부모-자식 계층 자체**를 보여줘야 한다 (2단 이상, 연결선) | `STree` |
1043
1135
 
1044
- `SExpansionList` 는 depth 별 단일 확장·선택을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다.
1136
+ `SExpansionList` 는 depth 별 단일 확장·선택을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다. 항목 사이 구분선은 여기서도 리스트가 그린다 — `separator` 를 준다 (§3-7-6).
1045
1137
 
1046
1138
  #### 3-7-8. SCard vs SSectionHeaderCard
1047
1139
 
@@ -1052,7 +1144,7 @@ const columns: STableColumn[] = [
1052
1144
 
1053
1145
  - 페이지 골격에서 콘텐츠를 묶는 섹션은 **사실상 전부 `SSectionHeaderCard`** 다 (§4-4·§4-5). 제목·필수 표시·도움말·헤더 우측 액션이 전부 여기 붙는다.
1054
1146
  - **카드 안에 카드를 겹치지 않는다.** 섹션 안을 더 나눠야 하면 `SDivider` 로 끊거나(§3-6) 섹션을 둘로 분리한다.
1055
- - 안쪽 여백은 `SSectionHeaderCard.Body` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1147
+ - 안쪽 여백은 `SSectionHeaderCard` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1056
1148
 
1057
1149
  #### 3-7-9. SLinearProgress vs SCircleProgress
1058
1150
 
@@ -1067,6 +1159,37 @@ const columns: STableColumn[] = [
1067
1159
 
1068
1160
  `STooltip`·`SPopover`·`SSelect`·날짜 피커가 내부에서 쓰는 저수준 레이어다. 앵커에 붙여 띄우는 동작이 필요하면 **먼저 §3-3 에서 대응 컴포넌트를 찾는다.** `SPortal` 을 직접 쓰는 것은 그 넷 중 어느 것도 아닌 새로운 부착형 레이어를 만들 때뿐이고, 그때도 모달 안에서 열릴 수 있다면 소속 컨테이너를 맞춰야 한다.
1069
1161
 
1162
+ #### 3-7-11. 필터 둘 — SKeyValueTable vs SChipFilter
1163
+
1164
+ 둘 다 §2-0 블록 순서의 **3번 자리(필터)** 에 놓이고, 한 화면에 둘을 같이 두지 않는다.
1165
+
1166
+ | 상황 | 컴포넌트 |
1167
+ | --- | --- |
1168
+ | 조건이 정해져 있고 **항상 다 보여야** 한다 (기본 형태) | `SKeyValueTable` |
1169
+ | 조건 후보가 많아 **쓸 것만 골라 붙이고** 나머지는 숨겨야 한다 | `SChipFilter` |
1170
+
1171
+ - **기본은 `SKeyValueTable` 이다** (§4-2). 조건이 대여섯 개 이하로 고정이면 표로 펼쳐 두는 편이 한눈에 읽힌다.
1172
+ - `SChipFilter` 는 조건을 **칩 한 줄**로 접고, "필터 추가" 로 필요한 것만 꺼내 쓰게 한다. 칩을 누르면 편집 팝오버가 열리고, 날짜는 프리셋(오늘·지난 7일·사용자 지정)으로 고른다. 조건 후보가 많은 목록 화면에서 필터가 화면을 세로로 잡아먹는 것을 막는 용도다.
1173
+ - 검색 실행 시점이 다르다 — `SKeyValueTable` 필터는 앱이 검색 버튼을 직접 놓지만, `SChipFilter` 는 편집 팝오버가 닫히거나 "검색" 을 누를 때 `onSearch` 가 값 맵과 함께 호출된다. 값이 바뀌지 않았으면 호출되지 않는다.
1174
+ - 함께 걸어야 하는 조건(예: 기간 중 하나는 필수)이 있으면 `fields` 를 그룹으로 넘긴다. 규칙을 못 채운 동안 경고 툴팁이 떠 있고 `onSearch` 가 막힌다.
1175
+
1176
+ #### 3-7-12. 이미지 — SImage
1177
+
1178
+ 사진·썸네일은 `<img>` 를 직접 쓰지 않고 `SImage` 를 쓴다. **로딩·실패 상태를 컴포넌트가 이미 처리하기 때문이다.**
1179
+
1180
+ - **로딩 스피너와 실패 표시를 앱이 만들지 않는다.** `src` 가 없거나 로드에 실패하면 빈 이미지 아이콘이, 로딩 중에는 스피너가 자동으로 놓인다. 크기는 상자 높이에 비례하므로 썸네일이든 큰 미리보기든 따로 맞출 것이 없다.
1181
+ - **크기는 `ratio` 와 너비로 준다.** `ratio` 기본값이 `1`(정사각형)이라 너비만 주면 정사각형이 된다. `className`/`style` 로 높이를 직접 주면 그쪽이 이기고 `ratio` 는 무시된다.
1182
+ - 상자를 채우는 방식은 `fit`(기본 `cover`), 붙는 위치는 `position` 이다. 안쪽 `<img>` 에 네이티브 속성이 필요하면 `imgProps` 로 넘긴다 — 목록에서 고스트 드래그를 막는 `imgProps={{ draggable: false }}` 가 대표적이다.
1183
+ - 썸네일이 많은 목록에서는 `loadingShowDelay` 를 준다. 캐시된 이미지가 즉시 로드될 때 스피너가 한 프레임 번쩍이는 것을 막는다.
1184
+
1185
+ ```tsx
1186
+ ✅ <SImage src={item.thumbnailUrl} alt={item.name} style={{ width: 64 }} /> {/* 정사각 썸네일 */}
1187
+ ✅ <SImage src={banner} ratio={16 / 9} className="w-full" /> {/* 가로형 배너 */}
1188
+
1189
+ ❌ <img src={item.thumbnailUrl} /> {/* 로딩·실패 상태가 없다 */}
1190
+ ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
1191
+ ```
1192
+
1070
1193
  ---
1071
1194
 
1072
1195
  ## 4. 페이지 레시피 — 표준 골격
@@ -1077,11 +1200,12 @@ const columns: STableColumn[] = [
1077
1200
  >
1078
1201
  > **핵심 원칙 — 표 형태의 정보는 `SKeyValueTable` 로 만든다.** 필터·등록/수정 폼·상세 정보가 모두 여기 해당한다.
1079
1202
  > `SField` 컨트롤을 `div` 로 직접 나열해 폼을 만들지 않는다.
1203
+ > (필터만 예외가 하나 있다 — 조건 후보가 많아 골라 붙이게 해야 하면 `SChipFilter` 다. §3-7-11)
1080
1204
 
1081
1205
  ### 4-1. 앱 셸 (모든 페이지 공통)
1082
1206
 
1083
1207
  ```tsx
1084
- import { SLayout, SGnb, SPage, type SGnbMenuItem } from 'sellmate-design-system-react';
1208
+ import { SLayout, SGnb, SPage, type SGnbMenuItem, type SPageHeaderProps } from 'sellmate-design-system-react';
1085
1209
 
1086
1210
  const MENU: SGnbMenuItem[] = [
1087
1211
  { label: '주문', value: 'orders', icon: 'bill' },
@@ -1090,20 +1214,44 @@ const MENU: SGnbMenuItem[] = [
1090
1214
 
1091
1215
  export default function AppShell({
1092
1216
  children,
1217
+ header,
1093
1218
  scrollEndSpacing,
1094
- }: { children: React.ReactNode; scrollEndSpacing?: boolean }) {
1219
+ contentHeight,
1220
+ }: {
1221
+ children: React.ReactNode;
1222
+ header?: SPageHeaderProps;
1223
+ scrollEndSpacing?: boolean;
1224
+ contentHeight?: SPageContentHeight;
1225
+ }) {
1095
1226
  return (
1096
1227
  <SLayout type="box" header="fix">
1097
1228
  {/* type/header/folded 는 SLayout 에만 준다 — SGnb 는 context 에서 읽는다 */}
1098
1229
  <SGnb items={MENU} value={current} onValueChange={navigate} logo={<Logo />} />
1099
1230
  {/* 콘텐츠 패딩은 SPage 가 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
1100
1231
  {/* 스크롤 끝 여백도 SPage 가 넣는다. 끄는 건 페이지네이션 있는 목록뿐이라 페이지가 정한다 */}
1101
- <SPage background="frame" scrollEndSpacing={scrollEndSpacing}>{children}</SPage>
1232
+ {/* 본문이 남은 높이를 채우는 목록 페이지는 contentHeight="fill" 을 넘긴다 (§4-2) */}
1233
+ {/* header 는 페이지마다 달라 AppShell 이 그대로 받아 넘긴다 — 페이지 제목은 여기서 만들지 않는다 */}
1234
+ <SPage
1235
+ background="frame"
1236
+ scrollEndSpacing={scrollEndSpacing}
1237
+ contentHeight={contentHeight}
1238
+ header={header}
1239
+ >
1240
+ {children}
1241
+ </SPage>
1102
1242
  </SLayout>
1103
1243
  );
1104
1244
  }
1105
1245
  ```
1106
1246
 
1247
+ **페이지는 `AppShell` 을 직접 호출하며 자기 `header` 를 넘긴다** — SPage 는 셸 안에 하나뿐이므로, 페이지 제목이 페이지마다 다르다는 사실은 이렇게 프레임 컴포넌트를 통해 흘려보낸다(§4-2·§4-3·§4-4 참고).
1248
+
1249
+ **페이지 헤더(`header`) 사용 규칙 — 이 앱에서는 값이 아니라 값의 일관성이 규칙이다.**
1250
+
1251
+ - **`fix` 는 앱 전체에서 하나로 고정한다.** 어떤 페이지는 `fix: true`(바), 다른 페이지는 `fix: false`(투명) 로 섞어 쓰지 않는다. 프로젝트에서 하나를 고르면(예: 전부 `fix: true`) 모든 `header` 가 그 값을 쓴다.
1252
+ - **`slot`·`onBack` 도 페이지 성격이 실제로 다른 경우가 아니면 있는 대로 통일한다.** "목록 페이지엔 없고 상세·등록 페이지엔 있다"처럼 화면 종류에 따라 갈리는 것은 허용되지만, 같은 종류의 화면끼리는 임의로 넣었다 뺐다 하지 않는다.
1253
+ - **`slot` 은 `ReactNode` 를 그대로 받지만, 원칙은 `size="sm"` 버튼 위주로만 채운다.** `STextLink`·복잡한 커스텀 마크업을 슬롯에 넣지 않는다 — 그 이상이 필요하면 페이지 헤더가 아니라 §4-2 의 `STableBar` 처럼 본문 쪽 액션 자리를 쓴다.
1254
+
1107
1255
  **GNB 폭을 사용자가 조절하게 하려면 `SGnb` 에 `resizable` 을 준다.** 메뉴 오른쪽 경계가 조절선이 되고, 레일 폭은 고정된 채 메뉴 컬럼만 늘고 준다. 범위는 컴포넌트가 정하므로 숫자를 직접 주지 않는다.
1108
1256
 
1109
1257
  ```tsx
@@ -1130,7 +1278,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1130
1278
 
1131
1279
  **최소 너비는 `SLayout` 이 보장한다 — 앱이 `min-w-*` 를 직접 주지 않는다.** 창이 최소 너비(`SLAYOUT_MIN_WIDTH`, GNB 포함한 전체 기준)보다 좁아지면 GNB·상단바는 제자리에 남고 **`SPage` 안에만 가로 스크롤이 생긴다.** 문서(브라우저 창)에는 가로 스크롤이 생기지 않는다. 그래서 셸을 감싸는 요소에 `min-width` 나 `overflow-x` 를 걸지 않는다 — 걸면 창 전체가 스크롤되어 GNB 가 화면 밖으로 밀린다.
1132
1280
 
1133
- **셸의 `SPage` 는 모든 페이지가 공유하므로, 스크롤 끝 여백을 끄려면 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `scrollEndSpacing` 을 받아 그대로 넘기고, 페이지네이션이 있는 목록 페이지만 `false` 를 준다 (§4-2). 나머지 페이지는 넘기지 않으면 기본값(켬)이 적용된다.
1281
+ **셸의 `SPage` 는 모든 페이지가 공유하므로, 페이지마다 달라지는 것은 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `scrollEndSpacing` · `contentHeight` 를 받아 그대로 넘기고, 페이지네이션이 있는 목록 페이지만 `contentHeight="fill"` 을 준다 (§4-2). 나머지 페이지는 넘기지 않으면 기본값(`auto`, 스크롤 끝 여백 켬)이 적용된다.
1134
1282
 
1135
1283
  **상단바 배치는 `header` 가 정한다.** 요소 순서가 달라지므로 슬롯을 채우기 전에 어느 쪽인지부터 정한다.
1136
1284
 
@@ -1152,7 +1300,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1152
1300
  topContent={
1153
1301
  /* 남는 폭 전체를 받는다 — 왼쪽은 그대로, 오른쪽 끝은 ml-auto */
1154
1302
  <div className="flex w-full items-center gap-sd-8">
1155
- <SInput value={keyword} onValueChange={setKeyword} placeholder="통합 검색" />
1303
+ <SSearchInput value={keyword} onValueChange={setKeyword} onSearch={runSearch} placeholder="통합 검색" />
1156
1304
  <SButton size="sm" color="neutral" outline label="내 계정" className="ml-auto" onClick={openAccount} />
1157
1305
  </div>
1158
1306
  }
@@ -1169,7 +1317,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1169
1317
  {/* 접히면 menuTop·menuFooter 가 함께 빠지므로, 폴드 레일에 남길 것만 foldedTop 으로 따로 준다 */}
1170
1318
  <SGnb
1171
1319
  items={MENU} value={current} onValueChange={navigate} useRail
1172
- menuTop={<SInput value={keyword} onValueChange={setKeyword} placeholder="메뉴 검색" />}
1320
+ menuTop={<SSearchInput value={keyword} onValueChange={setKeyword} placeholder="메뉴 검색" />}
1173
1321
  menuFooter={<AccountRow />}
1174
1322
  foldedTop={<SGhostButton icon="search" size="sm" ariaLabel="메뉴 검색" onClick={openSearch} />}
1175
1323
  />
@@ -1179,18 +1327,22 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1179
1327
 
1180
1328
  ### 4-2. 목록 페이지 (필터 + 테이블)
1181
1329
 
1182
- 구조: **페이지 헤더(제목 + 가이드 링크) → 필터(`SKeyValueTable`) → `STableBar` → `STable`**
1330
+ 구조: **페이지 헤더(`AppShell` 의 `header` prop, 가이드 등 부가 액션은 slot) → 필터(`SKeyValueTable`) → `STableBar` → `STable`**
1331
+
1332
+ 필터 자리는 `SChipFilter` 로 바꿔 놓을 수 있다 — 조건 후보가 많아 쓸 것만 골라 붙이게 하는 화면이면 그쪽이다 (§3-7-11). 나머지 골격은 같다.
1183
1333
 
1184
1334
  액션 버튼의 위치가 핵심이다:
1185
1335
 
1186
- - **페이지 제목 줄에는 액션 버튼을 두지 않는다.** 가이드·매뉴얼 링크 등 부가 정보만 온다.
1336
+ - **페이지 제목 줄에는 이 페이지의 주요 액션을 두지 않는다.** 부가적인 것만 `header.slot` 에 `SButton size="sm"` 으로 온다 (§4-1 "페이지 헤더 사용 규칙").
1187
1337
  - **주요 액션(등록 등)은 `STableBar` 의 `rightActions`** 에 둔다.
1188
1338
  - **선택 상태 액션(선택 삭제 등)은 `STableBar` 의 `actions`** 에 둔다. `actions` 슬롯은 건수 요약이 있으면 앞에 구분선(`SDivider`)을 **자동으로** 넣으므로 직접 구분선을 만들지 않는다.
1189
- - **페이지네이션이 있으면 스크롤 끝 여백을 끈다** — 셸의 `SPage` 에 `scrollEndSpacing={false}` 를 넘긴다 (§2-2). 페이지네이션이 이미 "여기서 끝"을 알려준다.
1339
+ - **페이지네이션이 있으면 본문이 남은 높이를 채우게 한다** — `AppShell` 에 `contentHeight="fill"` 을 넘긴다. 페이지가 통째로 스크롤되면 페이지네이션이 화면 밖으로 밀려 "여기서 끝"이 읽히지 않는다. `fill` 이면 **표만 자기 안에서 스크롤하고 페이지네이션은 하단에 고정**된다.
1340
+ - 본문 루트에 `h-full min-h-0` 으로 세로 축을 잇고, 남은 높이를 먹을 `STable` 에 `min-h-0 flex-1` 을 준다. 이 사슬이 하나라도 끊기면 표가 높이를 못 잡는다.
1341
+ - `fill` 에서는 페이지가 스크롤하지 않으므로 **`scrollEndSpacing` 은 무시된다** — 따로 끄지 않는다 (§2-2).
1190
1342
 
1191
1343
  ```tsx
1192
1344
  import {
1193
- SButton, STextLink, SKeyValueTable, STableBar, STable, STag,
1345
+ SButton, SKeyValueTable, STableBar, STable, STag,
1194
1346
  type STableColumn, type SRow, type SKeyValueField,
1195
1347
  } from 'sellmate-design-system-react';
1196
1348
 
@@ -1228,55 +1380,61 @@ export default function ProductListPage() {
1228
1380
  const [selected, setSelected] = useState<SRow[]>([]);
1229
1381
 
1230
1382
  return (
1231
- <div className="flex flex-col gap-sd-12">
1232
- {/* 페이지 헤더 — 액션 버튼 없음. 가이드/매뉴얼 링크 자리 */}
1233
- <div className="flex items-center justify-between">
1234
- <h1 className="typo-heading-lg m-0">상품 목록</h1>
1235
- <STextLink label="이용 가이드" rightArrow="chevron" onClick={openGuide} />
1236
- </div>
1383
+ <AppShell
1384
+ header={{
1385
+ title: '상품 목록',
1386
+ // 이 페이지의 주요 액션이 아니라 부가 액션 — slot 은 sm 버튼으로만 채운다
1387
+ slot: <SButton size="sm" color="neutral" outline label="이용 가이드" onClick={openGuide} />,
1388
+ }}
1389
+ contentHeight="fill" // 표가 남은 높이를 채우고 페이지네이션이 하단에 고정된다
1390
+ >
1391
+ {/* h-full min-h-0 → STable 의 min-h-0 flex-1 로 세로 축이 이어진다 */}
1392
+ <div className="flex h-full min-h-0 flex-col gap-sd-12">
1393
+ {/* 필터 — search 를 켜면 우측에 검색 패널이 붙는다 */}
1394
+ <SKeyValueTable
1395
+ fields={filterFields}
1396
+ values={filters}
1397
+ search
1398
+ onChange={({ values }) => setFilters(values)}
1399
+ onSearch={fetchList}
1400
+ />
1237
1401
 
1238
- {/* 필터 — search 를 켜면 우측에 검색 패널이 붙는다 */}
1239
- <SKeyValueTable
1240
- fields={filterFields}
1241
- values={filters}
1242
- search
1243
- onChange={({ values }) => setFilters(values)}
1244
- onSearch={fetchList}
1245
- />
1246
-
1247
- {/* 툴바 — 좌: 건수 + (구분선 자동) + 선택 액션 / 우: 주요 액션 */}
1248
- <STableBar
1249
- total={total}
1250
- selected={selected.length}
1251
- actions={
1252
- /* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
1253
- <SButton size="sm" color="danger" outline label="선택 삭제"
1254
- disabled={!selected.length} onClick={removeSelected} />
1255
- }
1256
- rightActions={
1257
- /* 이 페이지의 유일한 primary 채움 (§3-5-1) */
1258
- <SButton size="sm" label="상품 등록" onClick={goCreate} />
1259
- }
1260
- />
1261
-
1262
- <STable
1263
- columns={columns}
1264
- rows={rows}
1265
- rowKey="id"
1266
- selectable
1267
- selected={selected}
1268
- onSelectedChange={setSelected}
1269
- pagination={{ currentPage, lastPage }}
1270
- isLoading={isLoading}
1271
- />
1272
- </div>
1402
+ {/* 툴바 — 좌: 건수 + (구분선 자동) + 선택 액션 / 우: 주요 액션 */}
1403
+ <STableBar
1404
+ total={total}
1405
+ selected={selected.length}
1406
+ actions={
1407
+ /* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
1408
+ <SButton size="sm" color="danger" outline label="선택 삭제"
1409
+ disabled={!selected.length} onClick={removeSelected} />
1410
+ }
1411
+ rightActions={
1412
+ /* 이 페이지의 유일한 primary 채움 (§3-5-1) */
1413
+ <SButton size="sm" label="상품 등록" onClick={goCreate} />
1414
+ }
1415
+ />
1416
+
1417
+ {/* 남은 높이를 채우고 본문만 스크롤한다 — 페이지네이션 바는 표 안에서 하단 고정 */}
1418
+ <STable
1419
+ className="min-h-0 flex-1"
1420
+ columns={columns}
1421
+ rows={rows}
1422
+ rowKey="id"
1423
+ selectable
1424
+ selected={selected}
1425
+ onSelectedChange={setSelected}
1426
+ pagination={{ currentPage, lastPage }}
1427
+ isLoading={isLoading}
1428
+ />
1429
+ </div>
1430
+ </AppShell>
1273
1431
  );
1274
1432
  }
1275
1433
  ```
1276
1434
 
1277
1435
  ### 4-3. 폼 페이지 (등록/수정)
1278
1436
 
1279
- 구조: **페이지 제목 → `SForm` + `SKeyValueTable` → 하단 버튼**
1437
+ 구조: **페이지 제목(`AppShell` 의 `header` prop) → `SForm` + `SKeyValueTable` → 하단 버튼**
1280
1438
 
1281
1439
  - 필드를 `div` 로 나열하지 않고 **`SKeyValueTable` 의 행으로 구성**한다.
1282
1440
  - 검증 규칙은 각 field 의 `options.rules` 로 넘긴다. `SForm` 이 하위 컨트롤을 자동 수집해 submit 시 일괄 검증하고, 실패 시 첫 실패 필드로 포커스를 옮긴다.
@@ -1314,37 +1472,37 @@ export default function ProductCreatePage() {
1314
1472
  const [values, setValues] = useState<Record<string, unknown>>({});
1315
1473
 
1316
1474
  return (
1317
- <div className="flex flex-col gap-sd-12">
1318
- <h1 className="typo-heading-lg m-0">상품 등록</h1>
1319
-
1320
- <SForm ref={formRef} formClass="flex flex-col gap-sd-12" onSubmit={save}>
1321
- <SKeyValueTable
1322
- fields={fields}
1323
- values={values}
1324
- onChange={({ values }) => setValues(values)}
1325
- />
1326
-
1327
- {/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
1328
- <div className="flex items-center justify-between">
1329
- <SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
1330
- <div className="flex items-center gap-sd-8">
1331
- <SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
1332
- <SButton type="submit" label="저장" />
1475
+ <AppShell header={{ title: '상품 등록', onBack: goBack }}>
1476
+ <div className="flex flex-col gap-sd-12">
1477
+ <SForm ref={formRef} formClass="flex flex-col gap-sd-12" onSubmit={save}>
1478
+ <SKeyValueTable
1479
+ fields={fields}
1480
+ values={values}
1481
+ onChange={({ values }) => setValues(values)}
1482
+ />
1483
+
1484
+ {/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
1485
+ <div className="flex items-center justify-between">
1486
+ <SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
1487
+ <div className="flex items-center gap-sd-8">
1488
+ <SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
1489
+ <SButton type="submit" label="저장" />
1490
+ </div>
1333
1491
  </div>
1334
- </div>
1335
- </SForm>
1336
- </div>
1492
+ </SForm>
1493
+ </div>
1494
+ </AppShell>
1337
1495
  );
1338
1496
  }
1339
1497
  ```
1340
1498
 
1341
1499
  ### 4-4. 상세(조회) 페이지
1342
1500
 
1343
- 구조: **페이지 헤더(제목) → 섹션별 `SSectionHeaderCard` + `SKeyValueTable` → 하단 버튼**
1501
+ 구조: **페이지 헤더(`AppShell` 의 `header` prop, 목록에서 들어오는 뒤로가기는 onBack) → 섹션별 `SSectionHeaderCard` + `SKeyValueTable` → 하단 버튼**
1344
1502
 
1345
1503
  - 조회 값은 `type: 'text'` 행으로 표시한다. **상태·분류 태그도 별도 영역이 아니라 표의 한 행**으로 넣는다 (`render` 에 `STag`).
1346
1504
  - 행이 많아지면 **유형별로 섹션을 나누고, 각 섹션을 `SSectionHeaderCard` 로 감싼다.**
1347
- 합성 컴포넌트라 `SSectionHeaderCard.Header` / `SSectionHeaderCard.Body` 를 자식으로 쓴다.
1505
+ 섹션 제목은 `title` prop 으로, 바디 여백은 `padding` prop 으로 준다.
1348
1506
  - **수정·삭제 버튼은 하단에 둔다.** 내용이 짧아 우측 상단에 두는 변형도 있으나 기본은 하단이다.
1349
1507
 
1350
1508
  ```tsx
@@ -1374,38 +1532,33 @@ const priceFields: SKeyValueField[][] = [
1374
1532
 
1375
1533
  export default function ProductDetailPage() {
1376
1534
  return (
1377
- <div className="flex flex-col gap-sd-12">
1378
- <h1 className="typo-heading-lg m-0">클래식 셔츠</h1>
1379
-
1380
- <SSectionHeaderCard>
1381
- <SSectionHeaderCard.Header title="기본 정보" marker thickness="accent" />
1382
- <SSectionHeaderCard.Body>
1535
+ // 목록에서 들어온 상세 페이지 — onBack 으로 뒤로가기를 준다
1536
+ <AppShell header={{ fix: true, title: '클래식 셔츠', onBack: goList }}>
1537
+ <div className="flex flex-col gap-sd-12">
1538
+ <SSectionHeaderCard title="기본 정보" marker thickness="accent">
1383
1539
  <SKeyValueTable fields={basicFields} values={product} />
1384
- </SSectionHeaderCard.Body>
1385
- </SSectionHeaderCard>
1540
+ </SSectionHeaderCard>
1386
1541
 
1387
- <SSectionHeaderCard>
1388
1542
  {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1389
- <SSectionHeaderCard.Header
1543
+ <SSectionHeaderCard
1390
1544
  title="가격 정보"
1391
1545
  marker
1392
1546
  helpText={['부가세 포함 금액입니다.']}
1393
1547
  slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1394
- />
1395
- <SSectionHeaderCard.Body>
1548
+ >
1396
1549
  <SKeyValueTable fields={priceFields} values={product} />
1397
- </SSectionHeaderCard.Body>
1398
- </SSectionHeaderCard>
1399
-
1400
- {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
1401
- <div className="flex items-center justify-between">
1402
- <SButton color="neutral" outline label="목록" onClick={goList} />
1403
- <div className="flex items-center gap-sd-8">
1404
- <SButton color="danger" outline label="삭제" onClick={confirmDelete} />
1405
- <SButton label="수정" onClick={goEdit} />
1550
+ </SSectionHeaderCard>
1551
+
1552
+ {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
1553
+ <div className="flex items-center justify-between">
1554
+ <SButton color="neutral" outline label="목록" onClick={goList} />
1555
+ <div className="flex items-center gap-sd-8">
1556
+ <SButton color="danger" outline label="삭제" onClick={confirmDelete} />
1557
+ <SButton label="수정" onClick={goEdit} />
1558
+ </div>
1406
1559
  </div>
1407
1560
  </div>
1408
- </div>
1561
+ </AppShell>
1409
1562
  );
1410
1563
  }
1411
1564
  ```
@@ -1442,12 +1595,14 @@ export default function ProductDetailPage() {
1442
1595
  - [ ] 텍스트 회색 위계를 순차 적용했는가 (기본 → `text-fg-secondary` → `text-fg-tertiary`, 단계 건너뛰기 ❌)
1443
1596
  - [ ] `SPage`·`SPopup` 의 기본 패딩을 `p-sd-*` 로 덮어쓰지 않았는가, 블록·섹션 **간격**이 `gap-sd-12` 인가 (`gap-sd-16`/`gap-sd-24` ❌ — 24 는 안쪽 여백에만 열린다)
1444
1597
  - [ ] 섹션·패널의 안쪽 **여백**이 §2-2 판정과 맞는가 (덩어리 두 종류 → 16 / 세 종류 이상 → 24, 서면 16)
1445
- - [ ] `SSectionHeaderCard.Body` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
1446
- - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 를 넘겼는가
1598
+ - [ ] `SSectionHeaderCard` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
1599
+ - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `contentHeight="fill"` 을 넘기고 `h-full min-h-0` → `STable` 의 `min-h-0 flex-1` 사슬을 이었는가
1447
1600
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-sd-24` 등)
1448
1601
  - [ ] 페이지가 §4의 표준 골격에서 시작했는가
1602
+ - [ ] `header.fix` 가 프로젝트 전체와 같은 값인가 (다른 페이지와 다르게 섞어 쓰지 않았는가, §4-1)
1603
+ - [ ] `header.slot` 을 채웠다면 `SButton size="sm"` 위주인가 (§4-1 "페이지 헤더 사용 규칙")
1449
1604
  - [ ] 앱 셸이나 그 바깥에 `min-width`·`overflow-x` 를 직접 걸지 않았는가 (최소 너비는 `SLayout` 이 보장한다, §4-1)
1450
- - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가)
1605
+ - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
1451
1606
  - [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
1452
1607
  - [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
1453
1608
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
@@ -1528,7 +1683,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1528
1683
  - oceanblue: 15 25 50 60 65 70 75 80 85 90 95 99
1529
1684
  - brilliantblue: 10 20 25 40 50 60 70 75 80 85 90 95 99 05
1530
1685
  - bg: screen frame brand subtle accent accentLight accentBright accentFaint deep danger dangerLight warning caution progress success successLight neutralLight neutralBright tableHeader
1531
- - fg: primary secondary tertiary quaternary brand subtleWarm subtleCool deep success dangerLight accent accentLight danger warning inverse
1686
+ - fg: primary secondary tertiary quaternary brand subtleWarm subtleCool deep success successLight dangerLight accent accentLight danger warning inverse
1532
1687
  - border: default strong bold accent accentLight danger warning success
1533
1688
  - disabled: bg text icon border
1534
1689
  - link: accent
@@ -1654,6 +1809,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1654
1809
  --sys-color-fg-subtleCool
1655
1810
  --sys-color-fg-subtleWarm
1656
1811
  --sys-color-fg-success
1812
+ --sys-color-fg-successLight
1657
1813
  --sys-color-fg-tertiary
1658
1814
  --sys-color-fg-warning
1659
1815
  --sys-color-field-bg-default
@@ -2006,6 +2162,64 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2006
2162
 
2007
2163
  ---
2008
2164
 
2165
+ # SChipFilter
2166
+
2167
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
2168
+
2169
+ ### SChipFilter
2170
+
2171
+ #### Props
2172
+
2173
+ | Prop | Type | Default | Description |
2174
+ |------|------|---------|-------------|
2175
+ | `fields?` | `SChipFilterField[] \| SChipFilterGroup[]` | — | 필터 정의 목록. 그룹으로 묶으려면 SChipFilterGroup[]을 넘긴다 — 그룹이 시작될 때마다 앞에 구분선이 자동으로 붙는다(showLabel·인접 그룹·인라인 date 필터와 중첩되지 않도록 처리됨). |
2176
+ | `value?` | `SChipFilterValueMap` | — | 필터 값 맵 |
2177
+ | `activeKeys?` | `string[]` | — | 노출 필터 key 목록. 지정하면 제어 컴포넌트로 동작 — "필터 추가"로 고른 필드를 이 배열에 직접 넣어줘야 칩이 나타난다(onActiveKeysChange에서 받은 keys로 갱신). 지정하지 않으면 필드별 defaultActive 값을 기준으로 컴포넌트가 내부 상태로 관리하는 비제어 방식으로 동작한다 |
2178
+ | `label?` | `string` | `'검색 필터'` | 좌측 태그 텍스트 |
2179
+ | `showLabel?` | `boolean` | `false` | 좌측 태그(label)·구분선 표시 여부 |
2180
+ | `showReset?` | `boolean` | `true` | 검색 초기화 링크 표시 여부 |
2181
+ | `disabled?` | `boolean` | `false` | 바 비활성 상태 |
2182
+ | `className?` | `string` | — | |
2183
+ | `style?` | `CSSProperties` | — | |
2184
+
2185
+ #### Events
2186
+
2187
+ | Event | Type | Description |
2188
+ |-------|------|-------------|
2189
+ | `onValueChange` | `(value: SChipFilterValueMap) => void` | 전체 값 변경 — 편집 중인 값이 바뀔 때마다(선택할 때마다) 호출된다. 실제 검색 실행은 onSearch를 쓴다 |
2190
+ | `onFilterChange` | `(detail: SChipFilterChangeDetail) => void` | 개별 필터 값 변경 |
2191
+ | `onSearch` | `(value: SChipFilterValueMap) => void` | 실제 검색을 실행할 시점 — 편집 팝오버의 "검색" 버튼을 누르거나 팝오버가 닫힐 때(바깥 클릭·Esc·다른 칩으로 전환 포함) 그 시점의 전체 값 맵과 함께 호출된다. 팝오버가 없는 필드(인라인 date 프리셋·custom, clearable ×, 검색 초기화)는 값이 바뀌는 즉시 호출된다. dirty 체크가 기본 적용되어 있어 — 마지막으로 실제 검색이 실행된 값 맵과 비교해 하나라도 달라진 게 없으면(예: 팝오버를 열었다 아무것도 안 바꾸고 닫는 경우) 호출되지 않는다. fields를 그룹으로 넘겼다면 rule을 만족하지 못한 그룹이 있는 동안엔 onSearch가 호출되지 않는다 — 해당 그룹의 경고 툴팁은 이 시점과 무관하게 값이 비어 있는 동안 항상 실시간으로 떠 있다(별도로 validate()를 호출할 필요 없음) |
2192
+ | `onActiveKeysChange` | `(keys: string[]) => void` | 노출 필터 key 변경(칩 추가·제거) — 비제어 방식에서도 참고용으로 호출된다. activeKeys를 직접 제어할 때는 이 값을 그대로 activeKeys에 반영해야 한다 |
2193
+ | `onReset` | `() => void` | "검색 초기화" 클릭 — 모든 필드가 기본값(또는 null)으로 리셋된 뒤 호출된다 |
2194
+ | `onAddFilter` | `(key: string) => void` | "필터 추가" 목록에서 항목을 골랐을 때 — activeKeys를 직접 제어 중이면 이 콜백에서 (또는 onActiveKeysChange에서) key를 activeKeys에 추가해줘야 칩이 실제로 나타난다. activeKeys를 넘기지 않았다면(비제어) 별도 처리 없이도 컴포넌트가 알아서 칩을 노출한다 |
2195
+
2196
+ #### Methods (ref)
2197
+
2198
+ | Method | Type | Description |
2199
+ |--------|------|-------------|
2200
+ | `open` | `(key: string) => void` | 특정 필터 편집 팝오버를 엽니다. |
2201
+ | `reset` | `() => void` | 모든 필터 값을 초기화합니다. |
2202
+ | `validate` | `() => boolean` | fields를 그룹(SChipFilterGroup[])으로 넘겼을 때, 현재 값 기준으로 각 그룹의 rule을 만족하는지 반환합니다. 경고 툴팁은 이 호출과 무관하게 rule 위반 상태인 동안 항상 실시간으로 떠 있으므로, 이 메서드는 그 상태를 그대로 읽어오는 용도다(예: 검색 버튼을 눌러도 되는지 사전 확인) |
2203
+
2204
+ ## Dependencies
2205
+
2206
+ ### Depends on
2207
+
2208
+ - [SDatePicker](../SDatePicker)
2209
+ - [SDateRangePicker](../SDateRangePicker)
2210
+ - [SGhostButton](../SGhostButton)
2211
+ - [SIcon](../SIcon)
2212
+ - [SRadio](../SRadio)
2213
+ - [SRadioButton](../SRadioButton)
2214
+ - [SSelect](../SSelect)
2215
+ - [STag](../STag)
2216
+ - [STextLink](../STextLink)
2217
+ - [STooltip](../STooltip)
2218
+
2219
+ ### Graph
2220
+
2221
+ ---
2222
+
2009
2223
  # SChipInput
2010
2224
 
2011
2225
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
@@ -2089,6 +2303,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2089
2303
  | `value?` | `number` | `0` | 진행률 (0–100) |
2090
2304
  | `type?` | `SCircleProgressType` | `'primary'` | 색상 테마 |
2091
2305
  | `indeterminate?` | `boolean` | `false` | 불확정(스피너) 모드 — value 무시 |
2306
+ | `size?` | `number \| string` | `DEFAULT_SIZE` | 링의 지름. 숫자는 px 로 해석한다. 부모 크기에 비례시키려면 컨테이너 쿼리 단위(`'45cqh'`)를 쓴다 — 퍼센트는 부모가 inline-flex 라 기준 폭이 정해지지 않아 해석되지 않는다. |
2092
2307
  | `label?` | `string` | — | 하단 레이블 |
2093
2308
  | `innerValue?` | `boolean` | `false` | true면 퍼센트를 원 아래가 아닌 원 가운데에 표시 |
2094
2309
  | `className?` | `string` | — | |
@@ -2098,6 +2313,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2098
2313
 
2099
2314
  ### Used by
2100
2315
 
2316
+ - [SImage](../SImage)
2101
2317
  - [SLoadingContainer](../SLoadingContainer)
2102
2318
  - [SLoadingModal](../SLoadingModal)
2103
2319
  - [STable](../STable)
@@ -2165,15 +2381,35 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2165
2381
 
2166
2382
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
2167
2383
 
2384
+ ### SDatePickerMonthListbox
2385
+
2386
+ #### Props
2387
+
2388
+ | Prop | Type | Default | Description |
2389
+ |------|------|---------|-------------|
2390
+ | `value?` | `string \| null` | — | 선택 월 (YYYY-MM) |
2391
+ | `anchorYear?` | `number` | — | 연도 리스트가 처음 열릴 때 중앙에 둘 연도. 기본값은 올해 |
2392
+ | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
2393
+ | `className?` | `string` | — | |
2394
+ | `style?` | `CSSProperties` | — | |
2395
+
2396
+ #### Events
2397
+
2398
+ | Event | Type | Description |
2399
+ |-------|------|-------------|
2400
+ | `onValueChange` | `(value: string) => void` | |
2401
+ | `onMonthSelect` | `(value: string) => void` | 월 컬럼에서 월을 선택했을 때 호출 |
2402
+
2168
2403
  ### SDatePicker
2169
2404
 
2170
2405
  #### Props
2171
2406
 
2172
2407
  | Prop | Type | Default | Description |
2173
2408
  |------|------|---------|-------------|
2174
- | `value?` | `string \| null` | — | 선택 날짜 (YYYY-MM-DD) |
2409
+ | `value?` | `string \| null` | — | 선택 값 (date: YYYY-MM-DD, month: YYYY-MM, year: YYYY) |
2410
+ | `mode?` | `SDatePickerMode` | `'date'` | 선택 모드 |
2175
2411
  | `size?` | `SDatePickerSize` | `'sm'` | |
2176
- | `placeholder?` | `string` | `'YYYY-MM-DD'` | |
2412
+ | `placeholder?` | `string` | — | |
2177
2413
  | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
2178
2414
  | `disabled?` | `boolean` | `false` | |
2179
2415
  | `width?` | `number \| string` | — | |
@@ -2198,13 +2434,32 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2198
2434
 
2199
2435
  | Event | Type | Description |
2200
2436
  |-------|------|-------------|
2201
- | `onValueChange` | `(date: string) => void` | 선택 변경 (sdUpdate) |
2437
+ | `onValueChange` | `(value: string) => void` | 선택 변경 (sdUpdate) |
2202
2438
  | `onViewChange` | `(view: { year: number; month: number }) => void` | 보이는 연·월 변경 (sdViewChange) |
2203
2439
 
2440
+ ### SDatePickerYearListbox
2441
+
2442
+ #### Props
2443
+
2444
+ | Prop | Type | Default | Description |
2445
+ |------|------|---------|-------------|
2446
+ | `value?` | `string \| null` | — | 선택 연도 (YYYY) |
2447
+ | `anchorYear?` | `number` | — | 리스트가 처음 열릴 때 중앙에 둘 연도. 기본값은 올해 |
2448
+ | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
2449
+ | `className?` | `string` | — | |
2450
+ | `style?` | `CSSProperties` | — | |
2451
+
2452
+ #### Events
2453
+
2454
+ | Event | Type | Description |
2455
+ |-------|------|-------------|
2456
+ | `onValueChange` | `(year: string) => void` | |
2457
+
2204
2458
  ## Dependencies
2205
2459
 
2206
2460
  ### Used by
2207
2461
 
2462
+ - [SChipFilter](../SChipFilter)
2208
2463
  - [SKeyValueTable](../SKeyValueTable)
2209
2464
 
2210
2465
  ### Depends on
@@ -2259,10 +2514,30 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2259
2514
  | `onValueChange` | `(range: SDateRangeValue) => void` | 선택 변경 (sdUpdate) — [start, end] |
2260
2515
  | `onViewChange` | `(view: { year: number; month: number }) => void` | 보이는 연·월 변경 (sdViewChange) |
2261
2516
 
2517
+ ### SRangeCalendar
2518
+
2519
+ #### Props
2520
+
2521
+ | Prop | Type | Default | Description |
2522
+ |------|------|---------|-------------|
2523
+ | `value` | `SDateRangeValue` | — | |
2524
+ | `selectable?` | `[string, string]` | — | |
2525
+ | `maxRange?` | `number` | — | |
2526
+ | `useTimePicker?` | `boolean` | `false` | |
2527
+
2528
+ #### Events
2529
+
2530
+ | Event | Type | Description |
2531
+ |-------|------|-------------|
2532
+ | `onSelect` | `(r: [string, string]) => void` | |
2533
+ | `onPendingStartChange` | `(start: string \| null) => void` | 시작일만 선택된(종료일 대기) 상태를 상위로 전달 — 트리거에 `start ~` 프리뷰 표시용 |
2534
+ | `onViewChange` | `(view: { year: number; month: number }) => void` | |
2535
+
2262
2536
  ## Dependencies
2263
2537
 
2264
2538
  ### Used by
2265
2539
 
2540
+ - [SChipFilter](../SChipFilter)
2266
2541
  - [SKeyValueTable](../SKeyValueTable)
2267
2542
 
2268
2543
  ### Depends on
@@ -2578,6 +2853,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2578
2853
  | `readOnly?` | `boolean` | `false` | 읽기 전용 (회색 배경) |
2579
2854
  | `width?` | `number \| string` | — | 컨트롤 너비 (숫자=px). 지정하면 필드가 부모 폭을 다 먹지 않고 (레이블 + width) 만큼만 차지한다. 다른 요소와 나란히 놓으려면 부모를 flex 로 두면 된다. |
2580
2855
  | `minWidth?` | `number \| string` | — | 컨트롤 최소 너비 (숫자=px). 하한선만 지정하며 필드는 계속 부모 폭을 채운다 |
2856
+ | `maxWidth?` | `number \| string` | — | 컨트롤 최대 너비 (숫자=px). 상한선만 지정하며 필드는 그 아래에서 부모 폭을 채운다. `width` 와 달리 hug 로 전환하지 않는다 — 부모가 좁으면 같이 좁아지고, 넓어도 여기서 멈춘다. 값 길이가 정해진 컨트롤(날짜·시간)이 `width="100%"` 를 받아 행 전체로 늘어나는 것을 막는 데 쓴다. **라벨은 상한에 들어가지 않는다** — 라벨은 컨트롤의 형제라 이 상한 밖이다. `addonLabel` 은 테두리 박스 안이라 상한을 나눠 먹으므로, `labelWidth`(= addon 폭)만큼 상한을 자동으로 늘린다. addon 이 있는데 `labelWidth` 가 없으면 폭을 알 수 없어 상한을 걸지 않는다 — 컨트롤이 잘리는 것보다 넓은 편이 낫다. |
2581
2857
  | `multiline?` | `boolean` | `false` | 멀티라인(textarea) — 컨트롤 높이를 고정하지 않고 min-height만 적용 |
2582
2858
  | `borderless?` | `boolean` | `false` | 테두리 박스 제거 (inline 컨트롤용) — border/배경/hover·focus 강조만 사라지고 label·hint·errorMessage 등 나머지 필드 구성은 그대로 동작한다. |
2583
2859
  | `children?` | `ReactNode` | — | 실제 컨트롤 (input/select 등) — 테두리 없이 렌더, 테두리는 SField가 제공 |
@@ -2608,6 +2884,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2608
2884
  - [SFilePicker](../SFilePicker)
2609
2885
  - [SInput](../SInput)
2610
2886
  - [SNumberInput](../SNumberInput)
2887
+ - [SSearchInput](../SSearchInput)
2611
2888
  - [SSelect](../SSelect)
2612
2889
  - [STextarea](../STextarea)
2613
2890
  - [STimePicker](../STimePicker)
@@ -2790,6 +3067,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2790
3067
  - [SBarcodeInput](../SBarcodeInput)
2791
3068
  - [SCalendar](../SCalendar)
2792
3069
  - [SChip](../SChip)
3070
+ - [SChipFilter](../SChipFilter)
2793
3071
  - [SDateRangePicker](../SDateRangePicker)
2794
3072
  - [SFilePicker](../SFilePicker)
2795
3073
  - [SGnb](../SGnb)
@@ -2797,7 +3075,9 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2797
3075
  - [SInput](../SInput)
2798
3076
  - [SModalContainer](../SModalContainer)
2799
3077
  - [SOverlayHeader](../SOverlayHeader)
3078
+ - [SPage](../SPage)
2800
3079
  - [SPopover](../SPopover)
3080
+ - [SSearchInput](../SSearchInput)
2801
3081
  - [SSelect](../SSelect)
2802
3082
  - [STimePicker](../STimePicker)
2803
3083
  - [STimeRangePicker](../STimeRangePicker)
@@ -2882,7 +3162,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2882
3162
  | `message?` | `SGuideMessage` | `''` | 팝업 메시지 (배열이면 리스트, 중첩 배열이면 depth 표현) |
2883
3163
  | `url?` | `string` | `''` | notion 타입 클릭 시 이동 URL |
2884
3164
  | `popupTitle?` | `string` | `''` | 팝업 제목 (없으면 type 기본값) |
2885
- | `popupWidth?` | `number` | — | 팝업 너비 (px, 기본 426) |
3165
+ | `popupWidth?` | `number` | — | 팝업 너비 (px, 기본 426). 창보다 넓게 줘도 창 폭까지만 벌어진다. |
2886
3166
  | `className?` | `string` | — | |
2887
3167
  | `style?` | `CSSProperties` | — | |
2888
3168
 
@@ -2922,6 +3202,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2922
3202
  - [SButton](../SButton)
2923
3203
  - [SCallout](../SCallout)
2924
3204
  - [SCheckbox](../SCheckbox)
3205
+ - [SChipFilter](../SChipFilter)
2925
3206
  - [SChipInput](../SChipInput)
2926
3207
  - [SConfirmModal](../SConfirmModal)
2927
3208
  - [SDatePicker](../SDatePicker)
@@ -2934,12 +3215,14 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2934
3215
  - [SGhostButton](../SGhostButton)
2935
3216
  - [SGnb](../SGnb)
2936
3217
  - [SGuide](../SGuide)
3218
+ - [SImage](../SImage)
2937
3219
  - [SKeyValueTable](../SKeyValueTable)
2938
3220
  - [SListItem](../SListItem)
2939
3221
  - [SLoadingModal](../SLoadingModal)
2940
3222
  - [SNumberInput](../SNumberInput)
2941
3223
  - [SPagination](../SPagination)
2942
3224
  - [SPopover](../SPopover)
3225
+ - [SSearchInput](../SSearchInput)
2943
3226
  - [SSectionHeaderCard](../SSectionHeaderCard)
2944
3227
  - [SSelect](../SSelect)
2945
3228
  - [SStepper](../SStepper)
@@ -2955,6 +3238,46 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2955
3238
 
2956
3239
  ---
2957
3240
 
3241
+ # SImage
3242
+
3243
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
3244
+
3245
+ ### SImage
3246
+
3247
+ #### Props
3248
+
3249
+ | Prop | Type | Default | Description |
3250
+ |------|------|---------|-------------|
3251
+ | `src?` | `string` | — | 이미지 경로. 없으면 빈 이미지 아이콘이 나온다 |
3252
+ | `alt?` | `string` | `''` | 대체 텍스트 |
3253
+ | `ratio?` | `number` | `1` | 가로세로 비율. 소비자가 높이를 직접 주면 그쪽이 이긴다 |
3254
+ | `fit?` | `SImageFit` | `'cover'` | 이미지가 상자를 채우는 방식 (object-fit) |
3255
+ | `position?` | `string` | `'50% 50%'` | 이미지 정렬 위치 (object-position) |
3256
+ | `loading?` | `'lazy' \| 'eager'` | `'lazy'` | 브라우저 로딩 방식 |
3257
+ | `loadingShowDelay?` | `number` | `0` | 스피너를 띄우기까지 기다리는 시간(ms). 이 시간 안에 로드되면 스피너를 아예 그리지 않는다 — 캐시된 이미지에서 스피너가 한 프레임 번쩍이는 것을 막는다. |
3258
+ | `noSpinner?` | `boolean` | `false` | 로딩 중에도 스피너를 그리지 않는다 |
3259
+ | `loadingSlot?` | `ReactNode` | — | 로딩 중 표시를 직접 지정한다 (기본: 스피너) |
3260
+ | `errorSlot?` | `ReactNode` | — | 실패·빈 상태 표시를 직접 지정한다 (기본: 빈 이미지 아이콘) |
3261
+ | `imgProps?` | `ImgHTMLAttributes<HTMLImageElement>` | — | 안쪽 `<img>` 로 넘길 네이티브 속성 (srcSet · sizes · crossOrigin · draggable 등) |
3262
+
3263
+ #### Events
3264
+
3265
+ | Event | Type | Description |
3266
+ |-------|------|-------------|
3267
+ | `onLoad` | `(src: string) => void` | 로드 완료. 로드된 src 를 넘긴다 |
3268
+ | `onError` | `(event: SyntheticEvent<HTMLImageElement>) => void` | 로드 실패 |
3269
+
3270
+ ## Dependencies
3271
+
3272
+ ### Depends on
3273
+
3274
+ - [SCircleProgress](../SCircleProgress)
3275
+ - [SIcon](../SIcon)
3276
+
3277
+ ### Graph
3278
+
3279
+ ---
3280
+
2958
3281
  # SInput
2959
3282
 
2960
3283
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
@@ -3030,6 +3353,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3030
3353
  | `values?` | `Record<string, unknown>` | `{}` | field name을 key로 하는 값 객체 (`{ [name]: value }`). 지정 시 해당 field의 값으로 사용되며, field별 `options.value`보다 우선합니다. `onChange`의 `detail.values`와 함께 controlled 패턴으로 사용합니다. |
3031
3354
  | `search?` | `boolean` | `false` | 우측 검색 패널 |
3032
3355
  | `radius?` | `'default' \| 'useTop' \| 'full'` | `'default'` | border-radius 제어 |
3356
+ | `bordered?` | `boolean` | `true` | 바깥 테두리. 기본은 `true`. `SSectionHeaderCard` 의 `padding="none"` 안에 넣어 카드 가장자리까지 채울 때 `false` 로 끈다 — 카드가 이미 바깥 테두리를 그리므로, 켜 두면 1px 두 개가 나란히 놓여 그 변만 2px 로 보인다. |
3033
3357
  | `className?` | `string` | — | |
3034
3358
  | `style?` | `CSSProperties` | — | |
3035
3359
 
@@ -3155,8 +3479,8 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3155
3479
  | `leading?` | `SListItemSlot` | — | 타이틀 앞에 표시할 아이콘/콘텐츠 |
3156
3480
  | `trailing?` | `SListItemSlot` | — | 타이틀 뒤에 표시할 태그/콘텐츠 |
3157
3481
  | `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
3158
- | `selected?` | `boolean` | `false` | 선택 상태 여부 |
3159
- | `clickable?` | `boolean` | `true` | 클릭 가능 상태 여부 |
3482
+ | `selected?` | `boolean` | `false` | 선택 상태 여부. `clickable` 이 false 면 무시된다 |
3483
+ | `clickable?` | `boolean` | `false` | 클릭 가능 상태 여부. hover·selected·chevron 등 인터랙션 표현이 여기에 딸린다 |
3160
3484
  | `interaction?` | `SListItemInteraction` | — | hover/selected 상태에서 적용할 인터랙션 preset |
3161
3485
  | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
3162
3486
  | `dense?` | `boolean` | `false` | 조밀한 높이 사용 여부 |
@@ -3634,6 +3958,24 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3634
3958
 
3635
3959
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
3636
3960
 
3961
+ ### SPageHeader
3962
+
3963
+ #### Props
3964
+
3965
+ | Prop | Type | Default | Description |
3966
+ |------|------|---------|-------------|
3967
+ | `title` | `string` | — | 타이틀 (필수) |
3968
+ | `subText?` | `string` | — | 서브 텍스트. fix=true 면 타이틀 오른쪽에, fix=false(기본) 면 타이틀 아래에 놓인다. |
3969
+ | `fix?` | `boolean` | `false` | true: 높이 56 고정 + 흰 배경 + 하단 구분선, 서브 텍스트가 타이틀과 같은 줄. false(기본): 최소 높이 66(상하 패딩 20/16 안에서 타이틀 한 줄 높이로 결정) + 투명 배경, 서브 텍스트가 타이틀 아래로 내려간다. |
3970
+ | `slot?` | `ReactNode` | — | 오른쪽 끝 커스텀 슬롯. 가이드 링크·액션 버튼 등 페이지마다 다른 것이 온다. |
3971
+ | `scrolled?` | `boolean` | `false` | `<SPage>` 가 스크롤 여부를 보고 자동으로 넣어준다 — 직접 주지 않는다. fix=false 일 때만 의미가 있다: 스크롤이 시작되면 그림자(floating)가 들어가 스크롤되는 콘텐츠 위에 뜬 것처럼 보인다. 배경은 그대로 페이지 배경을 따라간다 — 배경을 바꾸는 건 scrolled 가 아니라 fix 다. |
3972
+
3973
+ #### Events
3974
+
3975
+ | Event | Type | Description |
3976
+ |-------|------|-------------|
3977
+ | `onBack` | `() => void` | 뒤로가기 클릭 핸들러. 지정하면 왼쪽에 뒤로가기 버튼이 나타난다. |
3978
+
3637
3979
  ### SPage
3638
3980
 
3639
3981
  #### Props
@@ -3642,11 +3984,15 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3642
3984
  |------|------|---------|-------------|
3643
3985
  | `background?` | `SPageBackground` | `'frame'` | 페이지 배경 표면. frame=흰 콘텐츠 면, neutral=옅은 회색 면, screen=앱 바탕. 스크롤바 처리도 여기 묶여 있다 — 셋 다 구분선+트랙 배경이고, 트랙 색만 neutral 에서 흰색이 된다. |
3644
3986
  | `scrollEndSpacing?` | `boolean` | `true` | 스크롤 끝 여백. 마지막 항목이 창 하단에 붙어 "여기서 끝"이 안 읽히는 것을 막는다. 기본으로 켜져 있고, 페이지네이션처럼 끝을 알려주는 것이 이미 있으면 끈다(목록 페이지). |
3987
+ | `overlayScrollbar?` | `boolean` | `false` | true면 네이티브 스크롤바를 숨기고 SPage 위에 오버레이 스크롤바를 얹는다. 스크롤바가 레이아웃 폭을 차지하지 않아 내부 콘텐츠 폭이 줄어들지 않는다. |
3988
+ | `contentHeight?` | `SPageContentHeight` | `'auto'` | 본문 높이 모드. 기본은 `auto` — 콘텐츠가 흐르고 넘치면 페이지가 스크롤한다. `fill` 은 본문이 남은 높이를 정확히 채우고 페이지는 스크롤하지 않는다. 표가 자기 안에서 스크롤하고 페이지네이션이 하단에 고정되는 목록 화면용이다. 본문 루트에 `h-full` 이 먹으므로 `<div className="flex h-full min-h-0 flex-col">` + `<STable className="min-h-0 flex-1" />` 구성이 성립한다. `fill` 에서는 페이지가 스크롤하지 않으므로 `scrollEndSpacing` 이 무시된다. |
3989
+ | `header?` | `SPageHeaderProps` | — | 페이지 타이틀 영역(pageHeader 포팅). 주면 스크롤·페이지 패딩 밖으로 빼내 상단에 고정 배치한다. `children` 은 항상 본문이다 — header 유무와 무관하게 같은 자리에 같은 뜻으로 들어간다. |
3645
3990
 
3646
3991
  ## Dependencies
3647
3992
 
3648
3993
  ### Depends on
3649
3994
 
3995
+ - [SGhostButton](../SGhostButton)
3650
3996
  - [SLayout](../SLayout)
3651
3997
 
3652
3998
  ### Graph
@@ -3793,7 +4139,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3793
4139
  | `maxHeight?` | `number` | — | 콘텐츠 최대 높이 px. 넘치면 세로 스크롤. |
3794
4140
  | `autoClose?` | `boolean` | `false` | 콘텐츠 내부를 클릭하면 자동으로 닫는다. |
3795
4141
  | `persistent?` | `boolean` | `false` | 외부 클릭 · ESC 로 닫히지 않게 한다. |
3796
- | `viewportPadding?` | `number` | `0` | 뷰포트 경계 여백 px (충돌 회피 패딩). |
4142
+ | `viewportPadding?` | `number` | `0` | 뷰포트 경계 여백 px (충돌 회피 패딩). 콘텐츠 폭은 항상 `창 폭 - viewportPadding*2` 를 넘지 않게 잘린다. |
3797
4143
  | `children?` | `ReactNode` | — | 본문 내용 (콘텐츠 자유). |
3798
4144
  | `className?` | `string` | — | |
3799
4145
  | `style?` | `CSSProperties` | — | |
@@ -3868,6 +4214,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3868
4214
 
3869
4215
  ### Used by
3870
4216
 
4217
+ - [SChipFilter](../SChipFilter)
3871
4218
  - [SKeyValueTable](../SKeyValueTable)
3872
4219
 
3873
4220
  ### Graph
@@ -3898,6 +4245,14 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3898
4245
  |-------|------|-------------|
3899
4246
  | `onValueChange` | `(value: string \| number) => void` | 변경 (sdUpdate) |
3900
4247
 
4248
+ ## Dependencies
4249
+
4250
+ ### Used by
4251
+
4252
+ - [SChipFilter](../SChipFilter)
4253
+
4254
+ ### Graph
4255
+
3901
4256
  ---
3902
4257
 
3903
4258
  # SScrollArea
@@ -3918,33 +4273,52 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3918
4273
 
3919
4274
  ---
3920
4275
 
3921
- # SSectionHeaderCard
4276
+ # SSearchInput
3922
4277
 
3923
4278
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
3924
4279
 
3925
- ### SSectionHeaderCardBody
4280
+ ### SSearchInput
3926
4281
 
3927
4282
  #### Props
3928
4283
 
3929
4284
  | Prop | Type | Default | Description |
3930
4285
  |------|------|---------|-------------|
3931
- | `children?` | `SSectionHeaderCardBodyChildren` | — | 바디 슬롯. 특정 컴포넌트 타입으로 제한하지 않습니다. |
3932
- | `padding?` | `SSectionHeaderCardBodyPadding` | `'default'` | 안쪽 여백. 기본은 `default`. 성격이 다른 요소가 세 종류 이상 섞인 영역에만 `wide`, 표를 가장자리까지 채울 때만 `none`. |
4286
+ | `value?` | `string \| number` | — | 값 (제어) |
4287
+ | `size?` | `SSearchInputSize` | `'sm'` | 크기 |
4288
+ | `placeholder?` | `string` | `'결과 내 검색'` | 플레이스홀더 |
4289
+ | `clearable?` | `boolean` | `false` | 지우기 버튼 — 값이 있을 때만 나타난다 |
4290
+ | `disabled?` | `boolean` | `false` | 비활성 |
4291
+ | `width?` | `number \| string` | — | 컨트롤 너비 (숫자=px). 미지정 시 부모 폭을 채운다 |
4292
+ | `focused?` | `boolean` | — | 포커스 상태 (제어/반영) |
4293
+ | `hovered?` | `boolean` | — | 호버 상태 (제어/반영) |
4294
+ | `inputClass?` | `string` | — | 내부 input 요소 className |
4295
+ | `inputStyle?` | `CSSProperties` | — | 내부 input 요소 style |
4296
+ | `className?` | `string` | — | |
4297
+ | `style?` | `CSSProperties` | — | |
3933
4298
 
3934
- ### SSectionHeaderCardHeader
4299
+ #### Events
3935
4300
 
3936
- #### Props
4301
+ | Event | Type | Description |
4302
+ |-------|------|-------------|
4303
+ | `onValueChange` | `(value: string) => void` | 값 변경 — 문자열 전달 |
4304
+ | `onChange` | `InputHTMLAttributes<HTMLInputElement>['onChange']` | 네이티브 onChange (form-agnostic 연동용, RHF 등) |
4305
+ | `onSearch` | `(value: string) => void` | 검색 실행 — Enter 키에서 현재 값과 함께 호출된다. 한글 조합 중의 Enter(IME 확정)는 검색으로 치지 않는다. |
3937
4306
 
3938
- | Prop | Type | Default | Description |
3939
- |------|------|---------|-------------|
3940
- | `title` | `ReactNode` | — | 헤더 제목 |
3941
- | `titleSize?` | `SSectionHeaderCardTitleSize` | `'xs'` | 제목 크기 |
3942
- | `marker?` | `boolean` | `false` | 제목 앞 점 표시 여부 |
3943
- | `required?` | `boolean` | `false` | 제목 뒤 필수 별 표시 여부 |
3944
- | `helpText?` | `string[]` | — | 도움말 툴팁 메시지 |
3945
- | `subtitle?` | `ReactNode` | — | 부제 |
3946
- | `slot?` | `ReactNode` | — | 헤더 우측 슬롯 |
3947
- | `thickness?` | `SSectionHeaderCardThickness` | `false` | 상단 border 색상 타입. false면 표시하지 않습니다. |
4307
+ ## Dependencies
4308
+
4309
+ ### Depends on
4310
+
4311
+ - [SField](../SField)
4312
+ - [SGhostButton](../SGhostButton)
4313
+ - [SIcon](../SIcon)
4314
+
4315
+ ### Graph
4316
+
4317
+ ---
4318
+
4319
+ # SSectionHeaderCard
4320
+
4321
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
3948
4322
 
3949
4323
  ### SSectionHeaderCard
3950
4324
 
@@ -3952,7 +4326,18 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3952
4326
 
3953
4327
  | Prop | Type | Default | Description |
3954
4328
  |------|------|---------|-------------|
3955
- | `children?` | `SSectionHeaderCardChildren` | — | SSectionHeaderCard 슬롯. 특정 컴포넌트 타입으로 제한하지 않습니다. |
4329
+ | `title?` | `ReactNode` | — | 헤더 제목 |
4330
+ | `titleSize?` | `SSectionHeaderCardTitleSize` | — | 제목 크기 |
4331
+ | `marker?` | `boolean` | — | 제목 앞 점 표시 여부 |
4332
+ | `required?` | `boolean` | — | 제목 뒤 필수 별 표시 여부 |
4333
+ | `helpText?` | `string[]` | — | 도움말 툴팁 메시지 |
4334
+ | `subtitle?` | `ReactNode` | — | 부제 |
4335
+ | `slot?` | `ReactNode` | — | 헤더 우측 슬롯 |
4336
+ | `thickness?` | `SSectionHeaderCardThickness` | — | 상단 border 색상 타입. false면 표시하지 않습니다. |
4337
+ | `headerClassName?` | `string` | — | 헤더 영역 클래스 |
4338
+ | `padding?` | `SSectionHeaderCardBodyPadding` | — | 바디 안쪽 여백. 기본은 `default`. 성격이 다른 요소가 세 종류 이상 섞인 영역에만 `wide`, 표를 가장자리까지 채울 때만 `none`. |
4339
+ | `background?` | `SSectionHeaderCardBodyBackground` | — | 본문 배경. 기본은 `frame` — 카드가 깐 흰 면을 그대로 쓴다. `neutral` 은 바탕을 한 단계 눌러앉혀, 흰 면 덩어리(표·리스트)가 여럿일 때 그것들이 **"면 위에 놓인 객체"로 읽히게** 한다. 위계를 한 단계 더 주고 싶을 때 고르는 선택지이며, **기본값이 틀린 것은 아니다** — 표·리스트는 테두리·라운드·헤더 줄과 간격을 이미 갖고 있어 흰 바탕에서도 경계가 읽힌다. 깔아도 **효과가 없는** 자리가 있다 — 덩어리가 가장자리까지 차는 경우(`padding="none"` 으로 표를 채우면 바탕이 완전히 가려진다), 덩어리에 회색 면이 섞인 경우(그 덩어리가 바탕에 묻힌다), 맨 텍스트나 폼 컨트롤만 있는 본문(떠오를 흰 면이 없다). |
4340
+ | `children?` | `SSectionHeaderCardChildren` | — | 바디 콘텐츠. 특정 컴포넌트 타입으로 제한하지 않습니다. |
3956
4341
 
3957
4342
  ## Dependencies
3958
4343
 
@@ -3990,8 +4375,8 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3990
4375
  | `rules?` | `Rule[]` | — | 유효성 규칙 — 닫힐 때 자동 검증 |
3991
4376
  | `labelTooltipProps?` | `Partial<STooltipProps>` | — | 레이블 툴팁 상세 옵션 |
3992
4377
  | `dropdownHeight?` | `string` | `'260px'` | 드롭다운 최대 높이 |
3993
- | `dropdownWidth?` | `string` | — | 드롭다운 너비 (없으면 트리거 너비) |
3994
- | `maxDropdownWidth?` | `string` | `'640px'` | 드롭다운 최대 너비 |
4378
+ | `dropdownWidth?` | `string` | — | 드롭다운 너비 (없으면 트리거 너비). 창보다 넓게 줘도 창 폭까지만 벌어진다. |
4379
+ | `maxDropdownWidth?` | `string` | `'640px'` | 드롭다운 최대 너비. 창이 이보다 좁으면 창 폭이 상한이 된다. |
3995
4380
  | `label?` | `string` | — | |
3996
4381
  | `labelWidth?` | `number \| string` | — | |
3997
4382
  | `icon?` | `SIconName` | — | 레이블 영역 아이콘 |
@@ -4023,6 +4408,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4023
4408
 
4024
4409
  ### Used by
4025
4410
 
4411
+ - [SChipFilter](../SChipFilter)
4026
4412
  - [SKeyValueTable](../SKeyValueTable)
4027
4413
  - [STable](../STable)
4028
4414
 
@@ -4156,6 +4542,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4156
4542
  | `stickyHeader?` | `boolean` | — | |
4157
4543
  | `stickyColumn?` | `STableStickyColumn` | — | 고정할 좌/우 컬럼 수 |
4158
4544
  | `radius?` | `'default' \| 'useTop' \| 'full'` | `'default'` | border-radius 제어 |
4545
+ | `bordered?` | `boolean` | `true` | 바깥 테두리. 기본은 `true`. `SSectionHeaderCard` 의 `padding="none"` 안에 넣어 카드 가장자리까지 채울 때 `false` 로 끈다 — 카드가 이미 바깥 테두리를 그리므로, 켜 두면 1px 두 개가 나란히 놓여 그 변만 2px 로 보인다. 페이지네이션 바의 테두리와 1px 겹침(`-mt-px`)도 함께 꺼진다. 본문 테두리가 없으면 겹칠 대상이 없어, 그대로 두면 페이지네이션만 테두리를 갖고 1px 어긋난다. |
4159
4546
  | `noDataLabel?` | `string` | `'데이터가 없습니다.'` | |
4160
4547
  | `noDataSlot?` | `ReactNode` | — | 데이터가 없을 때 body 영역 전체를 대체하는 슬롯. 지정하면 `noDataLabel` 대신 이 콘텐츠가 헤더 아래 영역을 채우며, 버튼 등 인터랙션도 동작한다. |
4161
4548
  | `isLoading?` | `boolean` | `false` | |
@@ -4247,7 +4634,6 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4247
4634
  |------|------|---------|-------------|
4248
4635
  | `value` | `string` | — | 현재 선택된 탭 value |
4249
4636
  | `tabs` | `STabOption[]` | — | 탭 옵션 목록 |
4250
- | `size?` | `STabSize` | `'md'` | 탭 크기 (main 전용) |
4251
4637
  | `isSub?` | `boolean` | `false` | 서브 탭(밑줄형) 스타일 |
4252
4638
  | `vertical?` | `boolean` | `false` | 세로 배치 (sub 전용 — main 폴더형은 항상 가로) |
4253
4639
  | `className?` | `string` | — | |
@@ -4291,6 +4677,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4291
4677
 
4292
4678
  ### Used by
4293
4679
 
4680
+ - [SChipFilter](../SChipFilter)
4294
4681
  - [SConfirmModal](../SConfirmModal)
4295
4682
  - [SGhostButton](../SGhostButton)
4296
4683
  - [SGnb](../SGnb)
@@ -4335,6 +4722,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4335
4722
 
4336
4723
  ### Used by
4337
4724
 
4725
+ - [SChipFilter](../SChipFilter)
4338
4726
  - [SChipInput](../SChipInput)
4339
4727
  - [SPopover](../SPopover)
4340
4728
 
@@ -4597,6 +4985,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4597
4985
  | `value?` | `boolean` | `false` | 선택(on/off) 상태 |
4598
4986
  | `label?` | `string` | `''` | 라벨 |
4599
4987
  | `disabled?` | `boolean` | `false` | 비활성 |
4988
+ | `size?` | `SToggleSize` | `'sm'` | 크기 |
4600
4989
  | `className?` | `string` | — | |
4601
4990
  | `style?` | `CSSProperties` | — | |
4602
4991
 
@@ -4654,6 +5043,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4654
5043
 
4655
5044
  ### Used by
4656
5045
 
5046
+ - [SChipFilter](../SChipFilter)
4657
5047
  - [SField](../SField)
4658
5048
  - [SKeyValueTable](../SKeyValueTable)
4659
5049
  - [SSectionHeaderCard](../SSectionHeaderCard)
@@ -4688,6 +5078,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4688
5078
  | `border?` | `boolean` | `true` | 아이템 하단 border 표시 여부 |
4689
5079
  | `indeterminate?` | `boolean` | `false` | 일부 선택 상태 |
4690
5080
  | `selectable?` | `boolean` | `true` | 체크박스 표시 여부 |
5081
+ | `size?` | `STreeSize` | `'sm'` | 타이포그래피 크기 |
4691
5082
  | `leading?` | `ReactNode \| ((state: STreeItemRenderState) => ReactNode)` | — | 사용자 지정 leading 콘텐츠 |
4692
5083
  | `trailing?` | `ReactNode \| ((state: STreeItemRenderState) => ReactNode)` | — | 사용자 지정 trailing 콘텐츠 |
4693
5084
 
@@ -4710,6 +5101,8 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4710
5101
  | `expandedIds?` | `string[]` | — | 펼쳐진 노드 ID 목록 |
4711
5102
  | `defaultExpandedIds?` | `string[]` | — | 비제어 펼침 초기값 |
4712
5103
  | `selectable?` | `boolean` | `true` | 체크박스 표시 여부 |
5104
+ | `size?` | `STreeSize` | `'sm'` | 타이포그래피 크기 |
5105
+ | `useAll?` | `boolean` | `false` | 체크박스 사용 시 최상단 전체 선택 아이템 표시 여부 |
4713
5106
  | `guideline?` | `boolean` | `true` | depth 연결선 표시 여부 |
4714
5107
  | `border?` | `boolean` | `true` | 아이템 하단 border 표시 여부 |
4715
5108
  | `cascadeSelection?` | `boolean` | `true` | 부모 선택 시 하위 노드까지 함께 토글 |