sellmate-design-system-react 9.0.0-beta.2 → 9.0.0-beta.20

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 (130) hide show
  1. package/AGENTS.md +463 -89
  2. package/README.md +101 -0
  3. package/dist/components/SBadge/README.md +24 -0
  4. package/dist/components/SBadge/SBadge.d.ts +1 -1
  5. package/dist/components/SBarcodeInput/README.md +9 -1
  6. package/dist/components/SBarcodeInput/SBarcodeInput.d.ts +7 -1
  7. package/dist/components/SButton/README.md +36 -0
  8. package/dist/components/SCalendar/README.md +13 -0
  9. package/dist/components/SCallout/README.md +15 -0
  10. package/dist/components/SCard/SCard.d.ts +1 -1
  11. package/dist/components/SCheckbox/README.md +8 -0
  12. package/dist/components/SChipFilter/README.md +288 -5
  13. package/dist/components/SChipFilter/SChipFilter.d.ts +95 -40
  14. package/dist/components/SChipFilter/index.d.ts +1 -1
  15. package/dist/components/SChipInput/README.md +15 -1
  16. package/dist/components/SChipInput/SChipInput.d.ts +7 -1
  17. package/dist/components/SCircleProgress/README.md +8 -0
  18. package/dist/components/SConfirmModal/README.md +16 -0
  19. package/dist/components/SDatePicker/README.md +60 -4
  20. package/dist/components/SDatePicker/SDatePicker.d.ts +61 -5
  21. package/dist/components/SDatePicker/index.d.ts +1 -1
  22. package/dist/components/SDateRangePicker/README.md +17 -2
  23. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +16 -3
  24. package/dist/components/SDivider/README.md +4 -0
  25. package/dist/components/SDraggableItem/README.md +37 -0
  26. package/dist/components/SDraggableItem/SDraggableItem.d.ts +2 -0
  27. package/dist/components/SDraggableList/README.md +29 -0
  28. package/dist/components/SDraggableList/SDraggableList.d.ts +10 -0
  29. package/dist/components/SDraggableList/index.d.ts +1 -1
  30. package/dist/components/SDrawer/README.md +8 -0
  31. package/dist/components/SDropdownButton/README.md +19 -0
  32. package/dist/components/SEditor/EditorBody.d.ts +40 -0
  33. package/dist/components/SEditor/EditorToolbar.d.ts +87 -0
  34. package/dist/components/SEditor/README.md +230 -0
  35. package/dist/components/SEditor/SEditor.d.ts +124 -0
  36. package/dist/components/SEditor/editor-icons.d.ts +59 -0
  37. package/dist/components/SEditor/editor.config.d.ts +85 -0
  38. package/dist/components/SEditor/index.d.ts +2 -0
  39. package/dist/components/SEditor/tiptap-api.d.ts +29 -0
  40. package/dist/components/SEditor/use-is-mobile.d.ts +14 -0
  41. package/dist/components/SExpansionItem/README.md +36 -0
  42. package/dist/components/SField/README.md +27 -2
  43. package/dist/components/SField/SField.d.ts +22 -4
  44. package/dist/components/SFilePicker/README.md +15 -1
  45. package/dist/components/SFilePicker/SFilePicker.d.ts +7 -1
  46. package/dist/components/SFooter/README.md +21 -0
  47. package/dist/components/SForm/README.md +11 -0
  48. package/dist/components/SGhostButton/README.md +20 -2
  49. package/dist/components/SGnb/README.md +45 -0
  50. package/dist/components/SGnb/gnb.config.d.ts +7 -0
  51. package/dist/components/SGuide/README.md +15 -0
  52. package/dist/components/SIcon/README.md +4 -0
  53. package/dist/components/SIcon/SIcon.d.ts +1 -1
  54. package/dist/components/SIcon/icons.gen.d.ts +2 -0
  55. package/dist/components/SImage/README.md +14 -0
  56. package/dist/components/SInput/README.md +3 -1
  57. package/dist/components/SInput/SInput.d.ts +7 -1
  58. package/dist/components/SKeyValueTable/README.md +87 -0
  59. package/dist/components/SKeyValueTable/SKeyValueTable.d.ts +14 -3
  60. package/dist/components/SLayout/README.md +16 -0
  61. package/dist/components/SLinearProgress/README.md +8 -0
  62. package/dist/components/SList/README.md +5 -1
  63. package/dist/components/SList/SList.d.ts +0 -2
  64. package/dist/components/SListItem/README.md +41 -0
  65. package/dist/components/SLoadingModal/README.md +8 -0
  66. package/dist/components/SNumberInput/README.md +9 -1
  67. package/dist/components/SNumberInput/SNumberInput.d.ts +7 -1
  68. package/dist/components/SPage/README.md +41 -1
  69. package/dist/components/SPage/SPage.d.ts +24 -2
  70. package/dist/components/SPage/index.d.ts +1 -1
  71. package/dist/components/SPage/page.config.d.ts +8 -0
  72. package/dist/components/SPopover/README.md +15 -0
  73. package/dist/components/SPopup/README.md +19 -0
  74. package/dist/components/SPortal/README.md +14 -0
  75. package/dist/components/SRadio/README.md +20 -0
  76. package/dist/components/SRadioButton/README.md +18 -0
  77. package/dist/components/SScrollArea/README.md +14 -0
  78. package/dist/components/SSearchInput/README.md +61 -0
  79. package/dist/components/SSearchInput/SSearchInput.d.ts +52 -0
  80. package/dist/components/SSearchInput/index.d.ts +1 -0
  81. package/dist/components/SSectionHeaderCard/README.md +39 -20
  82. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.d.ts +21 -14
  83. package/dist/components/SSectionHeaderCard/index.d.ts +1 -1
  84. package/dist/components/SSelect/README.md +25 -3
  85. package/dist/components/SSelect/SSelect.d.ts +13 -1
  86. package/dist/components/SSplitter/README.md +15 -0
  87. package/dist/components/SStepper/README.md +26 -0
  88. package/dist/components/SSwitch/README.md +13 -0
  89. package/dist/components/STable/README.md +72 -1
  90. package/dist/components/STable/STable.d.ts +129 -11
  91. package/dist/components/STable/index.d.ts +1 -1
  92. package/dist/components/STabs/README.md +12 -1
  93. package/dist/components/STabs/STabs.d.ts +2 -4
  94. package/dist/components/STabs/index.d.ts +1 -1
  95. package/dist/components/STabs/tabs.config.d.ts +3 -4
  96. package/dist/components/STag/README.md +47 -0
  97. package/dist/components/STextLink/README.md +17 -0
  98. package/dist/components/STextLink/STextLink.d.ts +2 -0
  99. package/dist/components/STextarea/README.md +1 -1
  100. package/dist/components/STextarea/STextarea.d.ts +7 -1
  101. package/dist/components/STimePicker/README.md +15 -1
  102. package/dist/components/STimePicker/STimePicker.d.ts +7 -1
  103. package/dist/components/STimePicker/timepicker.config.d.ts +7 -0
  104. package/dist/components/STimeRangePicker/README.md +27 -1
  105. package/dist/components/STimeRangePicker/STimeRangePicker.d.ts +7 -1
  106. package/dist/components/SToast/README.md +24 -0
  107. package/dist/components/SToggle/README.md +8 -0
  108. package/dist/components/STooltip/README.md +22 -0
  109. package/dist/components/STree/README.md +41 -0
  110. package/dist/components/STree/STree.d.ts +8 -0
  111. package/dist/components/STree/index.d.ts +1 -1
  112. package/dist/index.cjs +3987 -496
  113. package/dist/index.cjs.map +1 -1
  114. package/dist/index.d.ts +3 -0
  115. package/dist/index.js +3970 -496
  116. package/dist/index.js.map +1 -1
  117. package/dist/lib/field-width.d.ts +31 -0
  118. package/dist/lib/story-docs.d.ts +19 -3
  119. package/dist/lib/truncated-value-tooltip.d.ts +18 -0
  120. package/dist/llms-full.txt +2312 -198
  121. package/dist/llms.txt +465 -91
  122. package/dist/styles.css +672 -41
  123. package/dist/theme.css +22 -6
  124. package/eslint/index.mjs +10 -0
  125. package/eslint/lib/table-column.mjs +26 -0
  126. package/eslint/rules/field-width-grade.d.mts +41 -0
  127. package/eslint/rules/field-width-grade.mjs +311 -0
  128. package/eslint/rules/table-column-width.mjs +110 -0
  129. package/eslint/scale.gen.mjs +3 -0
  130. package/package.json +13 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  # sellmate-design-system-react — AI 에이전트 참조 문서
4
4
 
5
- 사용 규칙(§1) · 토큰 어휘(§2) · 전체 컴포넌트 Props(§3) 를 한 파일에 담은 판본이다.
5
+ 사용 규칙(§1) · 토큰 어휘(§2) · 전체 컴포넌트 Props·Types(§3) 를 한 파일에 담은 판본이다.
6
6
 
7
7
  ## 1. 사용 규칙 (AGENTS.md 전문)
8
8
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  > **대상**: 이 패키지로 화면을 만드는 소비 앱의 개발자와 AI 코딩 에이전트(Claude 등).
12
12
  > 이 문서는 "무엇을 언제 쓰고, 무엇을 쓰면 안 되는지"의 단일 기준이다.
13
- > 개별 컴포넌트의 상세 Props/Events는 `node_modules/sellmate-design-system-react/dist/components/<이름>/README.md` 를 참조한다.
13
+ > 개별 컴포넌트의 상세 Props/Events와 그 Props 가 쓰는 타입 정의(Types)는 `node_modules/sellmate-design-system-react/dist/components/<이름>/README.md` 를 참조한다.
14
14
 
15
15
  ## 0. 최우선 원칙 — 디자인 시스템 컴포넌트가 먼저다
16
16
 
@@ -28,15 +28,15 @@
28
28
 
29
29
  ### 0-1. 전체 컴포넌트 인덱스
30
30
 
31
- 무엇을 만들지 정했으면 **이 표에서 먼저 찾는다.** 상세 Props 는 `dist/components/<이름>/README.md` 참조.
31
+ 무엇을 만들지 정했으면 **이 표에서 먼저 찾는다.** 상세 Props·Types 는 `dist/components/<이름>/README.md` 참조.
32
32
 
33
33
  **이 표에서 어느 것을 골라야 할지 모르겠으면 §3-0 "의도 → 컴포넌트 라우팅" 으로 간다.** 하려는 일을 문장으로 찾으면 답이 하나 나온다 — 여기 인덱스는 "무엇이 있는지", §3-0 은 "언제 그걸 쓰는지" 를 담당한다.
34
34
 
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` |
38
+ | **입력 (폼)** | `SForm` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
39
+ | **날짜·시간** | `SCalendar` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
40
40
  | **표·목록** | `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
41
41
  | **레이아웃** | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
42
42
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
@@ -68,19 +68,21 @@ 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` |
74
75
  | `<input type="file">` | `SFilePicker` |
75
76
  | `<select>` | `SSelect` |
76
77
  | `<textarea>` | `STextarea` |
78
+ | `contenteditable`, 직접 붙인 에디터 라이브러리 | `SEditor` |
77
79
  | `<table>` | `STable`, `SKeyValueTable` |
78
80
  | `<form>` | `SForm` |
79
81
  | `<dialog>`, 직접 만든 오버레이 | `SModal.confirm(...)`, `SModal.create(...)`, `SPopup` |
80
82
  | `alert()`, `confirm()` | `SToast`, `SModal.confirm(...)` |
81
83
  | 직접 만든 탭/페이지네이션/스텝퍼 | `STabs`, `SPagination`, `SStepper` |
82
84
  | `<ul>`/`<li>` 로 만든 목록 UI | `SList` + `SListItem` (드래그 정렬은 `SDraggableItem`) |
83
- | 직접 만든 섹션 카드(제목 바 + 본문 박스) | `SSectionHeaderCard` + `.Header` / `.Body` |
85
+ | 직접 만든 섹션 카드(제목 바 + 본문 박스) | `SSectionHeaderCard` 의 `title` / `padding` props |
84
86
  | `<svg>` 직접 삽입, 이모지 아이콘 | `SIcon` |
85
87
  | `<hr>` | `SDivider` |
86
88
  | `<details>` / `<summary>` | `SExpansionItem` |
@@ -117,7 +119,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
117
119
 
118
120
  `text-14 font-bold` 같은 조합을 즉흥으로 만들지 않는다. §2-1의 `typo-*` 프리셋 클래스를 쓴다.
119
121
 
120
- ### 1-4. 숫자는 무조건 `toLocaleString()`
122
+ ### 1-4. 숫자·날짜 표기
121
123
 
122
124
  **숫자를 화면에 표시할 때는 예외 없이 `toLocaleString()` 을 거쳐 세 자리마다 콤마를 넣는다.**
123
125
  금액·수량·건수·재고 무엇이든, 테이블·상세·요약 문구 어디에 놓이든 같다.
@@ -132,6 +134,18 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
132
134
 
133
135
  **번호·코드는 제외한다.** 전화번호·사업자번호·송장번호·상품코드처럼 대상을 가리키는 값은 크기를 비교하는 숫자가 아니라 **서식이 정해진 문자열**이다. 여기에 콤마를 넣으면 송장번호 `123456789` 가 `123,456,789` 로 보여 값 자체가 달라진다.
134
136
 
137
+ **날짜는 `YYYY-MM-DD` 로 쓴다.** 자릿수를 채우고 하이픈으로 구분한다 — `2026-08-13`.
138
+ `2026. 8. 13.` 처럼 점으로 구분하거나 한 자리로 줄이지 않는다. 자릿수가 고정돼야 세로줄이 맞고,
139
+ 컬럼 폭을 형식으로 계산할 수 있다(§3-4). 일시가 필요하면 `YYYY-MM-DD HH:mm`.
140
+
141
+ ```tsx
142
+ ❌ {new Date(v).toLocaleDateString()} ❌ {`${y}. ${m}. ${d}.`}
143
+ ✅ {v} // 서버가 이미 YYYY-MM-DD 로 준 값
144
+ ✅ format: (v: string) => v.slice(0, 10)
145
+ ```
146
+
147
+ **`toLocaleDateString()` 은 쓰지 않는다** — 로케일에 따라 결과가 바뀌어 표기를 지킬 수 없다.
148
+
135
149
  ---
136
150
 
137
151
  ## 2. 조합 규칙 — 화면을 어떻게 쌓는가
@@ -146,7 +160,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
146
160
  | --- | --- | --- |
147
161
  | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) |
148
162
  | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SChipFilter` `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` `SImage` `SLinearProgress` `SCircleProgress` |
163
+ | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SImage` `SLinearProgress` `SCircleProgress` |
150
164
  | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
151
165
  | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
152
166
 
@@ -166,7 +180,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
166
180
  | 담는 것 | 올 수 있는 것 | 오면 안 되는 것 |
167
181
  | --- | --- | --- |
168
182
  | `SPage` | **블록만** | **요소를 직접** — 버튼 하나도 블록에 담아 놓는다 |
169
- | `SSectionHeaderCard.Body` | 그리는 블록 · 요소 | `SSectionHeaderCard` · `SCard` (카드 겹침, §3-7-8) |
183
+ | `SSectionHeaderCard` 의 `children` | 그리는 블록 · 요소 | `SSectionHeaderCard` · `SCard` (카드 겹침, §3-7-8) |
170
184
  | `SCard` | 그리는 블록 · 요소 | `SCard` · `SSectionHeaderCard` |
171
185
  | `SForm` | 블록 (보통 `SKeyValueTable` + 하단 액션) | — |
172
186
  | `SSplitter.Before` / `.After` | 블록 | — |
@@ -230,7 +244,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
230
244
 
231
245
  페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
232
246
 
233
- - **페이지·섹션 제목의 타이포를 직접 주지 않는다.** `SPage` 의 `header.title`, `SSectionHeaderCard.Header` 의 `title` 이 이미 넣는다 — 그 위에 `typo-heading-lg`/`typo-heading-sm` 을 또 씌우지 않는다.
247
+ - **페이지·섹션 제목의 타이포를 직접 주지 않는다.** `SPage` 의 `header.title`, `SSectionHeaderCard` 의 `header.title` 이 이미 넣는다 — 그 위에 `typo-heading-lg`/`typo-heading-sm` 을 또 씌우지 않는다.
234
248
  - **하위 제목이 필요하면 먼저 섹션을 나눌 수 없는지 본다.** 한 섹션 안에서 제목이 두 단으로 갈린다는 것은 대개 섹션이 둘이라는 뜻이다 (§3-7-8).
235
249
  - 본문 안에서 한 단어를 강조할 때는 `typo-body-sm-medium` 을 쓴다. `typo-body-sm-bold` 는 제목 성격의 짧은 라벨에만 쓴다. <!-- TODO(디자인): 강조 굵기 기준 확정 -->
236
250
 
@@ -300,7 +314,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
300
314
 
301
315
  **페이지 프레임은 예외 없이 `SPage` 가 넣는다.** 아래 규칙은 그 안의 **섹션·패널 레벨에만** 적용된다.
302
316
 
303
- **컴포넌트가 자체 여백을 가지면 컴포넌트 기준이 우선한다.** `SKeyValueTable`·`STable` 처럼 자기 여백을 토큰으로 갖고 있는 컴포넌트에는 이 판정을 적용하지 않는다 — 손댈 것이 없다. 아래 판정이 필요한 자리는 **직접 만든 컨테이너**와 **`SSectionHeaderCard.Body`** 두 곳뿐이다.
317
+ **컴포넌트가 자체 여백을 가지면 컴포넌트 기준이 우선한다.** `SKeyValueTable`·`STable` 처럼 자기 여백을 토큰으로 갖고 있는 컴포넌트에는 이 판정을 적용하지 않는다 — 손댈 것이 없다. 아래 판정이 필요한 자리는 **직접 만든 컨테이너**와 **`SSectionHeaderCard` 의 `padding`** 두 곳뿐이다.
304
318
 
305
319
  판정은 **그 영역이 담고 있는 콘텐츠 덩어리의 종류 수**로 한다.
306
320
 
@@ -335,14 +349,67 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
335
349
 
336
350
  **중첩되면 안쪽 여백을 주지 않는다.** 24 영역 안에 또 여백을 주면 가장자리가 40 으로 벌어져 한 면적처럼 읽힌다. 안쪽 카드·목록이 **배경색이 다르거나 테두리가 있어** 경계가 스스로 보이는 경우에만 자기 여백을 유지한다.
337
351
 
338
- `SSectionHeaderCard.Body` 는 이 규칙을 **prop 으로 받는다** — 직접 `p-sd-*` 를 주지 않는다.
352
+ `SSectionHeaderCard` 는 이 규칙을 **`padding` prop 으로 받는다** — 직접 `p-sd-*` 를 주지 않는다.
353
+
354
+ ```tsx
355
+ <SSectionHeaderCard title="기본 정보">…</SSectionHeaderCard> {/* 기본 = 16 */}
356
+ <SSectionHeaderCard title="기본 정보" padding="wide">…</SSectionHeaderCard> {/* 3종류 이상 */}
357
+ <SSectionHeaderCard title="기본 정보" padding="none">…</SSectionHeaderCard> {/* 표를 가장자리까지 */}
358
+ ```
359
+
360
+ ##### 카드 가장자리까지 채우는 표는 자기 테두리를 끈다
361
+
362
+ 여기서 "표"는 **`STable` 과 `SKeyValueTable` 둘 다**다. 두 컴포넌트 모두 자기 바깥 테두리를 그리는데, 카드도 바깥 테두리를 그린다. `padding="none"` 으로 붙이면 **1px 두 개가 나란히 놓여 그 변만 2px** 로 보인다(카드의 다른 변은 1px 그대로라 굵기가 어긋난다).
363
+
364
+ ```tsx
365
+ <SSectionHeaderCard title="발주 내역" padding="none">
366
+ <SKeyValueTable fields={…} bordered={false} radius="useTop" />
367
+ <STable columns={…} rows={…} bordered={false} radius="useTop" />
368
+ </SSectionHeaderCard>
369
+ ```
370
+
371
+ - **`bordered={false}`** 로 표의 테두리를 끈다. 카드가 이미 그린다.
372
+ - **`radius="useTop"`** 으로 위쪽 모서리를 죽인다. 아래쪽 라운드는 카드가 처리한다.
373
+ - `STable` 은 **페이지네이션 바의 테두리와 `-mt-px` 겹침도 함께 꺼진다** — 본문 테두리가 없으면 겹칠 대상이 없어, 그대로 두면 페이지네이션만 테두리를 갖고 1px 어긋난다.
374
+
375
+ #### 본문 바탕 눌러앉히기 (선택)
376
+
377
+ `SSectionHeaderCard` 는 **`background="neutral"`** 로 본문 바탕을 한 단계 눌러앉힐 수 있다. 흰 면 덩어리(표·리스트)가 여럿일 때 그 덩어리들이 **"면 위에 놓인 객체"로 읽혀 묶음이 더 강하게 보인다.**
339
378
 
340
379
  ```tsx
341
- <SSectionHeaderCard.Body>…</SSectionHeaderCard.Body> {/* 기본 = 16 */}
342
- <SSectionHeaderCard.Body padding="wide">…</SSectionHeaderCard.Body> {/* 3종류 이상 */}
343
- <SSectionHeaderCard.Body padding="none">…</SSectionHeaderCard.Body> {/* 표를 가장자리까지 */}
380
+ <SSectionHeaderCard title="발주 상세" background="neutral">…흰 면 표 여럿…</SSectionHeaderCard>
344
381
  ```
345
382
 
383
+ **기본값(`frame`, 흰 면)이 틀린 것이 아니다.** 이 저장소의 표·리스트는 테두리·라운드·헤더 줄과 `gap-sd-12` 를 이미 갖고 있어 흰 바탕에서도 경계가 읽힌다. 위계를 한 단계 더 주고 싶을 때 고르는 수단이지, 덩어리가 둘 이상이면 반드시 깔아야 하는 규칙이 아니다.
384
+
385
+ 깔아도 **효과가 없는** 자리는 있다.
386
+
387
+ - **덩어리가 가장자리까지 차는 경우** — `padding="none"` 으로 표를 채우면 깐 바탕이 표에 완전히 가려 보이지 않는다.
388
+ - **덩어리에 회색 면이 섞인 경우** — 그 덩어리가 바탕과 같은 색이 되어 묻힌다.
389
+ - **맨 텍스트·폼 컨트롤만 있는 본문** — 떠오를 흰 면이 없다.
390
+
391
+ 바탕을 깐 경우, 표 사이 구분선(`SDivider`)은 대개 불필요해진다 — 색이 이미 경계를 만든다.
392
+
393
+ #### 페이지 높이 — 화면을 꽉 채우고, 스크롤은 각 영역 안에서
394
+
395
+ **대부분의 화면은 본문이 창을 꽉 채우고, 스크롤은 각 영역 안에서 일어난다.** 표는 자기 안에서 스크롤하고, 좌측 목록은 목록 안에서 스크롤하고, 페이지네이션·하단 액션은 자리에 고정된다. 이것이 표준이다 — 목록 페이지만의 예외가 아니다.
396
+
397
+ `SPage` 의 `contentHeight="fill"` 이 그 모드다. 프레임 컴포넌트에서 넘긴다(§4-1).
398
+
399
+ ```tsx
400
+ <SPage contentHeight="fill">
401
+ <div className="flex h-full min-h-0 flex-col gap-sd-12">
402
+ <STableBar … />
403
+ <STable className="min-h-0 flex-1" pagination={…} />
404
+ </div>
405
+ </SPage>
406
+ ```
407
+
408
+ - **`min-h-0 flex-1` 사슬이 페이지의 기본 골격이다.** `fill` 은 본문 래퍼에 `h-full` 을 주고, 거기서부터 스크롤될 자리까지 `min-h-0 flex-1` 이 이어져야 자식이 남은 높이를 잡는다.
409
+ - **사슬이 한 군데만 끊겨도 자식이 높이를 못 잡는데, 그 실패가 조용하다** — 화면은 그려지고 스크롤만 엉뚱한 데서 일어난다. 체크리스트(§5)로 확인한다.
410
+ - **페이지 스크롤은 예외다.** 블록의 높이가 정해져 있고 그 높이가 창보다 클 때만 페이지가 스크롤한다. 그때만 `contentHeight="auto"` 와 `scrollEndSpacing` 을 켠다.
411
+ - **`SScrollArea` 로 페이지 본문 전체를 감싸지 않는다.** 스크롤은 `SPage` 의 `<main>` 몫이고, 감싸면 스크롤바가 본문 패딩 안쪽으로 들어와 가장자리에서 뜬다. `SScrollArea` 는 페이지 안의 특정 영역에만 쓴다(§3-0 D).
412
+
346
413
  #### 스크롤 영역의 하단 여백
347
414
 
348
415
  스크롤을 끝까지 내렸을 때 마지막 항목이 화면 경계에 붙으면 **목록이 끝난 것인지 더 있는 것인지** 읽히지 않는다. 그래서 스크롤 영역은 **하단만** 넓게 둔다. 나머지 세 방향은 위 16 / 24 규칙 그대로다.
@@ -350,9 +417,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
350
417
  | 스크롤 종류 | 어떻게 |
351
418
  | --- | --- |
352
419
  | **패널 자체 스크롤** (좌측 목록, 중앙 대화 등) | 그 패널 안쪽 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` — `SPage` 와 같은 토큰이라 값이 바뀌어도 함께 따라간다 |
353
- | **페이지 단위 스크롤** | **`SPage` 가 넣는다. 직접 주지 않는다** |
420
+ | **페이지 단위 스크롤** | **`SPage` 의 `scrollEndSpacing` 으로 켠다. 직접 패딩을 주지 않는다** |
354
421
 
355
- `SPage` 는 기본으로 넣으므로 **아무것도 하지 않으면 맞다.** 끄는 경우는 하나뿐이다 — **페이지네이션이 붙은 테이블.** 페이지네이션이 이미 "여기서 끝"을 알려주므로 `scrollEndSpacing={false}` 로 끈다 (§4-2 목록 페이지).
422
+ **`scrollEndSpacing` 은 기본이 꺼져 있다.** 페이지가 실제로 스크롤될 때만 필요한 값이라, 조건 없이 붙이면 내용이 화면에 거의 딱 맞는 페이지까지 그 여백 때문에 스크롤되게 만든다. 페이지 스크롤을 쓰는 화면(`contentHeight="auto"` + 내용이 창보다 김)에서만 켠다. 페이지네이션처럼 끝을 알려주는 것이 이미 있으면 켜지 않는다.
356
423
 
357
424
  ### 2-3. 색상
358
425
 
@@ -414,6 +481,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
414
481
  | --- | --- | --- |
415
482
  | 한 줄 텍스트를 받는다 | `SInput` | §3-7-1 |
416
483
  | 여러 줄 텍스트를 받는다 | `STextarea` | §3-7-1 |
484
+ | 제목·굵게·목록·색 같은 **서식이 남아야 하는** 글을 받는다 | `SEditor` | §3-7-1 |
485
+ | 목록·결과를 검색어로 좁힌다 | `SSearchInput` | §3-7-1 |
417
486
  | 숫자(수량·금액)를 받는다 | `SNumberInput` | |
418
487
  | 바코드를 스캔해 받는다 | `SBarcodeInput` | |
419
488
  | 목록에서 하나 고르게 한다 | `SSelect` | §3-7-2 |
@@ -427,6 +496,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
427
496
  | 입력된 값 하나를 지우거나 고치게 한다 | `SChip` | §3-1 |
428
497
  | 파일을 받는다 | `SFilePicker` | |
429
498
  | 날짜 하나를 받는다 | `SDatePicker` | §3-7-4 |
499
+ | 연도 선택 리스트만 커스텀 조합에 넣는다 | `SDatePickerYearListbox` | §3-7-4 |
500
+ | 연도+월 선택 리스트만 커스텀 조합에 넣는다 | `SDatePickerMonthListbox` | §3-7-4 |
430
501
  | 날짜 기간을 받는다 | `SDateRangePicker` | §3-7-4 |
431
502
  | 시각 하나를 받는다 | `STimePicker` | |
432
503
  | 시각 범위를 받는다 | `STimeRangePicker` | |
@@ -475,6 +546,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
475
546
  | 사용자가 영역 크기를 조절하게 한다 | `SSplitter` | §3-6 |
476
547
  | 특정 영역 안에서만 스크롤시킨다 | `SScrollArea` | |
477
548
 
549
+ **`SScrollArea` 로 페이지 본문 전체를 감싸지 않는다.** 페이지 스크롤은 `SPage` 의 `<main>` 몫이다 — 감싸면 스크롤바가 본문 패딩 안쪽으로 들어와 페이지 가장자리에서 떨어져 그려진다 (§2-2).
550
+
478
551
  #### E. 다른 곳으로 이동시킨다
479
552
 
480
553
  | 하려는 일 | 컴포넌트 | 갈림 |
@@ -558,9 +631,26 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
558
631
  | | 무엇인가 | 크기 |
559
632
  | --- | --- | --- |
560
633
  | **SPopup** | **별도 브라우저 창** (`window.open` 으로 여는 전용 라우트) | 창 크기 = 콘텐츠 크기 |
561
- | **SActionModal** | 같은 창 위 오버레이 카드 | `width` / `height` prop |
634
+ | **SActionModal** | 같은 창 위 오버레이 카드 | `width` prop. **높이는 주지 않는다** (아래) |
562
635
  | **SModal.confirm** (`SConfirmModal`) | 같은 창 위 확인창 | 고정 |
563
636
 
637
+ ##### 모달 높이는 내용이 정하고, 상한은 시스템이 건다
638
+
639
+ **`height` 를 주지 않는다.** 높이는 내용이 정하고, 카드는 **뷰포트의 85%** 에서 멈춘다(시스템이 모든 모달에 건다). 데이터가 적으면 내용만큼 작아지고, 많으면 85% 에서 멈춘다.
640
+
641
+ 가로는 좌우 24px 씩을 뺀 값으로 클램핑하는데 **세로만 비율**인 이유는, 모달이 화면을 거의 다 덮으면 뒤 맥락이 사라져 "떠 있는 것"으로 읽히지 않기 때문이다.
642
+
643
+ **상한에 닿았을 때 스크롤되어야 하는 것은 모달 본문이 아니라 표다.**
644
+
645
+ ```tsx
646
+ <SActionModal modalTitle="발주 검토" button={{ label: '확정', onClick: submit }}>
647
+ {/* 표가 남은 높이를 먹고 자기 안에서 스크롤한다 — 헤더·합계·푸터는 늘 보인다 */}
648
+ <STable className="min-h-0 flex-1" columns={columns} rows={rows} />
649
+ </SActionModal>
650
+ ```
651
+
652
+ 본문(`overflow-auto` 영역)이 통째로 스크롤되면 표 헤더와 합계 줄이 위로 밀려 사라진다. `SActionModal` 의 본문은 이미 `min-h-0 flex-1` 이므로, 표에 `min-h-0 flex-1` 을 주면 세로 축이 이어져 표만 스크롤한다 (§4-2 목록 페이지와 같은 사슬이다).
653
+
564
654
  **성격이 먼저 둘로 갈린다.**
565
655
 
566
656
  | 성격 | 정의 | 컴포넌트 |
@@ -698,36 +788,51 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
698
788
 
699
789
  작성 중인 내용이 있을 때 닫기를 시도하면 이탈 안내를 띄우는 것은 **소비 앱 몫**이다. 디자인 시스템은 dirty 상태를 알지 못하므로 백드롭·ESC 를 일괄 차단할 뿐이다. 안내가 필요하면 앱이 자체 dirty 판정 후 `SModal.confirm` 으로 띄운다.
700
790
 
701
- ### 3-4. 테이블 컬럼 — 정렬과 너비
791
+ ### 3-4. 테이블 컬럼 — 정렬·너비·헤더
702
792
 
703
- #### 정렬
793
+ #### 정렬과 너비는 같은 표에서 정한다
704
794
 
705
- **값의 크기를 비교하는 숫자 컬럼은 예외 없이 오른쪽 정렬한다** (`align: 'right'`).
706
- 자릿수가 세로로 맞아야 값의 크기를 눈으로 비교할 수 있기 때문이다.
795
+ 컬럼을 정의할 때 정렬과 너비는 따로 판단하는 것이 아니다. 둘 다 **값의 성격**에서 나온다.
707
796
 
708
- **판별 기준은 "숫자인가"가 아니라 "크기를 비교하는가"다.** 자릿수 차이가 거의 없고 값끼리 대소를 견줄 일이 없으면 숫자로만 이루어져 있어도 우측 정렬하지 않는다.
797
+ 원칙 한 줄: **길이를 형식이 정하면 고정, 사용자가 정하면 가변.**
709
798
 
710
- | 값 성격 | 정렬 | 예 |
711
- | --- | --- | --- |
712
- | **금액·수량·개수·비율 등 양을 나타내는 값** | **`'right'`** | `39,000원` · `12개` · `3건` · `15%` |
713
- | 코드·식별자 (주문번호, 상품코드, 순번) | **`'center'`** | `RV20250728-000010` · `1024` |
714
- | 전화번호·사업자번호 | **`'center'`** | `010-1234-5678` |
715
- | 일자·일시 | **`'center'`** | `2024-10-23` |
716
- | 텍스트 | 생략(기본 `left`) | 상품명, 카테고리 |
717
- | 상태 태그·아이콘·체크박스 등 고정폭 요소 | `'center'` | `STag`, `SIcon` |
799
+ | 값 성격 | 정렬 | 너비 | 예 |
800
+ | --- | --- | --- | --- |
801
+ | **금액·수량·개수·비율** (양을 나타내는 값) | **`'right'`** | 고정 | `39,000원` · `12개` · `3건` · `15%` |
802
+ | 코드·식별자, 전화번호, 일자·일시 | **`'center'`** | 고정 | `RV20250728-000010` · `010-1234-5678` · `2026-08-13` |
803
+ | **닫힌 값 집합** (enum · 마스터 목록에서 고르는 값) | **`'center'`** | 고정 | 상태 · 직급 · 공개 범위 · 고용 형태 · 요일 |
804
+ | 상태 태그·아이콘·버튼·체크박스 | `'center'` | 고정 (`contentType: 'control'`) | `STag` · `SIcon` · `SGhostButton` |
805
+ | **텍스트** (사용자가 자유 입력) | 생략(기본 `left`) | 기준 폭 + `resizable` | 이름 · 목표명 · 이메일 · 메모 |
718
806
 
719
807
  **중앙 정렬은 `align: 'center'` 를 명시한다.** 기본값이 좌측이라 생략하면 중앙이 되지 않는다.
720
808
 
809
+ ##### 판별 — 값의 크기를 비교하는가
810
+
811
+ 우측 정렬의 근거는 "자릿수를 세로로 맞춰 크기를 읽는다"다. 자릿수 차이가 거의 없고 값끼리 대소를 견줄 일이 없으면 숫자로만 이루어져 있어도 우측 정렬하지 않는다 — 송장번호 `123456789` 는 크기를 비교하는 값이 아니다.
812
+
813
+ ##### 판별 — 값 집합이 닫혀 있는가
814
+
815
+ **닫힌 값 집합이면 태그로 그리든 맨 텍스트로 그리든 `center` 다.** 판별 질문 하나 — *사용자가 그 칸을 직접 치는 값인가?* 아니면 닫힌 집합이다.
816
+
817
+ | | 값의 출처 | 정렬 | 예 |
818
+ | --- | --- | --- | --- |
819
+ | **닫힘** | enum · 마스터 목록에서 선택 (`SSelect` 의 `options` 에서 오는 값) | `center` | 직급 · 상태 · 공개 범위 · 최종 등급 · 고용 형태 · 요일 |
820
+ | **열림** | 사용자가 자유 입력 (자유 입력 필드에서 오는 값) | `left` | 이름 · 목표명 · 이메일 · 문항 그룹명 |
821
+
822
+ - **무엇으로 그렸는지로 가르지 않는다.** 같은 성격의 값이 `STag` 면 `center`, 맨 텍스트면 `left` 가 되면 한 테이블 안에서 기준이 어긋난다.
823
+ - **길이로도 가르지 않는다.** "짧은 라벨이면 center" 같은 단서를 붙이면 `프로덕트디자인팀`(8자)처럼 경계에 걸리는 값에서 매번 판단이 갈린다.
824
+ - 한 열에 텍스트와 태그가 함께 오면 태그 기준(`center`)에 맞춘다.
825
+
721
826
  ```tsx
722
827
  const columns: STableColumn[] = [
723
- { name: 'orderNo', label: '주문번호', field: 'orderNo', width: '140px', align: 'center' },
724
- { name: 'orderedAt', label: '주문일자', field: 'orderedAt', width: '100px', align: 'center' },
725
- { name: 'name', label: '상품명', field: 'name' }, // 텍스트 → 생략
726
- { name: 'qty', label: '수량', field: 'qty', width: '80px', align: 'right',
828
+ { name: 'orderNo', label: '주문번호', field: 'orderNo', width: 140, align: 'center' },
829
+ { name: 'orderedAt', label: '주문일자', field: 'orderedAt', width: 100, align: 'center' },
830
+ { name: 'name', label: '상품명', field: 'name', width: 240 }, // 자유 입력 → 생략
831
+ { name: 'qty', label: '수량', field: 'qty', width: 80, align: 'right',
727
832
  format: (v: number) => `${Number(v).toLocaleString()}개` },
728
- { name: 'price', label: '판매가', field: 'price', width: '120px', align: 'right',
833
+ { name: 'price', label: '판매가', field: 'price', width: 120, align: 'right',
729
834
  format: (v: number) => `${Number(v).toLocaleString()}원` },
730
- { name: 'status', label: '상태', field: 'status', width: '100px', align: 'center',
835
+ { name: 'status', label: '상태', field: 'status', width: 100, align: 'center',
731
836
  render: () => <STag size="sm" color="green" label="판매중" /> },
732
837
  ];
733
838
  ```
@@ -735,38 +840,73 @@ const columns: STableColumn[] = [
735
840
  - `format` 으로 단위를 붙이더라도 **양을 나타내면 오른쪽 정렬**이다. 단위 때문에 문자열이 되는 것은 정렬 판단과 무관하다.
736
841
  - 양을 나타내는 숫자는 §1-4 대로 **`toLocaleString()` 이 필수**다. 세 자리 콤마 없이 출력하지 않는다.
737
842
  - **번호·코드에는 세 자리 콤마를 넣지 않는다.** 송장번호 `123456789` 를 `123,456,789` 로 표시하면 값 자체가 달라 보인다.
843
+ - 날짜는 §1-4 대로 `YYYY-MM-DD` 로 적는다. 자릿수가 고정이라 폭을 형식으로 계산할 수 있다.
738
844
  - **헤더는 가운데, 셀만 우측**으로 두려면 `align` 이 아니라 `tdClass` 를 쓴다. `align` 은 `<th>` 와 `<td>` 에 함께 적용된다.
739
845
 
740
846
  ```tsx
741
- { name: 'views', label: '조회수', field: 'views', align: 'center', tdClass: 'text-right!',
847
+ { name: 'views', label: '조회수', field: 'views', align: 'center', width: 100, tdClass: 'text-right!',
742
848
  format: (v: number) => Number(v).toLocaleString() },
743
849
  ```
744
850
 
745
851
  - `SKeyValueTable` 의 값 셀도 같은 기준을 따른다.
746
852
 
747
- #### 컨트롤이 들어가는 컬럼은 너비를 명시한다
853
+ #### 너비는 px 로만 준다
748
854
 
749
- 컬럼 폭은 `width` 로 **고정**되고, `<td>` 는 그 폭을 넘는 내용을 잘라낸다(`overflow: hidden`). 텍스트라면 말줄임으로 끝나지만, 셀에 `STag` · `SButton` · `SGhostButton` · `SSelect` · `SInput` · `SNumberInput` 처럼 **자기 폭을 가진 요소**를 넣으면 요소 자체가 잘려 **누르거나 읽거나 입력할 수 없게 된다.** `width` 를 생략해도 내용에 맞춰 늘어나지 않고 `STable` 의 기본 폭이 될 뿐이므로, 컨트롤이 들어가는 컬럼은 폭을 직접 판단해서 준다.
855
+ **컬럼 폭은 px 이다.** 숫자를 주면 px 로 읽고, 문자열은 `'120px'` 형태만 받는다. `%` · `clamp()` · `min()` 은 쓰지 않는다.
750
856
 
751
- - 기준은 **요소가 온전히 보이는 폭 + 셀 좌우 패딩**이다. 좌우 패딩은 `STable` 이 토큰으로 넣으므로(직접 주지 않는다) 그만큼을 뺀 나머지가 요소 몫이라는 점을 계산에 넣는다.
752
- - 요소가 둘 이상이면 요소 폭의 합에 **`gap` 까지** 더한다 (행 내부 인라인 액션 간격은 `gap-sd-4` 고정, §3-5-5).
857
+ 컬럼 폭은 `<colgroup>` 의 `<col width>` 로 들어가고 테이블이 `table-fixed` 라, 함수형 값은 계산되지 않고 통째로 무시된 뒤 auto 폭으로 떨어진다. `'30%'` 는 더 나쁘게 `30`(px)으로 읽힌다. **둘 다 에러 없이 화면만 틀어진다.**
858
+
859
+ - **내용이 들어가는 열은 전부 폭을 명시한다.** 생략하면 기본 120px 이 조용히 들어가고, "짧은 열이라 그대로 둔 것"과 "판단을 빠뜨린 것"이 구분되지 않는다. 120px 이 맞더라도 `width: 120` 을 적는다.
860
+ - **`autoWidth` 는 남은 폭을 흡수하는 스페이서 열 하나에만 쓴다.** 내용이 들어가는 열에는 쓰지 않는다 — 폭이 다른 열에 좌우돼 화면마다 달라진다. 스페이서 열은 값을 그리지 않으므로 `field` 도 생략한다.
861
+
862
+ ```tsx
863
+ { name: 'spacer', label: '', autoWidth: true },
864
+ ```
865
+
866
+ - **`minWidth` · `maxWidth` 는 `resizable` 손잡이의 이동 범위일 뿐, 레이아웃에는 관여하지 않는다.** 폭을 주지 않은 열이 이 값 안에서 잡히는 것이 아니다.
867
+ - **고정폭 합이 최소 창 폭을 넘으면 가로 스크롤이 된다.** `STable` 이 자기 안에서 가로로 스크롤하고 헤더·바디가 함께 움직이므로 별도 조치는 필요 없다 — 폭을 줄여 맞추지 말고, 열이 정말 그만큼 필요한지를 본다.
868
+
869
+ ##### 고정폭을 어떻게 정하는가
870
+
871
+ ```text
872
+ 폭 = ceil( ( max(값 폭 + 값 기준 패딩, 헤더 폭 + 32) + 여유 ) / 8 ) × 8
873
+ ```
874
+
875
+ - **값과 헤더를 따로 계산해 큰 쪽을 쓴다.** `contentType: 'control'` 은 `<td>` 에만 적용되고 `<th>` 는 항상 텍스트 패딩이라, 짧은 컨트롤 + 긴 헤더 조합에서 헤더가 잘린다.
753
876
  - 값에 따라 폭이 달라지는 요소(`STag` 라벨, 라벨 있는 버튼)는 **가장 긴 값** 기준으로 잡는다. `판매중` 에 맞춰두면 `판매중지 요청` 에서 잘린다.
877
+ - **정렬 가능한 헤더(`sortable`)는 아이콘 버튼 + 간격만큼 `+20px` 더 든다.** `helpText` 를 함께 달면 그만큼 또 더한다.
878
+ - **`editable` · `navigable` 표식과 `required` 표시(`*`) 도 각각 폭을 먹는다.** 헤더에 붙는 것이 늘수록 **라벨이 먼저 잘리므로**, 붙인 열은 폭을 함께 넓힌다.
879
+ - **계산값은 픽셀 단위까지 맞추면 어긋난다** — 서브픽셀 반올림 때문이다. 여유 8px 을 얹고 8 단위로 올림한다.
880
+ - 좌우 패딩은 `STable` 이 토큰으로 넣으므로 직접 주지 않는다. 그만큼을 뺀 나머지가 요소 몫이라는 점만 계산에 넣는다.
881
+
882
+ #### 컨트롤이 들어가는 컬럼
883
+
884
+ `<td>` 는 폭을 넘는 내용을 잘라낸다(`overflow: hidden`). 텍스트라면 말줄임으로 끝나지만, 셀에 `STag` · `SButton` · `SGhostButton` · `SSelect` · `SInput` · `SNumberInput` 처럼 **자기 폭을 가진 요소**를 넣으면 요소 자체가 잘려 **누르거나 읽거나 입력할 수 없게 된다.**
885
+
886
+ - **컨트롤이 들어가는 컬럼에는 `contentType: 'control'` 을 함께 준다.** 좌우 패딩이 텍스트용(넓게)에서 컨트롤용(좁게)으로 바뀌어, 같은 컬럼 폭에서도 요소가 쓸 폭이 넓어진다. 기본값은 `text` 다.
887
+ - 요소가 둘 이상이면 요소 폭의 합에 **`gap` 까지** 더한다 (행 내부 인라인 액션 간격은 `gap-sd-4` 고정, §3-5-5).
754
888
  - `SSelect` · `SInput` 처럼 셀 폭을 채우는 컨트롤은 **컬럼 폭이 곧 컨트롤 폭**이다. 실제 선택값·입력값이 말줄임 없이 읽히는 폭인지 확인한다.
755
889
  - 폭을 넉넉히 줄 수 없는 자리는 폭을 줄이는 게 아니라 **요소를 바꾼다** — 라벨 버튼 대신 아이콘만 있는 `SGhostButton`, `size="xs"` (§3-5-2, §3-5-5).
756
- - **`autoWidth` 는 해법이 아니다.** 내용에 맞춰 늘어나는 게 아니라 고정폭 컬럼들이 가져가고 **남은 폭을 나눠 갖는 것**이라, 테이블이 좁으면 역시 잘린다. 컨트롤 컬럼은 `width` 로 직접 확보한다.
890
+
891
+ ```tsx
892
+ { name: 'normal', label: '정상', field: 'normal', width: 96,
893
+ align: 'center', contentType: 'control', render: row => <SNumberInput … /> },
894
+ ```
895
+
896
+ **셀 좌우 여백은 내용이 정한다.** 텍스트는 넓게, 컨트롤은 좁게다 — `SKeyValueTable` 은 `field.type` 으로 이 판정을 스스로 하지만, `STable` 의 셀은 소비 앱이 넘긴 임의의 `render` 결과라 컴포넌트가 알 수 없다. 그래서 `contentType` 으로 알려준다. 여백 값 자체는 토큰이 정하므로 `tdClass` 로 패딩을 직접 덮어쓰지 않는다.
757
897
 
758
898
  **`resizable` 테이블이면 `minWidth` 를 함께 준다.** resize 하한 기본값은 어떤 컨트롤도 담지 못할 만큼 작아, 사용자가 끝까지 끌면 그대로 잘린다. `width` 를 정한 근거와 같은 값을 하한으로 둔다 — 텍스트 컬럼과 달리 여기서는 더 줄일 여지가 없다.
759
899
 
760
900
  ```tsx
761
901
  const columns: STableColumn[] = [
762
902
  // 태그 — 가장 긴 라벨 기준
763
- { name: 'status', label: '상태', field: 'status', width: '120px', minWidth: 120, align: 'center',
903
+ { name: 'status', label: '상태', field: 'status', width: 120, minWidth: 120, align: 'center',
764
904
  render: (row: SRow) => <STag size="sm" color="green" label={row.statusLabel} /> },
765
905
  // 셀 안 입력 — 컬럼 폭이 곧 입력 폭
766
- { name: 'qty', label: '수량', field: 'qty', width: '100px', minWidth: 100, align: 'right',
906
+ { name: 'qty', label: '수량', field: 'qty', width: 100, minWidth: 100, align: 'right',
767
907
  render: (row: SRow) => <SNumberInput value={row.qty} onValueChange={v => setQty(row, v)} /> },
768
908
  // 인라인 액션 둘 — 폭 = xs 버튼 2개 + gap-sd-4 + 셀 좌우 패딩
769
- { name: 'actions', label: '', field: 'id', width: '84px', minWidth: 84, align: 'center',
909
+ { name: 'actions', label: '', field: 'id', width: 84, minWidth: 84, align: 'center',
770
910
  render: (row: SRow) => (
771
911
  <div className="flex items-center justify-center gap-sd-4">
772
912
  <SGhostButton size="xs" intent="action" icon="edit" ariaLabel="수정" onClick={() => editRow(row)} />
@@ -774,11 +914,34 @@ const columns: STableColumn[] = [
774
914
  </div>
775
915
  ) },
776
916
 
777
- // ❌ 컨트롤 컬럼에 width 생략 — 기본 폭에 맡기면 버튼이 잘린다
917
+ // ❌ 컨트롤 컬럼에 width 생략 — 기본 폭(120px)에 맡기면 버튼이 잘린다
778
918
  { name: 'move', label: '', field: 'id', render: () => <SButton label="재고 이동" size="xs" /> },
779
919
  ];
780
920
  ```
781
921
 
922
+ #### 정렬 가능한 컬럼
923
+
924
+ **정렬 상태는 `STable` 이 갖지 않는다.** 컬럼에 `sortable: true` 를 주고, 페이지가 `sort` · `onSortChange` 로 상태를 들고 있는다.
925
+
926
+ ```tsx
927
+ const [sort, setSort] = useState<STableSort | null>({ name: 'orderedAt', dir: 'desc' });
928
+
929
+ <STable
930
+ columns={columns}
931
+ rows={rows}
932
+ sort={sort}
933
+ onSortChange={setSort}
934
+ />
935
+ ```
936
+
937
+ - **정렬은 조회 조건이다.** 서버 정렬이면 `?sort=createdAt&dir=desc` 가 곧 요청이고, 뒤로가기·새로고침·링크 공유로 복원돼야 한다. 컴포넌트가 사본을 들면 URL 과 화면이 어긋난다 — `SExpansionList` 의 선택을 앱이 드는 것과 같은 이유다 (§3-7-7).
938
+ - **행을 실제로 정렬하는 것도 페이지 몫이다.** `STable` 은 받은 순서대로 그린다.
939
+ - **동작** — 헤더 클릭 시 `asc → desc → 해제` 3단. 다른 열을 누르면 그 열의 `asc` 로 시작한다. 해제되면 `onSortChange(null)`.
940
+ - **아이콘** — 미정렬 `updown`, 오름 `arrowUp`, 내림 `arrowDown`. 정렬 중인 열만 `action` 색으로 올라온다. `SGhostButton size="xxs"` 로 그려지므로 직접 만들지 않는다.
941
+ - **클릭 영역은 정렬 버튼뿐이다.** 헤더 셀 전체를 누르게 하지 않는다 — 라벨을 드래그해 고르거나 `helpText` 아이콘에 hover 하는 것과 뒤섞인다.
942
+ - **다중 정렬은 지원하지 않는다.** 한 번에 한 열이다.
943
+ - `renderHeader` 로 헤더를 통째로 교체하면 정렬 아이콘도 클릭도 그리지 않는다 — 헤더 전체가 소비 앱 책임이 된다.
944
+
782
945
  #### 값이 없는 셀은 회색 하이픈
783
946
 
784
947
  셀을 **빈칸으로 두지 않는다.** 값이 `null` · `undefined` · 빈 문자열이면 `-` 를 `text-fg-tertiary`(`grey_65`)로 표시한다.
@@ -790,9 +953,9 @@ const emptyCell = <span className="text-fg-tertiary">-</span>;
790
953
  const hasValue = (v: unknown) => v !== null && v !== undefined && v !== '';
791
954
 
792
955
  const columns: STableColumn[] = [
793
- { name: 'memo', label: '메모', field: 'memo',
956
+ { name: 'memo', label: '메모', field: 'memo', width: 240,
794
957
  render: (row: SRow) => (hasValue(row.memo) ? row.memo : emptyCell) },
795
- { name: 'price', label: '판매가', field: 'price', width: '120px', align: 'right',
958
+ { name: 'price', label: '판매가', field: 'price', width: 120, align: 'right',
796
959
  render: (row: SRow) =>
797
960
  hasValue(row.price) ? `${Number(row.price).toLocaleString()}원` : emptyCell },
798
961
  ];
@@ -800,6 +963,54 @@ const columns: STableColumn[] = [
800
963
 
801
964
  `0` 은 값이 있는 것이므로 하이픈으로 바꾸지 않는다 — `0원` 그대로 표시한다.
802
965
 
966
+ #### 라벨만으로 뜻이 안 통하는 컬럼은 `helpText`
967
+
968
+ 헤더 라벨은 컬럼 폭 안에 들어가야 해서 짧아진다. **산출 기준·단위·상태 값의 뜻처럼 라벨에 담기지 않는 설명은 `column.helpText` 로 준다** — 라벨 뒤에 도움말 아이콘이 붙고 hover 하면 툴팁이 뜬다. 배열의 각 항목이 한 줄이다. `SKeyValueTable` 의 `field.helpText`, `SSectionHeaderCard` 의 `helpText` 와 같은 것이다.
969
+
970
+ 판단 기준 한 줄: *컬럼 제목이 줄임말·사내 용어·계산식이거나, 값이 아니라 열 자체의 설명이 필요할 때 헤더에 단다.*
971
+
972
+ ```tsx
973
+ const columns: STableColumn[] = [
974
+ { name: 'orderCount', label: '주문 수', field: 'orderCount', width: 120, align: 'right',
975
+ helpText: ['취소·반품을 제외한 확정 주문 수입니다.'],
976
+ format: (v: number) => `${Number(v).toLocaleString()}건` },
977
+ { name: 'status', label: '상태', field: 'status', width: 100, align: 'center',
978
+ helpText: ['활성: 최근 30일 내 주문 있음', '보관됨: 거래 종료'] },
979
+ ];
980
+ ```
981
+
982
+ - **`renderHeader` 로 헤더를 직접 만들어 `STooltip` 을 붙이지 않는다.** `renderHeader` 는 헤더 전체를 교체하므로 `helpText` 가 무시되고, 아이콘·크기·색·간격을 손으로 맞추게 된다.
983
+ - **모든 컬럼에 달지 않는다.** 라벨로 뜻이 통하는 컬럼(`주문번호`·`상품명`)까지 붙이면 헤더가 아이콘으로 뒤덮여 정작 설명이 필요한 컬럼이 묻힌다.
984
+ - 긴 문장을 넣는 자리가 아니다. 한 줄에 한 가지 사실만 담고, 그 이상은 페이지 상단 안내(`SCallout`)로 뺀다.
985
+ - 아이콘도 폭을 먹는다 — 헤더 폭 계산에 넣는다.
986
+
987
+ #### 헤더에 붙는 것들의 순서 · 열을 어떻게 다루는지 알리는 표식
988
+
989
+ 헤더 라벨 뒤에 붙는 것은 네 가지고, **순서는 `STable` 이 고정한다.** 소비 앱이 바꾸는 것이 아니다.
990
+
991
+ ```text
992
+ 라벨 [helpText ?] [editable] [navigable] [required *] [sortable 정렬버튼]
993
+ ```
994
+
995
+ **`editable` 은 값을 직접 고칠 수 있는 열, `navigable` 은 눌러서 다른 화면으로 넘어가는 열에 준다.** 둘 다 **표식일 뿐 버튼이 아니다** — 아이콘·크기·색은 컴포넌트가 고정하고 클릭은 받지 않는다.
996
+
997
+ ```tsx
998
+ const columns: STableColumn[] = [
999
+ { name: 'name', label: '상품명', field: 'name', width: 200,
1000
+ navigable: true,
1001
+ render: (row: SRow) => <STextLink label={row.name} onClick={() => goDetail(row.id)} /> },
1002
+ { name: 'stock', label: '재고', field: 'stock', width: 160, contentType: 'control',
1003
+ editable: true, required: true,
1004
+ render: (row: SRow) => <SNumberInput value={row.stock} width="100%" /> },
1005
+ ];
1006
+ ```
1007
+
1008
+ - **실제로 그렇게 동작하는 열에만 켠다.** 표식만 켜고 셀은 텍스트 그대로 두면, 고칠 수 있다고 해 놓고 고칠 방법이 없고 넘어갈 수 있다고 해 놓고 누를 것이 없다. `editable` 이면 셀에 입력 컨트롤이, `navigable` 이면 셀에 링크·클릭이 있어야 한다.
1009
+ - **표식으로 동작을 대신하지 않는다.** 고치는 UI 도 넘어가는 동작도 셀(`column.render`) 몫이다.
1010
+ - **`renderHeader` 로 헤더를 통째로 교체하면 표식도 `required` 도 그려지지 않는다** — 헤더 전체가 소비 앱 책임이 되므로 순서·크기·색을 손으로 맞추게 된다.
1011
+
1012
+ **값을 반드시 채워야 하는 열에는 `column.required`** 를 준다 — 라벨 뒤에 `*` 가 붙는다. `SKeyValueTable` 의 `field.required` 와 같은 것이다. **읽기 전용 열에 붙이지 않는다** — 표시만 있고 채울 방법이 없어 사용자가 막힌다.
1013
+
803
1014
  ### 3-5. 버튼류
804
1015
 
805
1016
  | 상황 | 사용 |
@@ -823,6 +1034,19 @@ const columns: STableColumn[] = [
823
1034
 
824
1035
  `SDropdownButton` 도 같은 규칙을 따르며, **페이지당 `primary` 채움 1개 계산에 포함**된다.
825
1036
 
1037
+ ##### 무엇이 어느 위계인가
1038
+
1039
+ 개수만으로는 후보가 여럿일 때 어느 것을 올릴지 갈리지 않는다. 기준은 **그 조작이 무엇에 미치는가**다.
1040
+
1041
+ | 위계 | 무엇에 쓰나 |
1042
+ | --- | --- |
1043
+ | `primary` 채움 | **페이지 전체에 해당하는 데이터를 확정**하는 실행 (폼 저장, 상세 수정 확정, 일괄 반영) |
1044
+ | `secondary` 채움 | **페이지 안 중심 데이터에 대한 처리** (선택 항목 상태 변경, 발송, 승인) |
1045
+ | `outline` (`neutral` · `primary`) | 단순 등록, 설정 변경, 이동·취소 |
1046
+
1047
+ - **`primary` 채움은 페이지 전체를 대표하는 실행 하나에만 쓴다. 그런 조작이 없으면 페이지에 `primary` 가 없어도 된다.** 개수 제한이 "반드시 하나 있어야 한다"는 뜻은 아니다.
1048
+ - **`secondary` 연속 배치 금지는 섹션이 다르면 적용되지 않는다.** 섹션마다 독립 인라인 폼이 있는 상세 페이지(§4-4)가 그렇다 — 나란히 놓인 두 버튼이 같은 판단 단위 안에 있을 때의 규칙이다.
1049
+
826
1050
  #### 3-5-2. `size` 는 놓이는 위치가 정한다
827
1051
 
828
1052
  | 위치 | size |
@@ -967,16 +1191,30 @@ const columns: STableColumn[] = [
967
1191
 
968
1192
  > §3-0 라우팅에서 이 절을 가리키는 자리들이다. <!-- TODO(디자인): 전체 검수·확정 -->
969
1193
 
970
- #### 3-7-1. SInput vs STextarea
1194
+ #### 3-7-1. SInput vs STextarea vs SEditor vs SSearchInput
971
1195
 
972
- **줄 수가 아니라 값의 성격으로 고른다.** 값의 길이를 미리 알 수 있으면 `SInput`, 없으면 `STextarea` 다.
1196
+ **먼저 "그 값이 저장되는가"를 본다.** 저장되면 폼 필드(`SInput`·`STextarea`), 화면을 좁히기만 하고 사라지면 `SSearchInput` 이다.
973
1197
 
974
1198
  | 값 | 사용 |
975
1199
  | --- | --- |
976
1200
  | 이름·코드·전화번호·URL 처럼 형식이 정해진 값 | `SInput` |
977
1201
  | 메모·사유·설명처럼 길이가 예측되지 않는 문장 | `STextarea` |
1202
+ | 서식(제목·굵게·목록·정렬·색·링크·이미지)이 값의 일부로 저장되어야 하는 글 | `SEditor` |
1203
+ | 지금 보이는 목록·결과를 좁히는 검색어 | `SSearchInput` |
1204
+
1205
+ 폼 필드 둘은 **줄 수가 아니라 값의 성격으로** 갈린다. 값이 길어질 수 있는데 `SInput` 을 쓰면 사용자가 자기가 쓴 것을 다시 읽지 못한다 — 한 줄 안에서 좌우로 스크롤해야 하기 때문이다. 반대로 짧은 값에 `STextarea` 를 쓰면 빈 공간이 남아 입력량을 잘못 기대하게 한다.
1206
+
1207
+ `SEditor` 는 **서식이 값의 일부일 때만** 쓴다. 값을 HTML 문자열로 주고받으므로 저장·검색·비교가 평문보다 비싸고, 화면에 다시 보여줄 때도 HTML 로 렌더해야 한다. 서식이 필요 없는 메모·사유는 `STextarea` 다 — "입력창이 커 보여서" 고르는 컴포넌트가 아니다. 반대로 공지·안내문·상품 상세처럼 **작성자가 정한 강조와 목록이 그대로 보여야 하는 글**이면 `STextarea` 로는 표현할 수 없다.
978
1208
 
979
- 값이 길어질 수 있는데 `SInput` 을 쓰면 사용자가 자기가 쓴 것을 다시 읽지 못한다 — 한 줄 안에서 좌우로 스크롤해야 하기 때문이다. 반대로 짧은 값에 `STextarea` 를 쓰면 빈 공간이 남아 입력량을 잘못 기대하게 한다.
1209
+ `SEditor` 도 `SInput`·`STextarea` 와 같은 폼 필드다 — `label`·`hint`·`rules`·`errorMessage` 를 자기 prop 으로 받고 `SForm` 제출 검증에도 들어간다. 빈 문서는 빈 문자열로 나오므로 `required` 규칙이 그대로 걸린다. 툴바 구성은 `toolbar` 로 줄이거나 늘릴 수 있고, 서식 입력이 필요 없는 자리에 굳이 놓아야 한다면 `toolbar={false}` 가 아니라 `STextarea` 를 고른다.
1210
+
1211
+ 글을 선택하면 **그 위에 서식 판이 뜬다**(버블 메뉴). 툴바까지 커서를 옮기지 않고 바로 굵게·색·링크를 걸기 위한 것이라 기본으로 켜져 있고, 읽기 전용·비활성일 때는 뜨지 않는다. 판은 한 줄이라 줄바꿈하지 않으므로 **좁은 칸에 놓인 에디터라면 `bubbleMenu` 로 항목을 줄이거나 `false` 로 끈다** — 그대로 두면 필드 밖으로 넘친다. 뜨는 자리는 DS 가 잡는다, 직접 감싸거나 위치를 주지 않는다.
1212
+
1213
+ `SEditor` 는 화면에 처음 놓일 때 **에디터 엔진을 따로 불러온다** — 앱 초기 번들에는 들어가지 않는다. 그동안은 같은 크기의 빈 편집 영역이 자리를 지키므로 레이아웃은 흔들리지 않지만, **마운트하자마자 `ref.current.getHTML()` 로 값을 읽거나 툴바를 누를 수는 없다.** 열자마자 커서를 놓고 싶으면 `ref.current.focus()` 를 그냥 부르면 된다 — 준비되는 순간 대신 실행된다.
1214
+
1215
+ **이미지를 넣으려면 `onImageUpload` 를 준다** — 고른 파일을 저장하고 표시할 URL 을 돌려주는 훅이다. 저장 위치는 앱마다 다르므로 DS 가 정하지 않고, 훅이 없으면 툴바에서 이미지 항목이 빠진다. 본문에 base64 를 박는 길은 막아 두었다 — 저장 HTML 이 수 MB 로 부풀어 그대로 DB·API 에 실리기 때문이다.
1216
+
1217
+ `SSearchInput` 은 폼 필드가 아니다 — 라벨·힌트·유효성 규칙·에러 메시지를 받지 않고, `SForm` 의 제출 검증 대상에도 들어가지 않는다. 돋보기 아이콘이 항상 앞에 붙어 "여기는 검색"임을 스스로 밝히므로 라벨을 따로 붙이지 않는다. 검색 실행은 `onSearch`(Enter) 로 받고, 값이 바뀔 때마다 좁히는 실시간 필터라면 `onValueChange` 만 쓴다. 반대로 검색어를 **저장하거나 검증해야 한다면** 그것은 폼 값이므로 `SInput` 이다.
980
1218
 
981
1219
  #### 3-7-2. 하나를 고르게 하는 다섯 — SSelect vs SRadioGroup vs SRadioButton vs STabs vs SRadio
982
1220
 
@@ -1013,15 +1251,45 @@ const columns: STableColumn[] = [
1013
1251
  | 판별 | 사용 |
1014
1252
  | --- | --- |
1015
1253
  | 날짜 **하나**를 값으로 받는다 | `SDatePicker` |
1254
+ | 연도 선택 리스트만 필요하다 (트리거·팝오버는 직접 조합) | `SDatePickerYearListbox` |
1255
+ | 연도+월 선택 리스트만 필요하다 (트리거·팝오버는 직접 조합) | `SDatePickerMonthListbox` |
1016
1256
  | **시작~종료** 를 값으로 받는다 | `SDateRangePicker` |
1017
1257
  | 달력 격자 **자체가 화면 콘텐츠** 다 (일정·이벤트 보기) | `SCalendar` |
1018
1258
 
1019
1259
  - **기간을 `SDatePicker` 두 개로 만들지 않는다.** 시작이 종료보다 뒤인 입력을 막는 검증과 한쪽만 고른 중간 상태 처리가 `SDateRangePicker` 안에 이미 있다. 두 개로 쪼개면 그게 전부 앱 몫이 된다.
1020
1260
  - `SDatePicker`·`SDateRangePicker` 는 내부적으로 `SCalendar` 를 팝오버로 띄운다. 값을 받는 자리에 `SCalendar` 를 직접 쓰지 않는다.
1261
+ - `SDatePickerYearListbox`·`SDatePickerMonthListbox` 는 `SDatePicker` 의 mode listbox 조각만 떼어낸 컴포넌트다. 일반 폼 입력에는 `SDatePicker mode="year" | "month"` 를 우선 쓰고, 다른 트리거·팝오버 안에 리스트만 끼워 넣을 때만 직접 쓴다.
1262
+
1263
+ **날짜·시간 피커는 폭 상한을 스스로 갖는다 — `width` 를 주지 않는다.** 값 길이가 `YYYY-MM-DD` 처럼 정해져 있어 컴포넌트가 사이즈별 상한을 안다. `SKeyValueTable` 이 모든 컨트롤에 `width="100%"` 를 넘기지만, 이 상한 덕분에 행 전체로 늘어나지 않고 제 폭에서 멈춘다.
1264
+
1265
+ | 컴포넌트 | `size="sm"` | `size="md"` |
1266
+ | --- | --- | --- |
1267
+ | `SDatePicker` | md | lg |
1268
+ | `SDateRangePicker` | lg | xl |
1269
+ | `STimePicker` | md | lg |
1270
+ | `STimeRangePicker` | md | lg (오전/오후 표시는 두 사이즈 모두 lg) |
1271
+
1272
+ `SDateRangePicker` 가 한 등급씩 위인 것은 값이 `YYYY-MM-DD ~ YYYY-MM-DD` 로 두 배가 넘기 때문이다. 같은 이유로 `STimeRangePicker` 의 오전/오후 모드도 sm 에서 한 등급 위를 쓴다 — 그 모드의 최소 폭이 md 등급을 이미 넘어, 그대로 두면 하한이 상한을 넘어 상한이 무력해진다.
1273
+
1274
+ `SDatePicker` 만 이 상한을 `maxWidth` 로 덮을 수 있다 — 등급을 주면 그 등급이 상한이 되고, `width="100%" maxWidth="100%"` 면 부모 폭을 그대로 채운다. **폭이 이미 좁게 정해진 자리(팝오버·좁은 카드)에서만 쓴다.** 폼·표 행에서는 쓰지 않는다 — 거기서 상한을 풀면 4~10글자짜리 값이 행 전체를 차지한다.
1275
+
1276
+ ##### 값을 지울 수 있게 하려면 `clearable`
1277
+
1278
+ `SSelect` · `SDatePicker` · `SDateRangePicker` · `STimePicker` · `STimeRangePicker` 가 같은 규칙으로 갖는다. 값이 있을 때만 지우기 버튼이 나타나고, 누르면 **`onValueChange` 로 `null` 이 온다** (빈 문자열이 아니다). 받는 쪽 상태도 `null` 을 담을 수 있어야 한다.
1279
+
1280
+ ```tsx
1281
+ const [from, setFrom] = useState<string | null>(null);
1282
+
1283
+ <SDatePicker label="시작일" clearable value={from} onValueChange={setFrom} />;
1284
+ ```
1285
+
1286
+ - **조회 조건(필터)에는 켠다.** 한 번 고른 날짜를 되돌릴 방법이 없으면 전체 조회로 돌아가려고 새로고침하게 된다.
1287
+ - **필수 입력 필드에는 켜지 않는다.** 지우면 다시 고르기 전까지 폼이 통과하지 못한다 — 지울 수 있어야 하는 값이면 애초에 필수가 아니다.
1288
+ - `disabled` 이면 지우기 버튼도 함께 사라진다. 끈 필드를 지울 수 있으면 안 되기 때문이다.
1021
1289
 
1022
1290
  #### 3-7-5. SField 를 직접 쓰는 경우
1023
1291
 
1024
- **거의 없다.** `SInput`·`SNumberInput`·`STextarea`·`SSelect`·날짜/시간 피커는 이미 내부에서 `SField` 를 쓰고 있어서 `label`·`required`·에러 표시를 자기 prop 으로 받는다. 그 위에 `SField` 를 한 겹 더 감싸면 라벨이 두 번 나온다.
1292
+ **거의 없다.** `SInput`·`SNumberInput`·`STextarea`·`SEditor`·`SSelect`·날짜/시간 피커는 이미 내부에서 `SField` 를 쓰고 있어서 `label`·`required`·에러 표시를 자기 prop 으로 받는다. 그 위에 `SField` 를 한 겹 더 감싸면 라벨이 두 번 나온다.
1025
1293
 
1026
1294
  직접 쓰는 경우는 하나뿐이다 — **디자인 시스템에 없는 컨트롤**에 다른 필드와 똑같은 라벨·필수·에러 모양을 붙일 때.
1027
1295
 
@@ -1034,15 +1302,17 @@ const columns: STableColumn[] = [
1034
1302
  | **순서 자체가 데이터**라 사용자가 끌어서 바꾼다 | `SDraggableList` + `SDraggableItem` |
1035
1303
 
1036
1304
  - **항목 하나의 속성을 나열하는 것은 목록이 아니다.** `라벨: 값` 이 세로로 쌓이는 것은 `SKeyValueTable` 이다 (§4-4).
1037
- - `SList` 는 레이아웃만 담당한다. 펼침·단일 선택 동작이 필요하면 `SExpansionList` 다 (§3-7-7).
1038
- - **항목 사이 구분선은 리스트가 그린다.** `SListItem`·`SExpansionItem`·`SDraggableItem` 셋 다 스스로 구분선을 그리지 않으므로, 목록을 감싸는 `SList`·`SExpansionList`·`SDraggableList` 에 `separator` 를 준다 — 아이템에 `border-b` 를 직접 붙이지 않는다. 테두리형(`bordered`)은 테두리가 구분 역할을 하므로 `separator` 대신 `useGap` 으로 띄운다.
1039
- - **`SListItem` 은 기본이 표시 전용이다.** 눌러서 이동·선택하게 하려면 `clickable` 을 준다 — hover·`selected`·`interaction="chevron"` 표현이 전부 여기에 딸려 있어서, `clickable` 없이 `selected` 만 주면 선택 표시가 나오지 않는다. `SExpansionList` 는 선택을 자기가 관리하므로 자식 아이템을 알아서 클릭 가능하게 만든다.
1305
+ - `SList` 는 레이아웃만 담당한다. depth 별 단일 펼침이 필요하면 `SExpansionList` 다 (§3-7-7).
1306
+ - **`SList` 의 자식은 `SListItem` 을 권장한다.** 다른 자식도 그대로 렌더되지만, 펼치는 항목은 `SExpansionList` + `SExpansionItem` 이, 끌어서 순서를 바꾸는 항목은 `SDraggableList` + `SDraggableItem` 이 여닫힘·정렬 동작까지 함께 관리하므로 그쪽을 쓴다 (§3-7-7).
1307
+ - **항목 사이 구분선은 리스트가 알아서 그린다.** `SListItem`·`SExpansionItem`·`SDraggableItem` 셋 다 스스로 구분선을 그리지 않는다. `SList`·`SExpansionList`·`SDraggableList` 가 자식 **사이에** 구분선을 넣으므로 아이템에 `border-b` 를 직접 붙이지 않고, 켜는 prop 도 따로 없다. 마지막 항목 아래에는 선이 남지 않는다. 테두리형(`bordered`)은 테두리가 구분 역할을 하므로 리스트가 구분선을 빼고, `useGap` 으로 띄운다 — `useGap` 을 준 목록에도 구분선은 들어가지 않는다.
1308
+ - **`SListItem` 은 기본이 표시 전용이다.** 눌러서 이동·선택하게 하려면 `clickable` 을 준다 — hover·`selected`·`interaction="chevron"` 표현이 전부 여기에 딸려 있어서, `clickable` 없이 `selected` 만 주면 선택 표시가 나오지 않는다. `SExpansionList` 는 자식 아이템을 알아서 클릭 가능하게 만들어 이 함정을 막아 준다 — 선택 상태 자체는 앱이 든다 (§3-7-7).
1040
1309
 
1041
1310
  ```tsx
1042
- ✅ <SList separator><SListItem title="일반 문의" />…</SList> {/* 줄로 구분되는 목록 */}
1311
+ ✅ <SList><SListItem title="일반 문의" />…</SList> {/* 줄로 구분되는 목록 — 구분선은 자동 */}
1043
1312
  ✅ <SList useGap><SListItem title="일반 문의" bordered />…</SList> {/* 카드처럼 떨어진 목록 */}
1044
1313
  ✅ <SListItem title="일반 문의" clickable selected onClick={…} /> {/* 눌러서 고르는 목록 */}
1045
- ❌ <SList><SListItem title="일반 문의" className="border-b" />…</SList>
1314
+ ❌ <SList><SListItem title="일반 문의" className="border-b" />…</SList> {/* 구분선을 직접 붙이지 않는다 */}
1315
+ ❌ <SList><><SListItem title="일반 문의" /><SListItem title="결제 문의" /></></SList> {/* Fragment 로 묶으면 그 안쪽은 구분되지 않는다 */}
1046
1316
  ❌ <SListItem title="일반 문의" selected /> {/* clickable 없으면 선택 표시가 안 나온다 */}
1047
1317
  ```
1048
1318
 
@@ -1054,7 +1324,30 @@ const columns: STableColumn[] = [
1054
1324
  | **한 번에 하나만** 열려야 한다 (아코디언) | `SExpansionList` + `SExpansionItem` |
1055
1325
  | **부모-자식 계층 자체**를 보여줘야 한다 (2단 이상, 연결선) | `STree` |
1056
1326
 
1057
- `SExpansionList` 는 depth 별 단일 확장·선택을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다. 항목 사이 구분선은 여기서도 리스트가 그린다 — `separator` 를 준다 (§3-7-6).
1327
+ `SExpansionList` 는 depth 별 **단일 확장**을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다. 항목 사이 구분선은 여기서도 리스트가 알아서 그린다 — 따로 줄 prop 이 없다 (§3-7-6).
1328
+
1329
+ ##### 펼침 ≠ 선택
1330
+
1331
+ **펼침은 리스트가 관리하고, 선택은 앱이 관리한다.**
1332
+
1333
+ 펼침은 화면 밖에 진실이 없는 순간 UI 상태다. 각 항목이 자기 `expanded` 를 들고 있으면 "하나만 열림"을 만들 수 없어 누군가 나머지를 닫아야 하고, 그것이 이 wrapper 다.
1334
+
1335
+ 선택은 다르다. 앱이 `selectedId` 스칼라 하나를 들면 상호배제가 구조적으로 보장되고, 그 값은 URL·store 로 복원돼야 한다. 리스트가 사본을 들면 그 순간 진실이 둘이 되어 어긋난다.
1336
+
1337
+ ```tsx
1338
+ const [selectedId, setSelectedId] = useState<string>();
1339
+
1340
+ <SExpansionList>
1341
+ {/* 하위가 없는 항목(전체·미분류)은 SExpansionItem 이 아니라 SListItem 이다 */}
1342
+ <SListItem title="전체" selected={selectedId === 'all'} onClick={() => setSelectedId('all')} />
1343
+ <SExpansionItem title="조직">
1344
+ <SListItem title="영업팀" selected={selectedId === 'sales'} onClick={() => setSelectedId('sales')} />
1345
+ </SExpansionItem>
1346
+ </SExpansionList>
1347
+ ```
1348
+
1349
+ - `clickable` 은 리스트가 자식 `SListItem` 에 기본으로 켜 준다 — `clickable` 없이 `selected` 만 주면 표시가 안 나오는 함정(§3-7-6)을 막는 값이다.
1350
+ - **하위를 가지지 않는 항목은 `SExpansionItem` 이 아니라 `SListItem`** 으로 둔다. 펼칠 것이 없는데 펼침 항목으로 만들면 화살표만 남는다.
1058
1351
 
1059
1352
  #### 3-7-8. SCard vs SSectionHeaderCard
1060
1353
 
@@ -1065,7 +1358,7 @@ const columns: STableColumn[] = [
1065
1358
 
1066
1359
  - 페이지 골격에서 콘텐츠를 묶는 섹션은 **사실상 전부 `SSectionHeaderCard`** 다 (§4-4·§4-5). 제목·필수 표시·도움말·헤더 우측 액션이 전부 여기 붙는다.
1067
1360
  - **카드 안에 카드를 겹치지 않는다.** 섹션 안을 더 나눠야 하면 `SDivider` 로 끊거나(§3-6) 섹션을 둘로 분리한다.
1068
- - 안쪽 여백은 `SSectionHeaderCard.Body` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1361
+ - 안쪽 여백은 `SSectionHeaderCard` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1069
1362
 
1070
1363
  #### 3-7-9. SLinearProgress vs SCircleProgress
1071
1364
 
@@ -1092,7 +1385,7 @@ const columns: STableColumn[] = [
1092
1385
  - **기본은 `SKeyValueTable` 이다** (§4-2). 조건이 대여섯 개 이하로 고정이면 표로 펼쳐 두는 편이 한눈에 읽힌다.
1093
1386
  - `SChipFilter` 는 조건을 **칩 한 줄**로 접고, "필터 추가" 로 필요한 것만 꺼내 쓰게 한다. 칩을 누르면 편집 팝오버가 열리고, 날짜는 프리셋(오늘·지난 7일·사용자 지정)으로 고른다. 조건 후보가 많은 목록 화면에서 필터가 화면을 세로로 잡아먹는 것을 막는 용도다.
1094
1387
  - 검색 실행 시점이 다르다 — `SKeyValueTable` 필터는 앱이 검색 버튼을 직접 놓지만, `SChipFilter` 는 편집 팝오버가 닫히거나 "검색" 을 누를 때 `onSearch` 가 값 맵과 함께 호출된다. 값이 바뀌지 않았으면 호출되지 않는다.
1095
- - 함께 걸어야 하는 조건(예: 기간 중 하나는 필수)이 있으면 `fields` 를 그룹으로 넘긴다. 규칙을 못 채운 동안 경고 툴팁이 떠 있고 `onSearch` 가 막힌다.
1388
+ - **`fields` 는 항상 그룹 배열이다.** 묶을 것이 없어도 `[{ fields: [...] }]` 로 한 겹 감싼다. 함께 걸어야 하는 조건(예: 기간 중 하나는 필수)이 있으면 그 필드들만 별도 그룹으로 떼어 `rule` 을 준다 — 규칙을 못 채운 동안 경고 툴팁이 떠 있고 `onSearch` 가 막힌다. 그룹 앞 구분선은 `divider` 로 켠다. 검증 단위와 구분선은 별개라, 묶어서 검증만 하고 싶으면 `divider` 를 주지 않는다.
1096
1389
 
1097
1390
  #### 3-7-12. 이미지 — SImage
1098
1391
 
@@ -1137,15 +1430,27 @@ export default function AppShell({
1137
1430
  children,
1138
1431
  header,
1139
1432
  scrollEndSpacing,
1140
- }: { children: React.ReactNode; header?: SPageHeaderProps; scrollEndSpacing?: boolean }) {
1433
+ contentHeight,
1434
+ }: {
1435
+ children: React.ReactNode;
1436
+ header?: SPageHeaderProps;
1437
+ scrollEndSpacing?: boolean;
1438
+ contentHeight?: SPageContentHeight;
1439
+ }) {
1141
1440
  return (
1142
1441
  <SLayout type="box" header="fix">
1143
1442
  {/* type/header/folded 는 SLayout 에만 준다 — SGnb 는 context 에서 읽는다 */}
1144
1443
  <SGnb items={MENU} value={current} onValueChange={navigate} logo={<Logo />} />
1145
1444
  {/* 콘텐츠 패딩은 SPage 가 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
1146
- {/* 스크롤 끝 여백도 SPage 가 넣는다. 끄는 건 페이지네이션 있는 목록뿐이라 페이지가 정한다 */}
1445
+ {/* 높이 모드는 페이지가 정한다 — 대부분 contentHeight="fill" 이다 (§2-2) */}
1446
+ {/* 스크롤 끝 여백도 SPage 가 넣는다. 페이지가 실제로 스크롤되는 화면에서만 켠다 */}
1147
1447
  {/* header 는 페이지마다 달라 AppShell 이 그대로 받아 넘긴다 — 페이지 제목은 여기서 만들지 않는다 */}
1148
- <SPage background="frame" scrollEndSpacing={scrollEndSpacing} header={header}>
1448
+ <SPage
1449
+ background="frame"
1450
+ scrollEndSpacing={scrollEndSpacing}
1451
+ contentHeight={contentHeight}
1452
+ header={header}
1453
+ >
1149
1454
  {children}
1150
1455
  </SPage>
1151
1456
  </SLayout>
@@ -1187,7 +1492,9 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1187
1492
 
1188
1493
  **최소 너비는 `SLayout` 이 보장한다 — 앱이 `min-w-*` 를 직접 주지 않는다.** 창이 최소 너비(`SLAYOUT_MIN_WIDTH`, GNB 포함한 전체 기준)보다 좁아지면 GNB·상단바는 제자리에 남고 **`SPage` 안에만 가로 스크롤이 생긴다.** 문서(브라우저 창)에는 가로 스크롤이 생기지 않는다. 그래서 셸을 감싸는 요소에 `min-width` 나 `overflow-x` 를 걸지 않는다 — 걸면 창 전체가 스크롤되어 GNB 가 화면 밖으로 밀린다.
1189
1494
 
1190
- **셸의 `SPage` 는 모든 페이지가 공유하므로, 스크롤 끝 여백을 끄려면 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `scrollEndSpacing` 을 받아 그대로 넘기고, 페이지네이션이 있는 목록 페이지만 `false` 를 준다 (§4-2). 나머지 페이지는 넘기지 않으면 기본값(켬)이 적용된다.
1495
+ **셸의 `SPage` 는 모든 페이지가 공유하므로, 페이지마다 달라지는 것은 페이지가 셸에 알려야 한다.** 위처럼 프레임 컴포넌트가 `contentHeight` · `scrollEndSpacing` 을 받아 그대로 넘긴다.
1496
+
1497
+ **대부분의 페이지는 `contentHeight="fill"` 이다** — 본문이 창을 꽉 채우고 스크롤은 각 영역 안에서 일어나는 것이 표준이다(§2-2). 블록의 높이가 정해져 있고 그 높이가 창보다 커서 페이지 자체가 스크롤돼야 하는 화면에서만 `contentHeight="auto"`(기본값) + `scrollEndSpacing` 을 켠다.
1191
1498
 
1192
1499
  **상단바 배치는 `header` 가 정한다.** 요소 순서가 달라지므로 슬롯을 채우기 전에 어느 쪽인지부터 정한다.
1193
1500
 
@@ -1209,7 +1516,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1209
1516
  topContent={
1210
1517
  /* 남는 폭 전체를 받는다 — 왼쪽은 그대로, 오른쪽 끝은 ml-auto */
1211
1518
  <div className="flex w-full items-center gap-sd-8">
1212
- <SInput value={keyword} onValueChange={setKeyword} placeholder="통합 검색" />
1519
+ <SSearchInput value={keyword} onValueChange={setKeyword} onSearch={runSearch} placeholder="통합 검색" />
1213
1520
  <SButton size="sm" color="neutral" outline label="내 계정" className="ml-auto" onClick={openAccount} />
1214
1521
  </div>
1215
1522
  }
@@ -1226,7 +1533,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1226
1533
  {/* 접히면 menuTop·menuFooter 가 함께 빠지므로, 폴드 레일에 남길 것만 foldedTop 으로 따로 준다 */}
1227
1534
  <SGnb
1228
1535
  items={MENU} value={current} onValueChange={navigate} useRail
1229
- menuTop={<SInput value={keyword} onValueChange={setKeyword} placeholder="메뉴 검색" />}
1536
+ menuTop={<SSearchInput value={keyword} onValueChange={setKeyword} placeholder="메뉴 검색" />}
1230
1537
  menuFooter={<AccountRow />}
1231
1538
  foldedTop={<SGhostButton icon="search" size="sm" ariaLabel="메뉴 검색" onClick={openSearch} />}
1232
1539
  />
@@ -1245,7 +1552,10 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1245
1552
  - **페이지 제목 줄에는 이 페이지의 주요 액션을 두지 않는다.** 부가적인 것만 `header.slot` 에 `SButton size="sm"` 으로 온다 (§4-1 "페이지 헤더 사용 규칙").
1246
1553
  - **주요 액션(등록 등)은 `STableBar` 의 `rightActions`** 에 둔다.
1247
1554
  - **선택 상태 액션(선택 삭제 등)은 `STableBar` 의 `actions`** 에 둔다. `actions` 슬롯은 건수 요약이 있으면 앞에 구분선(`SDivider`)을 **자동으로** 넣으므로 직접 구분선을 만들지 않는다.
1248
- - **페이지네이션이 있으면 스크롤 끝 여백을 끈다** — `AppShell` 에 `scrollEndSpacing={false}` 를 넘긴다 (§2-2). 페이지네이션이 이미 "여기서 끝"을 알려준다.
1555
+ - **본문이 남은 높이를 채우게 한다** — `AppShell` 에 `contentHeight="fill"` 을 넘긴다(§2-2 표준). 페이지가 통째로 스크롤되면 페이지네이션이 화면 밖으로 밀려 "여기서 끝"이 읽히지 않는다. `fill` 이면 **표만 자기 안에서 스크롤하고 페이지네이션은 하단에 고정**된다.
1556
+ - 본문 루트에 `h-full min-h-0` 으로 세로 축을 잇고, 남은 높이를 먹을 `STable` 에 `min-h-0 flex-1` 을 준다. 이 사슬이 하나라도 끊기면 표가 높이를 못 잡는다.
1557
+ - `fill` 에서는 페이지가 스크롤하지 않으므로 **`scrollEndSpacing` 은 무시된다** — 따로 끄지 않는다 (§2-2).
1558
+ - **정렬 가능한 컬럼은 `sortable` 로 준다.** 정렬 상태(`sort`)는 이 페이지가 들고 `onSortChange` 로 받는다 — 조회 조건이라 URL 에 실려야 한다 (§3-4).
1249
1559
 
1250
1560
  ```tsx
1251
1561
  import {
@@ -1293,9 +1603,10 @@ export default function ProductListPage() {
1293
1603
  // 이 페이지의 주요 액션이 아니라 부가 액션 — slot 은 sm 버튼으로만 채운다
1294
1604
  slot: <SButton size="sm" color="neutral" outline label="이용 가이드" onClick={openGuide} />,
1295
1605
  }}
1296
- scrollEndSpacing={false} // 페이지네이션이 있으므로 끈다
1606
+ contentHeight="fill" // 표가 남은 높이를 채우고 페이지네이션이 하단에 고정된다
1297
1607
  >
1298
- <div className="flex flex-col gap-sd-12">
1608
+ {/* h-full min-h-0 → STable 의 min-h-0 flex-1 로 세로 축이 이어진다 */}
1609
+ <div className="flex h-full min-h-0 flex-col gap-sd-12">
1299
1610
  {/* 필터 — search 를 켜면 우측에 검색 패널이 붙는다 */}
1300
1611
  <SKeyValueTable
1301
1612
  fields={filterFields}
@@ -1320,7 +1631,9 @@ export default function ProductListPage() {
1320
1631
  }
1321
1632
  />
1322
1633
 
1634
+ {/* 남은 높이를 채우고 본문만 스크롤한다 — 페이지네이션 바는 표 안에서 하단 고정 */}
1323
1635
  <STable
1636
+ className="min-h-0 flex-1"
1324
1637
  columns={columns}
1325
1638
  rows={rows}
1326
1639
  rowKey="id"
@@ -1336,6 +1649,29 @@ export default function ProductListPage() {
1336
1649
  }
1337
1650
  ```
1338
1651
 
1652
+ #### 한 화면에 더 많은 행을 — `dense` 와 밀도 토글
1653
+
1654
+ 행 높이를 줄이는 것은 `dense` 다. 세로 여백만 줄고 좌우 패딩은 그대로라, 값이 잘리지 않으면서 한 화면에 들어가는 행 수가 늘어난다.
1655
+
1656
+ **어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 목록 페이지는 밀도를 고정하지 말고 `useDensityToggle` 로 고를 수 있게 둔다 — 페이지네이션 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다.
1657
+
1658
+ ```tsx
1659
+ // 사용자가 고른 밀도는 다음 방문에도 남는 것이 자연스럽다 — 저장은 페이지 몫이다
1660
+ const [dense, setDense] = useState(() => loadPref('list.dense', true));
1661
+
1662
+ <STable
1663
+ dense={dense}
1664
+ onDenseChange={next => { setDense(next); savePref('list.dense', next); }}
1665
+ useDensityToggle
1666
+ useRowsPerPageSelect
1667
+ pagination={{ currentPage, lastPage }}
1668
+ />;
1669
+ ```
1670
+
1671
+ - **밀도는 `STable` 이 갖지 않는다.** `dense` 가 곧 현재 상태이고, `onDenseChange` 없이 `useDensityToggle` 만 켜면 눌러도 아무 일도 일어나지 않는다.
1672
+ - **토글은 페이지네이션이 있을 때만 나타난다** — 사는 곳이 그 바이기 때문이다. 페이지네이션 없는 표에서 밀도를 고르게 하려면 `STableBar` 쪽에 직접 둔다.
1673
+ - 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. `dense` 면 `넓게 보기` 다.
1674
+
1339
1675
  ### 4-3. 폼 페이지 (등록/수정)
1340
1676
 
1341
1677
  구조: **페이지 제목(`AppShell` 의 `header` prop) → `SForm` + `SKeyValueTable` → 하단 버튼**
@@ -1343,6 +1679,19 @@ export default function ProductListPage() {
1343
1679
  - 필드를 `div` 로 나열하지 않고 **`SKeyValueTable` 의 행으로 구성**한다.
1344
1680
  - 검증 규칙은 각 field 의 `options.rules` 로 넘긴다. `SForm` 이 하위 컨트롤을 자동 수집해 submit 시 일괄 검증하고, 실패 시 첫 실패 필드로 포커스를 옮긴다.
1345
1681
  - **버튼 순서: 취소·닫기가 왼쪽, 저장·등록·수정·삭제가 오른쪽.** 이 순서는 모든 화면에서 동일하다.
1682
+ - **폼 페이지도 `contentHeight="fill"` 이 기본이다**(§2-2). 폼이 길어 페이지가 실제로 스크롤되는 화면에서만 `auto` + `scrollEndSpacing` 을 켠다.
1683
+ - **필드 폭은 등급으로 준다** — `width="md"` 처럼 `'xs' | 'sm' | 'md' | 'lg' | 'xl'` 중 하나다. px 를 직접 적지 않는다. 등급은 `maxLength`(= 스키마 상한)로 고르고, 상한이 `xl` 을 넘거나 상한이 없으면 `width="100%"` 로 행 전체를 쓴다 (§6 `field-width-grade`).
1684
+
1685
+ **`SKeyValueTable` 의 전체 열 수는 가장 긴 행이 정한다.** 어떤 행이 그보다 짧으면 남는 자리에 셀이 없어 그 구간의 행 구분선이 끊긴다. 마지막 필드에 `tdColSpan` 을 주어 채운다.
1686
+
1687
+ ```tsx
1688
+ [
1689
+ [{ name: 'category', … }, { name: 'price', … }], // 필드 2개 → 4칸
1690
+ [{ name: 'memo', …, tdColSpan: 3 }], // th(1) + td(3) = 4칸
1691
+ ]
1692
+ ```
1693
+
1694
+ **한 행에 필드를 추가하면 다른 행들의 `tdColSpan` 도 함께 봐야 한다.** 전체 열 수가 늘면 나머지 행들이 조용히 짧아진다 — 화면에서만 드러나는 컴포넌트 고유 동작이라 자동으로 채워 주지 않는다.
1346
1695
 
1347
1696
  ```tsx
1348
1697
  import {
@@ -1406,8 +1755,10 @@ export default function ProductCreatePage() {
1406
1755
 
1407
1756
  - 조회 값은 `type: 'text'` 행으로 표시한다. **상태·분류 태그도 별도 영역이 아니라 표의 한 행**으로 넣는다 (`render` 에 `STag`).
1408
1757
  - 행이 많아지면 **유형별로 섹션을 나누고, 각 섹션을 `SSectionHeaderCard` 로 감싼다.**
1409
- 합성 컴포넌트라 `SSectionHeaderCard.Header` / `SSectionHeaderCard.Body` 를 자식으로 쓴다.
1758
+ 섹션 제목은 `title` prop 으로, 바디 여백은 `padding` prop 으로 준다.
1410
1759
  - **수정·삭제 버튼은 하단에 둔다.** 내용이 짧아 우측 상단에 두는 변형도 있으나 기본은 하단이다.
1760
+ - **상세 페이지도 `contentHeight="fill"` 이 기본이다**(§2-2). 섹션이 많아 페이지가 실제로 스크롤되는 화면에서만 `auto` + `scrollEndSpacing` 을 켠다.
1761
+ - **섹션마다 독립 인라인 폼이 있는 형태**도 상세 페이지의 변형이다. 섹션 안에서 바로 수정·저장하게 하는 화면인데, 이때 버튼 강조는 **섹션 단위가 아니라 페이지 단위로 판단한다** — §3-5-1 의 "`secondary` 연속 배치 금지"는 섹션이 다르면 적용되지 않는다.
1411
1762
 
1412
1763
  ```tsx
1413
1764
  import {
@@ -1439,24 +1790,18 @@ export default function ProductDetailPage() {
1439
1790
  // 목록에서 들어온 상세 페이지 — onBack 으로 뒤로가기를 준다
1440
1791
  <AppShell header={{ fix: true, title: '클래식 셔츠', onBack: goList }}>
1441
1792
  <div className="flex flex-col gap-sd-12">
1442
- <SSectionHeaderCard>
1443
- <SSectionHeaderCard.Header title="기본 정보" marker thickness="accent" />
1444
- <SSectionHeaderCard.Body>
1445
- <SKeyValueTable fields={basicFields} values={product} />
1446
- </SSectionHeaderCard.Body>
1793
+ <SSectionHeaderCard title="기본 정보" marker thickness="accent">
1794
+ <SKeyValueTable fields={basicFields} values={product} />
1447
1795
  </SSectionHeaderCard>
1448
1796
 
1449
- <SSectionHeaderCard>
1450
- {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1451
- <SSectionHeaderCard.Header
1452
- title="가격 정보"
1453
- marker
1454
- helpText={['부가세 포함 금액입니다.']}
1455
- slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1456
- />
1457
- <SSectionHeaderCard.Body>
1458
- <SKeyValueTable fields={priceFields} values={product} />
1459
- </SSectionHeaderCard.Body>
1797
+ {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1798
+ <SSectionHeaderCard
1799
+ title="가격 정보"
1800
+ marker
1801
+ helpText={['부가세 포함 금액입니다.']}
1802
+ slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1803
+ >
1804
+ <SKeyValueTable fields={priceFields} values={product} />
1460
1805
  </SSectionHeaderCard>
1461
1806
 
1462
1807
  {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
@@ -1491,6 +1836,21 @@ export default function ProductDetailPage() {
1491
1836
  | --- | --- |
1492
1837
  | `padding` | 안쪽 여백 — `'default'`(기본) / `'wide'` / `'none'`. 판정은 §2-2 "섹션·패널 안쪽 여백". `p-sd-*` 를 직접 주지 않는다 |
1493
1838
 
1839
+ **한 페이지에 섹션 카드가 둘 이상이면 `marker` 를 켠다.** 점은 섹션을 서로 구분할 대상이 여럿일 때만 의미가 있어, 카드가 하나뿐인 페이지에서는 켜지 않는다. 한 페이지 안에서는 켜거나 끄거나 전부 같게 간다.
1840
+
1841
+ **섹션 본문이 자기 안에서 스크롤해야 하면 루트 `className` 으로 마지막 자식에 세로 축을 잇는다.**
1842
+
1843
+ ```tsx
1844
+ <SSectionHeaderCard
1845
+ title="…"
1846
+ className="[&>div:last-child]:min-h-0 [&>div:last-child]:flex-1"
1847
+ >
1848
+ <STable className="min-h-0 flex-1" … />
1849
+ </SSectionHeaderCard>
1850
+ ```
1851
+
1852
+ 본문 래퍼는 `className` 을 받지 않으므로(여백은 `padding` prop 으로만 받는다) 루트에서 내려 준다. 흔한 구성은 아니다 — 대부분은 `STable` 이 자기 안에서 스크롤하므로 여기까지 갈 일이 없다.
1853
+
1494
1854
  ---
1495
1855
 
1496
1856
  ## 5. 자가 점검 체크리스트
@@ -1505,8 +1865,11 @@ export default function ProductDetailPage() {
1505
1865
  - [ ] 텍스트 회색 위계를 순차 적용했는가 (기본 → `text-fg-secondary` → `text-fg-tertiary`, 단계 건너뛰기 ❌)
1506
1866
  - [ ] `SPage`·`SPopup` 의 기본 패딩을 `p-sd-*` 로 덮어쓰지 않았는가, 블록·섹션 **간격**이 `gap-sd-12` 인가 (`gap-sd-16`/`gap-sd-24` ❌ — 24 는 안쪽 여백에만 열린다)
1507
1867
  - [ ] 섹션·패널의 안쪽 **여백**이 §2-2 판정과 맞는가 (덩어리 두 종류 → 16 / 세 종류 이상 → 24, 서면 16)
1508
- - [ ] `SSectionHeaderCard.Body` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
1509
- - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 를 넘겼는가
1868
+ - [ ] `SSectionHeaderCard` 의 여백을 `p-sd-*` 가 아니라 `padding` prop 으로 줬는가
1869
+ - [ ] 페이지에 `contentHeight="fill"` 을 넘겼는가 (§2-2 표준 — 페이지 스크롤을 쓰는 화면에서만 `auto` + `scrollEndSpacing`)
1870
+ - [ ] `fill` 을 쓴 블록에서 **자식까지 `min-h-0 flex-1` 이 끊기지 않았는가** (한 군데만 끊겨도 자식이 높이를 못 잡는데 실패가 조용하다)
1871
+ - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가
1872
+ - [ ] 한 페이지에 섹션 카드가 둘 이상이면 `marker` 를 켰는가, 하나뿐이면 껐는가 (§4-5)
1510
1873
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-sd-24` 등)
1511
1874
  - [ ] 페이지가 §4의 표준 골격에서 시작했는가
1512
1875
  - [ ] `header.fix` 가 프로젝트 전체와 같은 값인가 (다른 페이지와 다르게 섞어 쓰지 않았는가, §4-1)
@@ -1515,11 +1878,20 @@ export default function ProductDetailPage() {
1515
1878
  - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
1516
1879
  - [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
1517
1880
  - [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
1881
+ - [ ] 목록 페이지 표에 `useDensityToggle` 로 밀도를 고를 수 있게 뒀는가, `onDenseChange` 를 함께 줬는가 (§4-2 — 핸들러 없이 켜면 눌러도 아무 일도 없다)
1518
1882
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
1519
1883
  - [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
1520
1884
  - [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
1521
- - [ ] 컨트롤(`STag`·`SButton`·`SSelect`·`SInput` …)이 들어가는 컬럼에 `width` 를 명시했는가, `resizable` 이면 `minWidth` 도 줬는가 (§3-4 — 폭이 모자라면 요소가 잘려 못 쓴다)
1885
+ - [ ] 닫힌 값 집합(enum·마스터 목록에서 고르는 값) 컬럼에 `align: 'center'` 를 줬는가 — 태그로 그렸든 맨 텍스트로 그렸든 같다 (§3-4)
1886
+ - [ ] **모든 컬럼에 폭을 명시**했는가, px 로만 줬는가 (`%`·`clamp()` ❌), `autoWidth` 는 스페이서 열 하나뿐인가 (§3-4)
1887
+ - [ ] 컨트롤(`STag`·`SButton`·`SSelect`·`SInput` …)이 들어가는 컬럼이 `resizable` 이면 `minWidth` 도 줬는가 (§3-4 — 폭이 모자라면 요소가 잘려 못 쓴다)
1888
+ - [ ] 정렬 가능한 열에 `sortable` 을 줬는가 (`renderHeader` 로 직접 만들지 않았는가), 정렬 상태를 페이지가 들고 있는가 (§3-4)
1889
+ - [ ] `editable` · `navigable` 표식을 켠 열이 **셀에서도 실제로 그렇게 동작하는가** (입력 컨트롤 · 링크가 있는가), 표식을 붙인 열의 폭을 함께 넓혔는가 (§3-4)
1522
1890
  - [ ] 금액·수량 등 양을 나타내는 숫자에 빠짐없이 `toLocaleString()` 을 썼는가 (번호·코드는 제외)
1891
+ - [ ] 날짜를 `YYYY-MM-DD` 로 표기했는가 (`toLocaleDateString()` ❌, §1-4)
1892
+ - [ ] 필드 폭을 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`)으로 줬는가 — px 직접 지정 ❌ (§4-3)
1893
+ - [ ] 조회 조건으로 쓰는 셀렉트·날짜·시간 피커에 `clearable` 을 줬는가, 그 상태가 `null` 을 담을 수 있는가 (§3-7-4 — 필수 입력 필드에는 켜지 않는다)
1894
+ - [ ] `SKeyValueTable` 의 짧은 행에 `tdColSpan` 을 주어 전체 열 수를 채웠는가 (§4-3 — 안 채우면 그 구간의 행 구분선이 끊긴다)
1523
1895
  - [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
1524
1896
  - [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
1525
1897
  - [ ] 버튼 `size` 가 위치 규칙과 맞는가 (행 내부 `xs` / 화면 액션 `sm` / 모달 푸터 `md`)
@@ -1556,6 +1928,8 @@ export default function ProductDetailPage() {
1556
1928
  | `sellmate/component-group-gap` | warn | §2-2 컴포넌트 그룹 간격 (체크박스 가로 24 / 세로 8 등) |
1557
1929
  | `sellmate/table-numeric-align` | warn | §3-4 숫자 컬럼의 `align: 'right'` 누락 (`--fix` 지원) |
1558
1930
  | `sellmate/require-locale-number` | warn | §1-4 금액·수량 등 수량 컬럼의 `toLocaleString()` 누락 |
1931
+ | `sellmate/field-width-grade` | warn | §4-3 필드 폭이 `maxLength` 상한과 맞는 등급인가, px 를 직접 적지 않았는가 (px → 등급 `--fix` 지원) |
1932
+ | `sellmate/table-column-width` | warn | §3-4 컬럼 폭 미지정(기본 120px)·px 아닌 값(`%`·`clamp()`)·`autoWidth` 오용 |
1559
1933
  | `sellmate/no-arbitrary-class` | off | §1-2 토큰 있는 속성의 임의 값 (`text-[14px]`, `bg-[#eee]`) — 팀이 켤 때만 |
1560
1934
 
1561
1935
  `configs.strict` 를 쓰는 프로젝트는 전부 error 이고 간격 `sd-` 접두까지 강제된다.
@@ -1741,7 +2115,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1741
2115
  --sys-color-link-accent
1742
2116
  --sys-color-navigation-gnb-bg-dark
1743
2117
 
1744
- ## 3. 컴포넌트 카탈로그 (Props / Events / Methods)
2118
+ ## 3. 컴포넌트 카탈로그 (Props / Events / Methods / Types)
1745
2119
 
1746
2120
  # SActionModal
1747
2121
 
@@ -1794,6 +2168,30 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1794
2168
  |------|------|---------|-------------|
1795
2169
  | `color?` | `SBadgeColor` | `'blue'` | 뱃지 색상 |
1796
2170
 
2171
+ ## Types
2172
+
2173
+ ### SBadgeColor
2174
+
2175
+ ```ts
2176
+ export type SBadgeColor = (typeof BADGE_COLORS)[number];
2177
+ ```
2178
+
2179
+ ### BADGE_COLORS
2180
+
2181
+ ```ts
2182
+ export const BADGE_COLORS = [
2183
+ 'red',
2184
+ 'orange',
2185
+ 'yellow',
2186
+ 'green',
2187
+ 'lightblue',
2188
+ 'blue',
2189
+ 'darkblue',
2190
+ 'indigo',
2191
+ 'grey',
2192
+ ] as const;
2193
+ ```
2194
+
1797
2195
  ## Dependencies
1798
2196
 
1799
2197
  ### Used by
@@ -1841,7 +2239,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1841
2239
  | `hint?` | `string` | — | |
1842
2240
  | `error?` | `boolean` | — | |
1843
2241
  | `errorMessage?` | `string` | — | |
1844
- | `width?` | `number \| string` | — | |
2242
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리며 `maxLength`(= 스키마 상한)로 고른다. 상한이 `xl` 을 넘거나 상한이 없으면 `"100%"`. (`sellmate/field-width-grade` 가 검사한다) |
1845
2243
  | `className?` | `string` | — | |
1846
2244
  | `style?` | `CSSProperties` | — | |
1847
2245
 
@@ -1853,6 +2251,14 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1853
2251
  | `onFocus` | `() => void` | |
1854
2252
  | `onBlur` | `() => void` | |
1855
2253
 
2254
+ ## Types
2255
+
2256
+ ### SBarcodeInputSize
2257
+
2258
+ ```ts
2259
+ export type SBarcodeInputSize = SFieldSize;
2260
+ ```
2261
+
1856
2262
  ## Dependencies
1857
2263
 
1858
2264
  ### Depends on
@@ -1881,12 +2287,47 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1881
2287
  | `rightIcon?` | `SIconName` | — | 레이블 오른쪽 아이콘 |
1882
2288
  | `label?` | `string` | — | 버튼 텍스트 (문자열만 — 아이콘은 icon/rightIcon 사용) |
1883
2289
 
2290
+ ## Types
2291
+
2292
+ ### SButtonColor
2293
+
2294
+ ```ts
2295
+ export type SButtonColor = (typeof BUTTON_COLORS)[number];
2296
+ ```
2297
+
2298
+ ### SButtonSize
2299
+
2300
+ ```ts
2301
+ export type SButtonSize = (typeof BUTTON_SIZES)[number];
2302
+ ```
2303
+
2304
+ ### BUTTON_COLORS
2305
+
2306
+ ```ts
2307
+ /**
2308
+ * SButton 색상/사이즈 설정 — sd-button(component.button 토큰) 충실 포팅.
2309
+ * Stencil `name`(예: primary_sm)의 preset을 color + outline(boolean) + size 로 분리.
2310
+ * - primary / danger : solid·outline 모두 지원
2311
+ * - secondary : solid 전용 (outline 스타일 없음 → outline 무시)
2312
+ * - neutral : 흰 배경 고정, outline 은 회색 테두리만 추가(solid = 테두리 없는 흰 버튼)
2313
+ * 색상은 theme.css의 `--cmp-button-*` CSS 변수를 참조한다.
2314
+ */
2315
+ export const BUTTON_COLORS = ['primary', 'secondary', 'neutral', 'danger'] as const;
2316
+ ```
2317
+
2318
+ ### BUTTON_SIZES
2319
+
2320
+ ```ts
2321
+ export const BUTTON_SIZES = ['xs', 'sm', 'md', 'lg'] as const;
2322
+ ```
2323
+
1884
2324
  ## Dependencies
1885
2325
 
1886
2326
  ### Used by
1887
2327
 
1888
2328
  - [SConfirmModal](../SConfirmModal)
1889
2329
  - [SDropdownButton](../SDropdownButton)
2330
+ - [SEditor](../SEditor)
1890
2331
  - [SFooter](../SFooter)
1891
2332
  - [SKeyValueTable](../SKeyValueTable)
1892
2333
  - [SLoadingModal](../SLoadingModal)
@@ -1925,6 +2366,19 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1925
2366
  | `onValueChange` | `(date: string) => void` | 선택 변경 (sdUpdate) |
1926
2367
  | `onViewChange` | `(v: { year: number; month: number }) => void` | 보이는 연·월 변경 (sdViewChange) |
1927
2368
 
2369
+ ## Types
2370
+
2371
+ ### SCalendarEventGroup
2372
+
2373
+ ```ts
2374
+ export interface SCalendarEventGroup {
2375
+ /** 도트 색상. 팔레트 키(`grey_65`, `red_95` …) 또는 임의 CSS 색상 */
2376
+ color: SColor;
2377
+ label: string;
2378
+ dates: string[];
2379
+ }
2380
+ ```
2381
+
1928
2382
  ## Dependencies
1929
2383
 
1930
2384
  ### Used by
@@ -1956,6 +2410,21 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
1956
2410
  | `className?` | `string` | — | |
1957
2411
  | `style?` | `CSSProperties` | — | |
1958
2412
 
2413
+ ## Types
2414
+
2415
+ ### SCalloutType
2416
+
2417
+ ```ts
2418
+ export type SCalloutType = 'default' | 'danger';
2419
+ ```
2420
+
2421
+ ### SCalloutMessage
2422
+
2423
+ ```ts
2424
+ /** 중첩 메시지: 문자열 또는 (한 단계 더 들어간) 문자열 배열 */
2425
+ export type SCalloutMessage = string | SCalloutMessage[];
2426
+ ```
2427
+
1959
2428
  ## Dependencies
1960
2429
 
1961
2430
  ### Depends on
@@ -2004,6 +2473,14 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2004
2473
  |-------|------|-------------|
2005
2474
  | `onValueChange` | `(value: boolean \| unknown[]) => void` | 값 변경 (sdUpdate) |
2006
2475
 
2476
+ ## Types
2477
+
2478
+ ### SCheckboxValue
2479
+
2480
+ ```ts
2481
+ export type SCheckboxValue = boolean | unknown[] | null;
2482
+ ```
2483
+
2007
2484
  ## Dependencies
2008
2485
 
2009
2486
  ### Used by
@@ -2082,12 +2559,11 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2082
2559
 
2083
2560
  | Prop | Type | Default | Description |
2084
2561
  |------|------|---------|-------------|
2085
- | `fields?` | `SChipFilterField[] \| SChipFilterGroup[]` | — | 필터 정의 목록. 그룹으로 묶으려면 SChipFilterGroup[]을 넘긴다 — 그룹이 시작될 때마다 앞에 구분선이 자동으로 붙는다(showLabel·인접 그룹·인라인 date 필터와 중첩되지 않도록 처리됨). |
2562
+ | `fields?` | `SChipFilterGroup[]` | — | 필터 정의 목록. 묶을 것이 없어도 한 그룹으로 감싸 넘긴다 — `[{ fields: [...] }]`. 그룹은 rule로 함께 검증하거나 divider로 갈라 놓을 때 나눈다. |
2086
2563
  | `value?` | `SChipFilterValueMap` | — | 필터 값 맵 |
2087
- | `activeKeys?` | `string[]` | — | 노출 필터 key 목록. 지정하면 제어 컴포넌트로 동작 — "필터 추가"로 고른 필드를 이 배열에 직접 넣어줘야 칩이 나타난다(onActiveKeysChange에서 받은 keys로 갱신). 지정하지 않으면 필드별 defaultActive 값을 기준으로 컴포넌트가 내부 상태로 관리하는 비제어 방식으로 동작한다 |
2564
+ | `activeKeys?` | `string[]` | — | 노출 필터 key 목록. 지정하면 제어 컴포넌트로 동작 — "필터 추가"로 고른 필드를 이 배열에 직접 넣어줘야 칩이 나타난다(onActiveKeysChange에서 받은 keys로 갱신). 지정하지 않으면 fixed·required 필드만 노출된 상태로 시작해 컴포넌트가 내부 상태로 관리하는 비제어 방식으로 동작한다. fixed·required 필드는 이 배열에 없어도 항상 노출된다 |
2088
2565
  | `label?` | `string` | `'검색 필터'` | 좌측 태그 텍스트 |
2089
2566
  | `showLabel?` | `boolean` | `false` | 좌측 태그(label)·구분선 표시 여부 |
2090
- | `showAddButton?` | `boolean` | `true` | 필터 추가 버튼 표시 여부 |
2091
2567
  | `showReset?` | `boolean` | `true` | 검색 초기화 링크 표시 여부 |
2092
2568
  | `disabled?` | `boolean` | `false` | 바 비활성 상태 |
2093
2569
  | `className?` | `string` | — | |
@@ -2099,7 +2575,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2099
2575
  |-------|------|-------------|
2100
2576
  | `onValueChange` | `(value: SChipFilterValueMap) => void` | 전체 값 변경 — 편집 중인 값이 바뀔 때마다(선택할 때마다) 호출된다. 실제 검색 실행은 onSearch를 쓴다 |
2101
2577
  | `onFilterChange` | `(detail: SChipFilterChangeDetail) => void` | 개별 필터 값 변경 |
2102
- | `onSearch` | `(value: SChipFilterValueMap) => void` | 실제 검색을 실행할 시점 — 편집 팝오버의 "검색" 버튼을 누르거나 팝오버가 닫힐 때(바깥 클릭·Esc·다른 칩으로 전환 포함) 그 시점의 전체 값 맵과 함께 호출된다. 팝오버가 없는 필드(인라인 date 프리셋·custom, clearable ×, 검색 초기화)는 값이 바뀌는 즉시 호출된다. dirty 체크가 기본 적용되어 있어 — 마지막으로 실제 검색이 실행된 값 맵과 비교해 하나라도 달라진 게 없으면(예: 팝오버를 열었다 아무것도 안 바꾸고 닫는 경우) 호출되지 않는다. fields를 그룹으로 넘겼다면 rule을 만족하지 못한 그룹이 있는 동안엔 onSearch가 호출되지 않는다 — 해당 그룹의 경고 툴팁은 이 시점과 무관하게 값이 비어 있는 동안 항상 실시간으로 떠 있다(별도로 validate()를 호출할 필요 없음) |
2578
+ | `onSearch` | `(value: SChipFilterValueMap) => void` | 실제 검색을 실행할 시점 — 편집 팝오버의 "검색" 버튼을 누르거나 팝오버가 닫힐 때(바깥 클릭·Esc·다른 칩으로 전환 포함) 그 시점의 전체 값 맵과 함께 호출된다. 팝오버가 없는 필드(인라인 date 프리셋·custom, clearable ×, 검색 초기화)는 값이 바뀌는 즉시 호출된다. keyword 필터에서 Enter 로 키워드를 추가할 때도 그 즉시 호출된다 — 팝오버는 열린 채라 키워드를 이어서 더 넣을 수 있고, 넣을 때마다 조회가 갱신된다. dirty 체크가 기본 적용되어 있어 — 마지막으로 실제 검색이 실행된 값 맵과 비교해 하나라도 달라진 게 없으면(예: 팝오버를 열었다 아무것도 안 바꾸고 닫는 경우) 호출되지 않는다. fields를 그룹으로 넘겼다면 rule을 만족하지 못한 그룹이 있는 동안엔 onSearch가 호출되지 않는다 — 해당 그룹의 경고 툴팁은 이 시점과 무관하게 값이 비어 있는 동안 항상 실시간으로 떠 있다(별도로 validate()를 호출할 필요 없음) |
2103
2579
  | `onActiveKeysChange` | `(keys: string[]) => void` | 노출 필터 key 변경(칩 추가·제거) — 비제어 방식에서도 참고용으로 호출된다. activeKeys를 직접 제어할 때는 이 값을 그대로 activeKeys에 반영해야 한다 |
2104
2580
  | `onReset` | `() => void` | "검색 초기화" 클릭 — 모든 필드가 기본값(또는 null)으로 리셋된 뒤 호출된다 |
2105
2581
  | `onAddFilter` | `(key: string) => void` | "필터 추가" 목록에서 항목을 골랐을 때 — activeKeys를 직접 제어 중이면 이 콜백에서 (또는 onActiveKeysChange에서) key를 activeKeys에 추가해줘야 칩이 실제로 나타난다. activeKeys를 넘기지 않았다면(비제어) 별도 처리 없이도 컴포넌트가 알아서 칩을 노출한다 |
@@ -2110,61 +2586,345 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2110
2586
  |--------|------|-------------|
2111
2587
  | `open` | `(key: string) => void` | 특정 필터 편집 팝오버를 엽니다. |
2112
2588
  | `reset` | `() => void` | 모든 필터 값을 초기화합니다. |
2113
- | `validate` | `() => boolean` | fields를 그룹(SChipFilterGroup[])으로 넘겼을 때, 현재 값 기준으로 각 그룹의 rule을 만족하는지 반환합니다. 경고 툴팁은 이 호출과 무관하게 rule 위반 상태인 동안 항상 실시간으로 떠 있으므로, 이 메서드는 그 상태를 그대로 읽어오는 용도다(예: 검색 버튼을 눌러도 되는지 사전 확인) |
2589
+ | `validate` | `() => boolean` | 현재 값 기준으로 각 그룹의 rule을 만족하는지 반환합니다. rule을 준 그룹이 없으면 항상 true. 경고 툴팁은 이 호출과 무관하게 rule 위반 상태인 동안 항상 실시간으로 떠 있으므로, 이 메서드는 그 상태를 그대로 읽어오는 용도다(예: 검색 버튼을 눌러도 되는지 사전 확인) |
2114
2590
 
2115
- ## Dependencies
2591
+ ## Types
2116
2592
 
2117
- ### Depends on
2593
+ ### SChipFilterGroup
2118
2594
 
2119
- - [SDatePicker](../SDatePicker)
2120
- - [SDateRangePicker](../SDateRangePicker)
2121
- - [SGhostButton](../SGhostButton)
2122
- - [SIcon](../SIcon)
2123
- - [SRadio](../SRadio)
2124
- - [SRadioButton](../SRadioButton)
2125
- - [STag](../STag)
2126
- - [STextLink](../STextLink)
2127
- - [STooltip](../STooltip)
2595
+ ```ts
2596
+ /** fields를 이루는 단위. 묶을 것이 없어도 한 그룹으로 감싸 넘긴다 — `[{ fields: [...] }]` */
2597
+ export interface SChipFilterGroup {
2598
+ fields: SChipFilterField[];
2599
+ /** 지정하면 이 규칙으로 그룹을 검증한다. values가 바뀔 때마다 즉시 재평가되는 실시간 검증이라 —
2600
+ * 그룹이 rule을 만족하지 못하면 검색 시도 여부와 무관하게 그 즉시 그룹 중앙에 경고 툴팁이 뜬다.
2601
+ * 지정 안 하면 검증하지 않는다. */
2602
+ rule?: SChipFilterGroupRule;
2603
+ /** rule을 만족하지 않을 때 그룹 중앙에 띄울 툴팁 메시지. 지정 안 하면 rule 종류에 따른 기본 문구를 쓴다 */
2604
+ tooltipMessage?: string;
2605
+ /** 이 그룹 앞에 구분선을 넣을지. 검증(rule)과 구분선은 별개라 — 묶어서 검증만 하고 싶으면
2606
+ * 주지 않는다. 첫 그룹에는 앞에 가를 것이 없으므로 무시된다(showLabel의 구분선이 이미 있다) */
2607
+ divider?: boolean;
2608
+ }
2609
+ ```
2128
2610
 
2129
- ### Graph
2611
+ ### SChipFilterValueMap
2130
2612
 
2131
- ---
2613
+ ```ts
2614
+ export type SChipFilterValueMap = Record<string, SChipFilterValue>;
2615
+ ```
2132
2616
 
2133
- # SChipInput
2617
+ ### SChipFilterChangeDetail
2134
2618
 
2135
- > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
2619
+ ```ts
2620
+ export interface SChipFilterChangeDetail {
2621
+ key: string;
2622
+ value: SChipFilterValue;
2623
+ values: SChipFilterValueMap;
2624
+ }
2625
+ ```
2136
2626
 
2137
- ### SChipInput
2627
+ ### SChipFilterField
2138
2628
 
2139
- #### Props
2629
+ ```ts
2630
+ /** 필터 하나의 정의. type에 따라 쓸 수 있는 속성이 달라진다 —
2631
+ * options는 single·multi·keyword, presets·selectable·maxRange는 date·period,
2632
+ * render는 custom 에만 있다 */
2633
+ export type SChipFilterField =
2634
+ | SChipFilterSingleField
2635
+ | SChipFilterMultiField
2636
+ | SChipFilterKeywordField
2637
+ | SChipFilterDateField
2638
+ | SChipFilterPeriodField
2639
+ | SChipFilterCustomField;
2640
+ ```
2140
2641
 
2141
- | Prop | Type | Default | Description |
2142
- |------|------|---------|-------------|
2143
- | `values?` | `string[]` | `[]` | 칩 값 목록 |
2144
- | `errors?` | `boolean[] \| ((value: string) => boolean)` | `[]` | 칩별 에러 (배열 또는 판별 함수) |
2145
- | `disabledChips?` | `boolean[] \| ((value: string) => boolean)` | `[]` | 칩별 비활성 (배열 또는 판별 함수) |
2146
- | `size?` | `SChipInputSize` | `'sm'` | |
2147
- | `disabled?` | `boolean` | `false` | |
2148
- | `placeholder?` | `string` | `'태그 입력 (Enter로 등록 / 콤마로 구분 / 띄어쓰기 불가)'` | |
2149
- | `name?` | `string` | — | |
2150
- | `rules?` | `Rule[]` | — | |
2151
- | `error?` | `boolean` | — | |
2152
- | `useReset?` | `boolean` | `false` | 입력 초기화 버튼 표시 |
2153
- | `maxCount?` | `number` | — | 최대 칩 개수 |
2154
- | `metaPlacement?` | `SChipInputMetaPlacement` | `'end'` | 최대 개수·초기화 meta 위치 |
2155
- | `duplicateLabel?` | `string` | — | 중복 에러 메시지 항목명 |
2156
- | `suggestions?` | `string[]` | `[]` | 자동완성 후보 |
2157
- | `loadingSuggestions?` | `boolean` | `false` | |
2158
- | `recommendedItems?` | `string[]` | `[]` | 추천 항목 (입력값 없을 때) |
2159
- | `loadingRecommendedItems?` | `boolean` | `false` | |
2160
- | `dropdownMinWidth?` | `number \| string` | `200` | 드롭다운 최소 너비 (숫자=px) |
2161
- | `label?` | `string` | — | |
2162
- | `labelWidth?` | `number \| string` | — | |
2163
- | `hint?` | `string` | — | |
2164
- | `errorMessage?` | `string` | — | |
2165
- | `width?` | `number \| string` | — | |
2166
- | `status?` | `SFieldStatus` | — | |
2167
- | `icon?` | `SIconName` | — | |
2642
+ ### SChipFilterGroupRule
2643
+
2644
+ ```ts
2645
+ /** 필터 그룹 검증 규칙 */
2646
+ export type SChipFilterGroupRule =
2647
+ | { type: 'requireKey'; key: string }
2648
+ | { type: 'requireAll' }
2649
+ | { type: 'requireAny'; dataGroupName?: string };
2650
+ ```
2651
+
2652
+ ### SChipFilterValue
2653
+
2654
+ ```ts
2655
+ export type SChipFilterValue =
2656
+ | SChipFilterOptionValue
2657
+ | SChipFilterOptionValue[]
2658
+ | SDateRangeValue
2659
+ | SChipFilterKeywordValue
2660
+ | SChipFilterPeriodValue
2661
+ | SChipFilterCustomValue
2662
+ | null
2663
+ | undefined;
2664
+ ```
2665
+
2666
+ ### SChipFilterSingleField
2667
+
2668
+ ```ts
2669
+ /** 후보 하나를 고른다 */
2670
+ export interface SChipFilterSingleField extends SChipFilterOptionsField {
2671
+ type: 'single';
2672
+ }
2673
+ ```
2674
+
2675
+ ### SChipFilterMultiField
2676
+
2677
+ ```ts
2678
+ /** 후보 여럿을 고른다 */
2679
+ export interface SChipFilterMultiField extends SChipFilterOptionsField {
2680
+ type: 'multi';
2681
+ }
2682
+ ```
2683
+
2684
+ ### SChipFilterKeywordField
2685
+
2686
+ ```ts
2687
+ /** 키워드를 입력해 누적한다. options는 입력 중 후보로만 뜬다 */
2688
+ export interface SChipFilterKeywordField extends SChipFilterOptionsField {
2689
+ type: 'keyword';
2690
+ /** 입력 placeholder */
2691
+ placeholder?: string;
2692
+ /** 입력 방식. 기본 'tag' — Enter 로 하나씩 추가한다.
2693
+ * 'csv' 는 쉼표도 구분자로 인정해, 쉼표를 치거나 쉼표가 섞인 텍스트를 붙여넣으면 그 자리에서
2694
+ * 여러 개로 쪼개져 목록에 쌓인다(엑셀에서 복사한 코드 목록을 한 번에 넣는 용도).
2695
+ * 쌓이는 목록도 값 형태도 'tag' 와 같다 — 구분자만 늘어난다. */
2696
+ input?: SChipFilterKeywordInput;
2697
+ /** 검색조건(포함/일치) 토글 표시 여부 */
2698
+ matchModes?: boolean;
2699
+ /** matchModes 활성 시 "미포함"까지 포함해 3개(포함/일치/미포함)로 노출할지.
2700
+ * 기본 false — 2개(포함/일치)만 */
2701
+ excludeMode?: boolean;
2702
+ }
2703
+ ```
2704
+
2705
+ ### SChipFilterDateField
2706
+
2707
+ ```ts
2708
+ /** 날짜 하나 또는 기간을 고른다 */
2709
+ export interface SChipFilterDateField extends SChipFilterPresetsField {
2710
+ type: 'date';
2711
+ /** presets 없이 단일 캘린더 트리거로 동작할 때의 placeholder */
2712
+ placeholder?: string;
2713
+ /** presets를 필터 바에 세그먼트 라디오로 펼쳐 놓는다(팝오버 없음).
2714
+ * 기본 false — 칩 클릭 시 팝오버 안에 세로 라디오 목록(+사용자 지정 선택 시 기간 피커) */
2715
+ radioButton?: boolean;
2716
+ }
2717
+ ```
2718
+
2719
+ ### SChipFilterPeriodField
2720
+
2721
+ ```ts
2722
+ /** 집계 단위(일·월·분기·반기·연)와 그 단위의 값을 함께 고른다 */
2723
+ export interface SChipFilterPeriodField extends SChipFilterPresetsField {
2724
+ type: 'period';
2725
+ }
2726
+ ```
2727
+
2728
+ ### SChipFilterCustomField
2729
+
2730
+ ```ts
2731
+ /** 칩+팝오버를 거치지 않고 바에 놓을 노드를 앱이 직접 그린다 */
2732
+ export interface SChipFilterCustomField extends SChipFilterFieldBase {
2733
+ type: 'custom';
2734
+ /** 필터 바의 이 필드 자리에 놓일 노드를 직접 그린다. 반환한 노드가 그대로 바에 노출된다 —
2735
+ * SSelect를 그대로 놓거나 SInput을 바로 노출하는 식으로 렌더 방식을 자유롭게 구성한다. */
2736
+ render?: (ctx: {
2737
+ value: SChipFilterValue;
2738
+ disabled?: boolean;
2739
+ /** 속한 그룹이 rule을 위반하는 동안 true — 직접 그린 노드에도 경고 표시를 맞추라는 신호다 */
2740
+ warning?: boolean;
2741
+ onValueChange: (value: SChipFilterValue) => void;
2742
+ }) => ReactNode;
2743
+ }
2744
+ ```
2745
+
2746
+ ### SChipFilterOptionValue
2747
+
2748
+ ```ts
2749
+ export type SChipFilterOptionValue = string | number;
2750
+ ```
2751
+
2752
+ ### SChipFilterKeywordValue
2753
+
2754
+ ```ts
2755
+ /** keyword 필드에서 matchModes 활성 시 사용하는 값 형태 — 입력해 추가한 키워드 목록 */
2756
+ export interface SChipFilterKeywordValue {
2757
+ keywords: string[];
2758
+ mode: SChipFilterMatchMode;
2759
+ }
2760
+ ```
2761
+
2762
+ ### SChipFilterPeriodValue
2763
+
2764
+ ```ts
2765
+ /** period 필드 값 — 선택 단위(unit)와 그 단위의 입력값(value)을 함께 보관한다 */
2766
+ export interface SChipFilterPeriodValue {
2767
+ unit: SChipFilterPeriodUnit;
2768
+ value?: string | number | SDateRangeValue | null;
2769
+ }
2770
+ ```
2771
+
2772
+ ### SChipFilterCustomValue
2773
+
2774
+ ```ts
2775
+ /** custom 필드가 자유롭게 담는 값. 형태를 강제하지 않는다 — render에서 직접 정의한 그대로 읽고 쓴다 */
2776
+ export type SChipFilterCustomValue = Record<string, unknown>;
2777
+ ```
2778
+
2779
+ ### SChipFilterOptionsField
2780
+
2781
+ ```ts
2782
+ /** 후보 목록에서 고르는 필터 — single·multi·keyword */
2783
+ export interface SChipFilterOptionsField extends SChipFilterFieldBase {
2784
+ /** 고를 수 있는 후보 목록 */
2785
+ options?: SChipFilterOption[];
2786
+ }
2787
+ ```
2788
+
2789
+ ### SChipFilterKeywordInput
2790
+
2791
+ ```ts
2792
+ /** keyword 필드의 입력 방식 — 무엇을 키워드 하나의 끝으로 볼지 */
2793
+ export type SChipFilterKeywordInput = 'tag' | 'csv';
2794
+ ```
2795
+
2796
+ ### SChipFilterPresetsField
2797
+
2798
+ ```ts
2799
+ /** 프리셋으로 기간을 고르는 필터 — date·period */
2800
+ export interface SChipFilterPresetsField extends SChipFilterFieldBase {
2801
+ /** 프리셋 라디오 목록. date에서 지정하지 않으면 단일 캘린더 트리거로 동작하고,
2802
+ * period에서 지정하지 않으면 일별·월별·분기별·반기별·연도별·사용자 지정 기본 목록을 쓴다 */
2803
+ presets?: SChipFilterDatePreset[];
2804
+ /** 선택 가능 범위 */
2805
+ selectable?: [string, string];
2806
+ /** "사용자 지정" 프리셋으로 기간을 고를 때의 최대 선택 일수 */
2807
+ maxRange?: number;
2808
+ }
2809
+ ```
2810
+
2811
+ ### SChipFilterFieldBase
2812
+
2813
+ ```ts
2814
+ /** 타입과 무관하게 모든 필터가 갖는 속성 */
2815
+ export interface SChipFilterFieldBase {
2816
+ /** 필터 식별자 */
2817
+ key: string;
2818
+ /** 칩에 표시할 레이블 */
2819
+ label: string;
2820
+ /** 필수 필터 표시. true면 값이 비어 있을 때 기본값(defaultValue 또는 타입별 내장 기본값)이
2821
+ * 자동으로 채워지고, clearable은 현재 값이 기본값과 같을 땐 숨겨지며 클릭 시 기본값으로 되돌아간다 */
2822
+ required?: boolean;
2823
+ /** 고정 필터. true면 activeKeys와 무관하게 항상 노출되고 "필터 추가" 목록에는 나타나지 않는다.
2824
+ * required도 같은 효과를 낸다 — 처음부터 바에 보이는 것은 fixed이거나 required인 필드뿐이고,
2825
+ * 나머지는 전부 "필터 추가"에서 골라야 나타난다 */
2826
+ fixed?: boolean;
2827
+ /** 초기값 및 clearable 클릭 시 되돌아갈 값. required 여부와 무관하게 적용된다 — 값이 비어 있으면
2828
+ * 마운트(또는 "필터 추가"로 활성화) 시 이 값이 자동으로 채워진다. required인데 지정하지 않으면
2829
+ * 타입별 내장 기본값(single: 첫 번째 옵션, date: 오늘 날짜)을 대신 쓴다 */
2830
+ defaultValue?: SChipFilterValue;
2831
+ /** 이 필터만 비활성. 바에 남아 있되 팝오버가 열리지 않고 clearable도 눌리지 않는다.
2832
+ * 바 전체를 잠그려면 SChipFilterProps.disabled를 쓴다 — 둘은 OR로 합쳐진다 */
2833
+ disabled?: boolean;
2834
+ }
2835
+ ```
2836
+
2837
+ ### SChipFilterMatchMode
2838
+
2839
+ ```ts
2840
+ export type SChipFilterMatchMode = 'contains' | 'exact' | 'excludes';
2841
+ ```
2842
+
2843
+ ### SChipFilterPeriodUnit
2844
+
2845
+ ```ts
2846
+ export type SChipFilterPeriodUnit = 'day' | 'month' | 'quarter' | 'half' | 'year' | 'custom';
2847
+ ```
2848
+
2849
+ ### SChipFilterOption
2850
+
2851
+ ```ts
2852
+ export interface SChipFilterOption {
2853
+ value: SChipFilterOptionValue;
2854
+ label: string;
2855
+ disabled?: boolean;
2856
+ }
2857
+ ```
2858
+
2859
+ ### SChipFilterDatePreset
2860
+
2861
+ ```ts
2862
+ /** date/period 필드의 프리셋 라디오 항목 (오늘/지난 7일/일별/월별/사용자 지정 등) */
2863
+ export interface SChipFilterDatePreset {
2864
+ /** 프리셋 식별자 */
2865
+ value: string;
2866
+ /** 라벨 */
2867
+ label: string;
2868
+ /** true면 "사용자 지정" — 선택 시 날짜/기간 피커가 추가로 노출된다. resolve는 무시된다. */
2869
+ custom?: boolean;
2870
+ /** custom이 아닐 때 실제 값을 계산한다. 단일 날짜(string) 또는 기간([start,end]) 모두 가능 */
2871
+ resolve?: () => string | SDateRangeValue;
2872
+ }
2873
+ ```
2874
+
2875
+ ## Dependencies
2876
+
2877
+ ### Depends on
2878
+
2879
+ - [SDatePicker](../SDatePicker)
2880
+ - [SDateRangePicker](../SDateRangePicker)
2881
+ - [SGhostButton](../SGhostButton)
2882
+ - [SIcon](../SIcon)
2883
+ - [SRadio](../SRadio)
2884
+ - [SRadioButton](../SRadioButton)
2885
+ - [STag](../STag)
2886
+ - [STextLink](../STextLink)
2887
+ - [STooltip](../STooltip)
2888
+
2889
+ ### Graph
2890
+
2891
+ ---
2892
+
2893
+ # SChipInput
2894
+
2895
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
2896
+
2897
+ ### SChipInput
2898
+
2899
+ #### Props
2900
+
2901
+ | Prop | Type | Default | Description |
2902
+ |------|------|---------|-------------|
2903
+ | `values?` | `string[]` | `[]` | 칩 값 목록 |
2904
+ | `errors?` | `boolean[] \| ((value: string) => boolean)` | `[]` | 칩별 에러 (배열 또는 판별 함수) |
2905
+ | `disabledChips?` | `boolean[] \| ((value: string) => boolean)` | `[]` | 칩별 비활성 (배열 또는 판별 함수) |
2906
+ | `size?` | `SChipInputSize` | `'sm'` | |
2907
+ | `disabled?` | `boolean` | `false` | |
2908
+ | `placeholder?` | `string` | `'태그 입력 (Enter로 등록 / 콤마로 구분 / 띄어쓰기 불가)'` | |
2909
+ | `name?` | `string` | — | |
2910
+ | `rules?` | `Rule[]` | — | |
2911
+ | `error?` | `boolean` | — | |
2912
+ | `useReset?` | `boolean` | `false` | 입력 초기화 버튼 표시 |
2913
+ | `maxCount?` | `number` | — | 최대 칩 개수 |
2914
+ | `metaPlacement?` | `SChipInputMetaPlacement` | `'end'` | 최대 개수·초기화 meta 위치 |
2915
+ | `duplicateLabel?` | `string` | — | 중복 에러 메시지 항목명 |
2916
+ | `suggestions?` | `string[]` | `[]` | 자동완성 후보 |
2917
+ | `loadingSuggestions?` | `boolean` | `false` | |
2918
+ | `recommendedItems?` | `string[]` | `[]` | 추천 항목 (입력값 없을 때) |
2919
+ | `loadingRecommendedItems?` | `boolean` | `false` | |
2920
+ | `dropdownMinWidth?` | `number \| string` | `200` | 드롭다운 최소 너비 (숫자=px) |
2921
+ | `label?` | `string` | — | |
2922
+ | `labelWidth?` | `number \| string` | — | |
2923
+ | `hint?` | `string` | — | |
2924
+ | `errorMessage?` | `string` | — | |
2925
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
2926
+ | `status?` | `SFieldStatus` | — | |
2927
+ | `icon?` | `SIconName` | — | |
2168
2928
  | `iconColor?` | `SColor` | — | |
2169
2929
  | `labelTooltip?` | `string` | — | |
2170
2930
  | `labelTooltipProps?` | `Partial<STooltipProps>` | — | |
@@ -2186,6 +2946,20 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2186
2946
  |--------|------|-------------|
2187
2947
  | `focus` | `() => void` | 입력 필드에 포커스를 이동합니다. |
2188
2948
 
2949
+ ## Types
2950
+
2951
+ ### SChipInputSize
2952
+
2953
+ ```ts
2954
+ export type SChipInputSize = SFieldSize;
2955
+ ```
2956
+
2957
+ ### SChipInputMetaPlacement
2958
+
2959
+ ```ts
2960
+ export type SChipInputMetaPlacement = 'end' | 'inline';
2961
+ ```
2962
+
2189
2963
  ## Dependencies
2190
2964
 
2191
2965
  ### Depends on
@@ -2219,6 +2993,14 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2219
2993
  | `className?` | `string` | — | |
2220
2994
  | `style?` | `CSSProperties` | — | |
2221
2995
 
2996
+ ## Types
2997
+
2998
+ ### SCircleProgressType
2999
+
3000
+ ```ts
3001
+ export type SCircleProgressType = 'primary' | 'inverse' | 'error' | 'complete' | 'neutral';
3002
+ ```
3003
+
2222
3004
  ## Dependencies
2223
3005
 
2224
3006
  ### Used by
@@ -2270,6 +3052,22 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2270
3052
  | `onCancel` | `() => void` | |
2271
3053
  | `onClose` | `() => void` | 닫기(X) 버튼 클릭 (sdClose) |
2272
3054
 
3055
+ ## Types
3056
+
3057
+ ### SConfirmModalType
3058
+
3059
+ ```ts
3060
+ export type SConfirmModalType = 'positive' | 'negative' | 'default';
3061
+ ```
3062
+
3063
+ ### ConfirmModalMainButton
3064
+
3065
+ ```ts
3066
+ /** 확인 버튼 프리셋 (sd-confirm-modal ConfirmModalMainButton) */
3067
+ export type ConfirmModalMainButton =
3068
+ 'primary_md' | 'primary_outline_md' | 'danger_md' | 'danger_outline_md' | 'neutral_outline_md';
3069
+ ```
3070
+
2273
3071
  ## Dependencies
2274
3072
 
2275
3073
  ### Used by
@@ -2291,18 +3089,40 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2291
3089
 
2292
3090
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
2293
3091
 
3092
+ ### SDatePickerMonthListbox
3093
+
3094
+ #### Props
3095
+
3096
+ | Prop | Type | Default | Description |
3097
+ |------|------|---------|-------------|
3098
+ | `value?` | `string \| null` | — | 선택 월 (YYYY-MM) |
3099
+ | `anchorYear?` | `number` | — | 연도 리스트가 처음 열릴 때 중앙에 둘 연도. 기본값은 올해 |
3100
+ | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
3101
+ | `className?` | `string` | — | |
3102
+ | `style?` | `CSSProperties` | — | |
3103
+
3104
+ #### Events
3105
+
3106
+ | Event | Type | Description |
3107
+ |-------|------|-------------|
3108
+ | `onValueChange` | `(value: string) => void` | |
3109
+ | `onMonthSelect` | `(value: string) => void` | 월 컬럼에서 월을 선택했을 때 호출 |
3110
+
2294
3111
  ### SDatePicker
2295
3112
 
2296
3113
  #### Props
2297
3114
 
2298
3115
  | Prop | Type | Default | Description |
2299
3116
  |------|------|---------|-------------|
2300
- | `value?` | `string \| null` | — | 선택 날짜 (YYYY-MM-DD) |
3117
+ | `value?` | `string \| null` | — | 선택 값 (date: YYYY-MM-DD, month: YYYY-MM, year: YYYY) |
3118
+ | `mode?` | `SDatePickerMode` | `'date'` | 선택 모드 |
2301
3119
  | `size?` | `SDatePickerSize` | `'sm'` | |
2302
- | `placeholder?` | `string` | `'YYYY-MM-DD'` | |
3120
+ | `placeholder?` | `string` | — | |
2303
3121
  | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
2304
3122
  | `disabled?` | `boolean` | `false` | |
2305
- | `width?` | `number \| string` | — | |
3123
+ | `clearable?` | `boolean` | `false` | 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다. **필수 입력 필드에는 켜지 않는다** — 지우면 다시 고르기 전까지 폼이 통과하지 못한다. `STimePicker` · `STimeRangePicker` · `SSelect` 의 `clearable` 과 같은 규칙이다. |
3124
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
3125
+ | `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 — 폭 등급 · 숫자=px · CSS 길이. 지정하지 않으면 값 길이(`YYYY-MM-DD`)에 맞춘 내장 상한(`size='md'` → `lg`, `'sm'` → `md`)이 걸려, `width="100%"` 를 받아도 행 전체로 늘어나지 않는다. 팝오버처럼 폭이 이미 좁게 정해진 자리에서 그 폭을 그대로 채워야 하면 `width="100%"` 와 함께 `maxWidth="100%"` 를 준다. `0` 은 상한을 아예 걸지 않는다 — 부모보다 넓어지는 것까지 허용해야 할 때만 쓴다. |
2306
3126
  | `name?` | `string` | — | |
2307
3127
  | `rules?` | `Rule[]` | — | |
2308
3128
  | `status?` | `SFieldStatus` | — | |
@@ -2324,9 +3144,41 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2324
3144
 
2325
3145
  | Event | Type | Description |
2326
3146
  |-------|------|-------------|
2327
- | `onValueChange` | `(date: string) => void` | 선택 변경 (sdUpdate) |
3147
+ | `onValueChange` | `(value: string \| null) => void` | 선택 변경 (sdUpdate). `clearable` 로 지우면 `null` 이 온다 |
2328
3148
  | `onViewChange` | `(view: { year: number; month: number }) => void` | 보이는 연·월 변경 (sdViewChange) |
2329
3149
 
3150
+ ### SDatePickerYearListbox
3151
+
3152
+ #### Props
3153
+
3154
+ | Prop | Type | Default | Description |
3155
+ |------|------|---------|-------------|
3156
+ | `value?` | `string \| null` | — | 선택 연도 (YYYY) |
3157
+ | `anchorYear?` | `number` | — | 리스트가 처음 열릴 때 중앙에 둘 연도. 기본값은 올해 |
3158
+ | `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
3159
+ | `className?` | `string` | — | |
3160
+ | `style?` | `CSSProperties` | — | |
3161
+
3162
+ #### Events
3163
+
3164
+ | Event | Type | Description |
3165
+ |-------|------|-------------|
3166
+ | `onValueChange` | `(year: string) => void` | |
3167
+
3168
+ ## Types
3169
+
3170
+ ### SDatePickerMode
3171
+
3172
+ ```ts
3173
+ export type SDatePickerMode = 'date' | 'month' | 'year';
3174
+ ```
3175
+
3176
+ ### SDatePickerSize
3177
+
3178
+ ```ts
3179
+ export type SDatePickerSize = SFieldSize;
3180
+ ```
3181
+
2330
3182
  ## Dependencies
2331
3183
 
2332
3184
  ### Used by
@@ -2338,6 +3190,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2338
3190
 
2339
3191
  - [SCalendar](../SCalendar)
2340
3192
  - [SField](../SField)
3193
+ - [SGhostButton](../SGhostButton)
2341
3194
  - [SIcon](../SIcon)
2342
3195
 
2343
3196
  ### Graph
@@ -2361,7 +3214,8 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2361
3214
  | `maxRange?` | `number` | — | 최대 선택 일수 |
2362
3215
  | `useTimePicker?` | `boolean` | `false` | 시간(시:분) 선택 푸터 사용 여부. true이면 value는 "YYYY-MM-DD HH:mm" 형식이 되고, 캘린더 하단에 시작·종료 시간 입력이 표시된다. |
2363
3216
  | `disabled?` | `boolean` | `false` | |
2364
- | `width?` | `number \| string` | — | |
3217
+ | `clearable?` | `boolean` | `false` | 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다. **필수 입력 필드에는 켜지 않는다** — 지우면 다시 고르기 전까지 폼이 통과하지 못한다. `SDatePicker` · `STimePicker` · `SSelect` 의 `clearable` 과 같은 규칙이다. |
3218
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
2365
3219
  | `name?` | `string` | — | |
2366
3220
  | `rules?` | `Rule[]` | — | |
2367
3221
  | `status?` | `SFieldStatus` | — | |
@@ -2383,7 +3237,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2383
3237
 
2384
3238
  | Event | Type | Description |
2385
3239
  |-------|------|-------------|
2386
- | `onValueChange` | `(range: SDateRangeValue) => void` | 선택 변경 (sdUpdate) — [start, end] |
3240
+ | `onValueChange` | `(range: SDateRangeValue) => void` | 선택 변경 (sdUpdate) — [start, end]. `clearable` 로 지우면 `null` 이 온다 |
2387
3241
  | `onViewChange` | `(view: { year: number; month: number }) => void` | 보이는 연·월 변경 (sdViewChange) |
2388
3242
 
2389
3243
  ### SRangeCalendar
@@ -2405,6 +3259,20 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2405
3259
  | `onPendingStartChange` | `(start: string \| null) => void` | 시작일만 선택된(종료일 대기) 상태를 상위로 전달 — 트리거에 `start ~` 프리뷰 표시용 |
2406
3260
  | `onViewChange` | `(view: { year: number; month: number }) => void` | |
2407
3261
 
3262
+ ## Types
3263
+
3264
+ ### SDateRangeValue
3265
+
3266
+ ```ts
3267
+ export type SDateRangeValue = [string, string] | null;
3268
+ ```
3269
+
3270
+ ### SDateRangePickerSize
3271
+
3272
+ ```ts
3273
+ export type SDateRangePickerSize = SFieldSize;
3274
+ ```
3275
+
2408
3276
  ## Dependencies
2409
3277
 
2410
3278
  ### Used by
@@ -2441,6 +3309,8 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2441
3309
 
2442
3310
  - [SCalendar](../SCalendar)
2443
3311
  - [SDateRangePicker](../SDateRangePicker)
3312
+ - [SList](../SList)
3313
+ - [STable](../STable)
2444
3314
  - [STableBar](../STableBar)
2445
3315
 
2446
3316
  ### Graph
@@ -2461,6 +3331,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2461
3331
  | `supportingText?` | `SDraggableItemSlot` | — | 제목을 보조하는 텍스트 |
2462
3332
  | `supportingTextPosition?` | `SDraggableItemSupportingTextPosition` | `'right'` | 보조 텍스트 위치 |
2463
3333
  | `trailing?` | `SDraggableItemSlot` | — | 타이틀 뒤에 표시할 태그/콘텐츠 |
3334
+ | `depth?` | `number` | `1` | 중첩 단계. `SListItem`·`SExpansionItem` 과 같은 들여쓰기 간격을 사용한다 |
2464
3335
  | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
2465
3336
  | `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
2466
3337
  | `selected?` | `boolean` | `false` | 선택 상태 여부 |
@@ -2478,6 +3349,42 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2478
3349
  |-------|------|-------------|
2479
3350
  | `onDragHandleMouseDown` | `(event: MouseEvent<HTMLDivElement>) => void` | 드래그 핸들 mouse down 이벤트 |
2480
3351
 
3352
+ ## Types
3353
+
3354
+ ### SDraggableItemSlot
3355
+
3356
+ ```ts
3357
+ export type SDraggableItemSlot = ReactNode | SDraggableItemRenderProp;
3358
+ ```
3359
+
3360
+ ### SDraggableItemSupportingTextPosition
3361
+
3362
+ ```ts
3363
+ export type SDraggableItemSupportingTextPosition = 'right' | 'bottom';
3364
+ ```
3365
+
3366
+ ### SDraggableItemSize
3367
+
3368
+ ```ts
3369
+ export type SDraggableItemSize = 'sm' | 'md';
3370
+ ```
3371
+
3372
+ ### SDraggableItemRenderProp
3373
+
3374
+ ```ts
3375
+ export type SDraggableItemRenderProp = (state: SDraggableItemRenderState) => ReactNode;
3376
+ ```
3377
+
3378
+ ### SDraggableItemRenderState
3379
+
3380
+ ```ts
3381
+ export interface SDraggableItemRenderState {
3382
+ hovered: boolean;
3383
+ dragging: boolean;
3384
+ disabled: boolean;
3385
+ }
3386
+ ```
3387
+
2481
3388
  ## Dependencies
2482
3389
 
2483
3390
  ### Depends on
@@ -2518,6 +3425,10 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2518
3425
  | `selectedKey?` | `string` | — | 외부에서 제어하는 selected 아이템 key |
2519
3426
  | `defaultSelectedKey?` | `string` | — | 초기 selected 아이템 key |
2520
3427
  | `getDisabled?` | `(item: T, index: number) => boolean` | — | disabled 아이템은 선택 및 드래그에서 제외 |
3428
+ | `getDepth?` | `(item: T, index: number) => number` | — | 아이템의 중첩 단계. 기본값은 1 |
3429
+ | `setDepth?` | `(item: T, depth: number) => T` | — | depth 변경이 필요한 드롭에서 다음 아이템을 만드는 함수 |
3430
+ | `getCanHaveChildren?` | `(item: T, index: number) => boolean` | — | 하위 depth 를 가질 수 있는 아이템인지 판정 |
3431
+ | `maxDepth?` | `number` | `3` | 허용할 최대 depth |
2521
3432
  | `listId?` | `string` | — | Provider 안에서 사용할 리스트 식별자 |
2522
3433
  | `group?` | `string` | — | 같은 group 값을 가진 리스트끼리 드래그 이벤트를 공유 |
2523
3434
 
@@ -2536,6 +3447,31 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2536
3447
  |------|------|---------|-------------|
2537
3448
  | `children` | `ReactNode` | — | |
2538
3449
 
3450
+ ## Types
3451
+
3452
+ ### SDraggableGroupMoveEvent
3453
+
3454
+ ```ts
3455
+ export interface SDraggableGroupMoveEvent {
3456
+ group: string;
3457
+ itemKey: string;
3458
+ fromListId: string;
3459
+ fromIndex: number;
3460
+ toListId: string;
3461
+ toIndex: number;
3462
+ }
3463
+ ```
3464
+
3465
+ ### SDraggableListRenderState
3466
+
3467
+ ```ts
3468
+ export interface SDraggableListRenderState {
3469
+ onDragHandleMouseDown: (event: MouseEvent<HTMLDivElement>) => void;
3470
+ selected: boolean;
3471
+ depth: number;
3472
+ }
3473
+ ```
3474
+
2539
3475
  ## Dependencies
2540
3476
 
2541
3477
  ### Depends on
@@ -2577,6 +3513,14 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2577
3513
  | `onClose` | `() => void` | 닫기(X) 버튼 클릭 |
2578
3514
  | `onWidthChange` | `(width: number) => void` | 너비가 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 |
2579
3515
 
3516
+ ## Types
3517
+
3518
+ ### SDrawerButton
3519
+
3520
+ ```ts
3521
+ export type SDrawerButton = SFooterButton;
3522
+ ```
3523
+
2580
3524
  ## Dependencies
2581
3525
 
2582
3526
  ### Depends on
@@ -2623,6 +3567,25 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2623
3567
  | `open` | `() => void` | 드롭다운 열기 (sdOpen) |
2624
3568
  | `close` | `() => void` | 드롭다운 닫기 (sdClose) |
2625
3569
 
3570
+ ## Types
3571
+
3572
+ ### SDropdownButtonSize
3573
+
3574
+ ```ts
3575
+ export type SDropdownButtonSize = 'xs' | 'sm' | 'md';
3576
+ ```
3577
+
3578
+ ### SDropdownButtonItem
3579
+
3580
+ ```ts
3581
+ export interface SDropdownButtonItem {
3582
+ value: string | number;
3583
+ label: string;
3584
+ icon?: SIconName;
3585
+ disabled?: boolean;
3586
+ }
3587
+ ```
3588
+
2626
3589
  ## Dependencies
2627
3590
 
2628
3591
  ### Depends on
@@ -2634,6 +3597,230 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2634
3597
 
2635
3598
  ---
2636
3599
 
3600
+ # SEditor
3601
+
3602
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
3603
+
3604
+ ### EditorBody
3605
+
3606
+ #### Props
3607
+
3608
+ | Prop | Type | Default | Description |
3609
+ |------|------|---------|-------------|
3610
+ | `api` | `TiptapApi` | — | 다 불러온 tiptap — 이 컴포넌트는 준비된 뒤에만 마운트된다 |
3611
+ | `value?` | `string` | — | |
3612
+ | `defaultValue?` | `string` | — | |
3613
+ | `htmlRef` | `RefObject<string>` | — | 지금 화면에 있는 HTML. 껍데기(SEditor)가 규칙 검증·폼 제출에 쓴다 |
3614
+ | `placeholder` | `string` | — | |
3615
+ | `typography` | `boolean` | — | |
3616
+ | `editable` | `boolean` | — | |
3617
+ | `disabled` | `boolean` | — | |
3618
+ | `minHeight?` | `number \| string` | — | |
3619
+ | `maxHeight?` | `number \| string` | — | |
3620
+ | `toolbar` | `SEditorToolbarItem[] \| false` | — | |
3621
+ | `bubbleMenu` | `SEditorToolbarItem[] \| false` | — | 선택 영역 위에 뜨는 서식 판. `false` 면 그리지 않는다 |
3622
+ | `fontSizes` | `number[]` | — | |
3623
+ | `colors` | `SEditorColorOption[]` | — | |
3624
+ | `highlights` | `SEditorColorOption[]` | — | |
3625
+ | `editorClass?` | `string` | — | |
3626
+ | `editorStyle?` | `CSSProperties` | — | |
3627
+
3628
+ #### Events
3629
+
3630
+ | Event | Type | Description |
3631
+ |-------|------|-------------|
3632
+ | `onInput` | `(html: string) => void` | 사용자가 고쳐서 값이 바뀌었다 (setHTML·clear 같은 프로그램 조작은 제외) |
3633
+ | `onFocusChange` | `(focused: boolean) => void` | |
3634
+ | `onReady` | `() => void` | 에디터 인스턴스가 생겼다 — 껍데기가 밀린 focus() 를 흘려보낸다 |
3635
+ | `onImageUpload` | `(file: File) => Promise<string>` | |
3636
+
3637
+ ### EditorToolbarBar
3638
+
3639
+ #### Props
3640
+
3641
+ | Prop | Type | Default | Description |
3642
+ |------|------|---------|-------------|
3643
+ | `state` | `EditorToolbarState` | — | 눌림 표시 — 에디터가 아직 없으면 `IDLE_TOOLBAR_STATE` |
3644
+ | `editor` | `Editor \| null` | — | 없으면 버튼을 눌러도 아무 일도 하지 않는다 (그때는 disabled 로 함께 잠근다) |
3645
+ | `items` | `SEditorToolbarItem[]` | — | |
3646
+ | `fontSizes` | `readonly number[]` | — | |
3647
+ | `colors` | `SEditorColorOption[]` | — | |
3648
+ | `highlights` | `SEditorColorOption[]` | — | |
3649
+ | `disabled` | `boolean` | — | 편집 불가(비활성·읽기전용·엔진 로딩 중) — 모든 버튼을 잠근다 |
3650
+ | `variant?` | `'bar' \| 'bubble'` | `'bar'` | `'bar'` 는 편집 영역 위에 붙는 막대, `'bubble'` 은 선택 영역 위에 뜨는 판이다. 그리는 버튼은 같고 담는 상자와 줄바꿈만 다르다. |
3651
+
3652
+ #### Events
3653
+
3654
+ | Event | Type | Description |
3655
+ |-------|------|-------------|
3656
+ | `onImageUpload` | `(file: File) => Promise<string>` | 이미지 업로드 훅. 없으면 `image` 항목을 그리지 않는다 |
3657
+
3658
+ ### SEditor
3659
+
3660
+ #### Props
3661
+
3662
+ | Prop | Type | Default | Description |
3663
+ |------|------|---------|-------------|
3664
+ | `value?` | `string` | — | 값 (제어) — HTML 문자열 |
3665
+ | `defaultValue?` | `string` | — | 초기값 (비제어) — HTML 문자열 |
3666
+ | `placeholder?` | `string` | `'내용을 입력해 주세요.'` | 빈 문서에 보일 안내 문구 |
3667
+ | `minHeight?` | `number \| string` | `200` | 편집 영역 최소 높이 (숫자=px) |
3668
+ | `maxHeight?` | `number \| string` | — | 편집 영역 최대 높이 (숫자=px). 넘으면 편집 영역 안에서만 스크롤한다 |
3669
+ | `toolbar?` | `SEditorToolbarItem[] \| false` | `SEDITOR_DEFAULT_TOOLBAR` | 툴바 구성. `false` 면 툴바 없이 본문만 (읽기 화면·간단 메모용) |
3670
+ | `bubbleMenu?` | `SEditorToolbarItem[] \| false` | `SEDITOR_DEFAULT_BUBBLE_MENU` | 글을 선택했을 때 그 위에 뜨는 서식 판의 구성. `false` 면 뜨지 않는다. 읽기 전용·비활성일 때는 어차피 뜨지 않는다. 좁은 칸에 놓인 에디터라면 판이 필드 밖으로 넘칠 수 있으니 항목을 줄이거나 `false` 로 끈다. |
3671
+ | `fontSizes?` | `number[]` | `[...SEDITOR_FONT_SIZES]` | 글자 크기 드롭다운 선택지 (px) |
3672
+ | `colors?` | `SEditorColorOption[]` | `SEDITOR_DEFAULT_COLORS` | 글자색 팔레트 |
3673
+ | `highlights?` | `SEditorColorOption[]` | `SEDITOR_DEFAULT_HIGHLIGHTS` | 형광펜(배경색) 팔레트 |
3674
+ | `typography?` | `boolean` | `false` | 따옴표·하이픈·화살표 자동 치환 (`"` → `“”`, `--` → `—`, `->` → `→`). 상품 코드·규격 문자열이 입력한 그대로 남아야 하는 화면이 많아 기본은 끔이다. **마운트 시점에만 반영된다** — 값이 바뀌어도 이미 만들어진 에디터에는 적용되지 않는다. |
3675
+ | `rules?` | `Rule[]` | — | 유효성 규칙 — blur 시 자동 검증 |
3676
+ | `status?` | `SFieldStatus` | — | 필드 상태 ('default' | 'pass' | 'error') |
3677
+ | `focused?` | `boolean` | — | 포커스 상태 (제어/반영) |
3678
+ | `hovered?` | `boolean` | — | 호버 상태 (제어/반영) |
3679
+ | `name?` | `string` | — | 폼 전송용 name |
3680
+ | `editorClass?` | `string` | — | 편집 영역 className |
3681
+ | `editorStyle?` | `CSSProperties` | — | 편집 영역 style |
3682
+ | `label?` | `string` | — | |
3683
+ | `labelWidth?` | `number \| string` | — | |
3684
+ | `icon?` | `SIconName` | — | 레이블 영역 아이콘 |
3685
+ | `iconColor?` | `SColor` | — | |
3686
+ | `labelTooltip?` | `string` | — | 레이블 툴팁 텍스트 |
3687
+ | `labelTooltipProps?` | `Partial<STooltipProps>` | — | 레이블 툴팁 상세 옵션 |
3688
+ | `addonLabel?` | `string` | — | 우측 어드온 레이블 |
3689
+ | `addonAlign?` | `SFieldAddonAlign` | — | 어드온 정렬 |
3690
+ | `hint?` | `string` | — | |
3691
+ | `error?` | `boolean` | — | |
3692
+ | `errorMessage?` | `string` | — | |
3693
+ | `width?` | `SFieldWidth` | `'100%'` | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 본문 길이에 상한이 없으므로 기본은 `"100%"`(행 전체)다. |
3694
+ | `disabled?` | `boolean` | `false` | |
3695
+ | `readOnly?` | `boolean` | `false` | |
3696
+ | `className?` | `string` | — | |
3697
+ | `style?` | `CSSProperties` | — | |
3698
+
3699
+ #### Events
3700
+
3701
+ | Event | Type | Description |
3702
+ |-------|------|-------------|
3703
+ | `onValueChange` | `(html: string) => void` | 값 변경 (sdUpdate) — 빈 문서면 빈 문자열을 준다 |
3704
+ | `onImageUpload` | `(file: File) => Promise<string>` | 이미지 업로드 — 고른 파일을 저장하고 **표시할 URL 을 돌려준다.** 저장 위치는 앱마다 다르므로 DS 가 정하지 않는다. 이 훅이 없으면 툴바에서 이미지 항목이 빠진다 (본문에 base64 를 박지 않는다 — HTML 이 그대로 DB 로 실려 간다). |
3705
+ | `onFocus` | `() => void` | 포커스 진입 |
3706
+ | `onBlur` | `() => void` | 포커스 이탈 |
3707
+
3708
+ #### Methods (ref)
3709
+
3710
+ | Method | Type | Description |
3711
+ |--------|------|-------------|
3712
+ | `focus` | `() => void` | 편집 영역에 포커스 |
3713
+ | `blur` | `() => void` | 포커스 해제 |
3714
+ | `getHTML` | `() => string` | 현재 내용을 HTML 로 반환 (빈 문서면 빈 문자열) |
3715
+ | `getText` | `() => string` | 현재 내용을 서식 없는 텍스트로 반환 |
3716
+ | `setHTML` | `(html: string) => void` | 내용을 HTML 로 교체 (onValueChange 를 발생시키지 않는다) |
3717
+ | `clear` | `() => void` | 내용을 비운다 |
3718
+ | `editor` | `Editor \| null` | tiptap 에디터 인스턴스 — 확장 명령이 필요할 때만 쓴다 |
3719
+
3720
+ ## Types
3721
+
3722
+ ### TiptapApi
3723
+
3724
+ ```ts
3725
+ /** 동적으로 불러온 tiptap — 이 객체를 거치지 않고는 에디터 코드가 tiptap 을 만지지 않는다. */
3726
+ export interface TiptapApi {
3727
+ useEditor: TiptapReact['useEditor'];
3728
+ useEditorState: TiptapReact['useEditorState'];
3729
+ EditorContent: TiptapReact['EditorContent'];
3730
+ /** 선택 영역 위에 뜨는 판 — 자리는 floating-ui 가 잡는다 */
3731
+ BubbleMenu: TiptapMenus['BubbleMenu'];
3732
+ /** tiptap 확장 구성 — 아래 createExtensions 주석 참고 */
3733
+ createExtensions: (options: SEditorExtensionOptions) => AnyExtension[];
3734
+ }
3735
+ ```
3736
+
3737
+ ### SEditorToolbarItem
3738
+
3739
+ ```ts
3740
+ export type SEditorToolbarItem = SEditorToolbarAction | '|';
3741
+ ```
3742
+
3743
+ ### SEditorColorOption
3744
+
3745
+ ```ts
3746
+ export interface SEditorColorOption {
3747
+ /** 팔레트 칸의 접근성 레이블·툴팁 */
3748
+ label: string;
3749
+ /** 팔레트 키(`red_75` …) 또는 CSS 색상 문자열 */
3750
+ color: SColor;
3751
+ }
3752
+ ```
3753
+
3754
+ ### EditorToolbarState
3755
+
3756
+ ```ts
3757
+ export type EditorToolbarState = ReturnType<typeof readEditorState>;
3758
+ ```
3759
+
3760
+ ### SEditorExtensionOptions
3761
+
3762
+ ```ts
3763
+ export interface SEditorExtensionOptions {
3764
+ /** 빈 문서에 보일 문구를 그때그때 읽어 오는 게터 */
3765
+ getPlaceholder: () => string;
3766
+ /** 따옴표·하이픈·화살표 자동 치환 (Typography) */
3767
+ typography: boolean;
3768
+ }
3769
+ ```
3770
+
3771
+ ### SEditorToolbarAction
3772
+
3773
+ ```ts
3774
+ export type SEditorToolbarAction = (typeof SEDITOR_TOOLBAR_ITEMS)[number];
3775
+ ```
3776
+
3777
+ ### SEDITOR_TOOLBAR_ITEMS
3778
+
3779
+ ```ts
3780
+ /** 툴바에 놓을 수 있는 항목. `'|'` 는 구분선이다. */
3781
+ export const SEDITOR_TOOLBAR_ITEMS = [
3782
+ 'heading',
3783
+ 'fontSize',
3784
+ 'bold',
3785
+ 'italic',
3786
+ 'underline',
3787
+ 'strike',
3788
+ 'code',
3789
+ 'color',
3790
+ 'highlight',
3791
+ 'superscript',
3792
+ 'subscript',
3793
+ 'alignLeft',
3794
+ 'alignCenter',
3795
+ 'alignRight',
3796
+ 'alignJustify',
3797
+ 'bulletList',
3798
+ 'orderedList',
3799
+ 'taskList',
3800
+ 'list',
3801
+ 'blockquote',
3802
+ 'codeBlock',
3803
+ 'horizontalRule',
3804
+ 'link',
3805
+ 'image',
3806
+ 'undo',
3807
+ 'redo',
3808
+ ] as const;
3809
+ ```
3810
+
3811
+ ## Dependencies
3812
+
3813
+ ### Depends on
3814
+
3815
+ - [SButton](../SButton)
3816
+ - [SField](../SField)
3817
+ - [SIcon](../SIcon)
3818
+ - [SInput](../SInput)
3819
+
3820
+ ### Graph
3821
+
3822
+ ---
3823
+
2637
3824
  # SExpansionItem
2638
3825
 
2639
3826
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
@@ -2667,6 +3854,42 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2667
3854
  |-------|------|-------------|
2668
3855
  | `onToggle` | `(expanded: boolean, event: MouseEvent<HTMLButtonElement>) => void` | |
2669
3856
 
3857
+ ## Types
3858
+
3859
+ ### SExpansionItemSupportingTextPosition
3860
+
3861
+ ```ts
3862
+ export type SExpansionItemSupportingTextPosition = 'right' | 'bottom';
3863
+ ```
3864
+
3865
+ ### SExpansionItemRenderProp
3866
+
3867
+ ```ts
3868
+ export type SExpansionItemRenderProp = (state: SExpansionItemRenderState) => ReactNode;
3869
+ ```
3870
+
3871
+ ### SExpansionItemInteraction
3872
+
3873
+ ```ts
3874
+ export type SExpansionItemInteraction = 'chevron';
3875
+ ```
3876
+
3877
+ ### SExpansionItemSize
3878
+
3879
+ ```ts
3880
+ export type SExpansionItemSize = 'sm' | 'md';
3881
+ ```
3882
+
3883
+ ### SExpansionItemRenderState
3884
+
3885
+ ```ts
3886
+ export interface SExpansionItemRenderState {
3887
+ expanded: boolean;
3888
+ selected: boolean;
3889
+ disabled: boolean;
3890
+ }
3891
+ ```
3892
+
2670
3893
  ## Dependencies
2671
3894
 
2672
3895
  ### Used by
@@ -2723,8 +3946,9 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2723
3946
  | `hint?` | `string` | `''` | 하단 힌트 |
2724
3947
  | `disabled?` | `boolean` | `false` | 비활성 |
2725
3948
  | `readOnly?` | `boolean` | `false` | 읽기 전용 (회색 배경) |
2726
- | `width?` | `number \| string` | — | 컨트롤 너비 (숫자=px). 지정하면 필드가 부모 폭을 다 먹지 않고 (레이블 + width) 만큼만 차지한다. 다른 요소와 나란히 놓으려면 부모를 flex 로 두면 된다. |
2727
- | `minWidth?` | `number \| string` | — | 컨트롤 최소 너비 (숫자=px). 하한선만 지정하며 필드는 계속 부모 폭을 채운다 |
3949
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비. 지정하면 필드가 부모 폭을 다 먹지 않고 (레이블 + width) 만큼만 차지한다. 다른 요소와 나란히 놓으려면 부모를 flex 로 두면 된다. **폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`)으로 준다** — `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 등급은 `maxLength`(= 스키마 상한)로 고르고, 상한이 `xl` 을 넘거나 상한이 없으면 `"100%"` 로 둔다. (`sellmate/field-width-grade` 가 검사한다) 숫자는 px, 그 밖의 문자열은 CSS 길이 그대로다. |
3950
+ | `minWidth?` | `SFieldWidth` | — | 컨트롤 최소 너비 (등급 · 숫자=px · CSS 길이). 하한선만 지정하며 필드는 계속 부모 폭을 채운다 |
3951
+ | `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 (등급 · 숫자=px · CSS 길이). 상한선만 지정하며 필드는 그 아래에서 부모 폭을 채운다. `width` 와 달리 hug 로 전환하지 않는다 — 부모가 좁으면 같이 좁아지고, 넓어도 여기서 멈춘다. 값 길이가 정해진 컨트롤(날짜·시간)이 `width="100%"` 를 받아 행 전체로 늘어나는 것을 막는 데 쓴다. **라벨은 상한에 들어가지 않는다** — 라벨은 컨트롤의 형제라 이 상한 밖이다. `addonLabel` 은 테두리 박스 안이라 상한을 나눠 먹으므로, `labelWidth`(= addon 폭)만큼 상한을 자동으로 늘린다. addon 이 있는데 `labelWidth` 가 없으면 폭을 알 수 없어 상한을 걸지 않는다 — 컨트롤이 잘리는 것보다 넓은 편이 낫다. |
2728
3952
  | `multiline?` | `boolean` | `false` | 멀티라인(textarea) — 컨트롤 높이를 고정하지 않고 min-height만 적용 |
2729
3953
  | `borderless?` | `boolean` | `false` | 테두리 박스 제거 (inline 컨트롤용) — border/배경/hover·focus 강조만 사라지고 label·hint·errorMessage 등 나머지 필드 구성은 그대로 동작한다. |
2730
3954
  | `children?` | `ReactNode` | — | 실제 컨트롤 (input/select 등) — 테두리 없이 렌더, 테두리는 SField가 제공 |
@@ -2744,6 +3968,26 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2744
3968
  |--------|------|-------------|
2745
3969
  | `focus` | `() => void` | 내부 컨트롤에 포커스하고 필드를 화면에 스크롤합니다. |
2746
3970
 
3971
+ ## Types
3972
+
3973
+ ### SFieldSize
3974
+
3975
+ ```ts
3976
+ export type SFieldSize = 'sm' | 'md';
3977
+ ```
3978
+
3979
+ ### SFieldAddonAlign
3980
+
3981
+ ```ts
3982
+ export type SFieldAddonAlign = 'start' | 'center' | 'end';
3983
+ ```
3984
+
3985
+ ### SFieldStatus
3986
+
3987
+ ```ts
3988
+ export type SFieldStatus = 'default' | 'pass' | 'error';
3989
+ ```
3990
+
2747
3991
  ## Dependencies
2748
3992
 
2749
3993
  ### Used by
@@ -2752,9 +3996,11 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2752
3996
  - [SChipInput](../SChipInput)
2753
3997
  - [SDatePicker](../SDatePicker)
2754
3998
  - [SDateRangePicker](../SDateRangePicker)
3999
+ - [SEditor](../SEditor)
2755
4000
  - [SFilePicker](../SFilePicker)
2756
4001
  - [SInput](../SInput)
2757
4002
  - [SNumberInput](../SNumberInput)
4003
+ - [SSearchInput](../SSearchInput)
2758
4004
  - [SSelect](../SSelect)
2759
4005
  - [STextarea](../STextarea)
2760
4006
  - [STimePicker](../STimePicker)
@@ -2806,7 +4052,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2806
4052
  | `hint?` | `string` | — | |
2807
4053
  | `error?` | `boolean` | — | |
2808
4054
  | `errorMessage?` | `string` | — | |
2809
- | `width?` | `number \| string` | — | |
4055
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
2810
4056
  | `className?` | `string` | — | |
2811
4057
  | `style?` | `CSSProperties` | — | |
2812
4058
 
@@ -2817,6 +4063,20 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2817
4063
  | `onValueChange` | `(value: SFilePickerValue) => void` | 파일 변경 (sdUpdate) |
2818
4064
  | `onReject` | `(detail: { files: File[]; reason: SFilePickerRejectReason }) => void` | 제한 초과 거부 (sdReject) |
2819
4065
 
4066
+ ## Types
4067
+
4068
+ ### SFilePickerValue
4069
+
4070
+ ```ts
4071
+ export type SFilePickerValue = File[] | File | null;
4072
+ ```
4073
+
4074
+ ### SFilePickerRejectReason
4075
+
4076
+ ```ts
4077
+ export type SFilePickerRejectReason = 'max-file-size' | 'max-total-size' | 'max-files';
4078
+ ```
4079
+
2820
4080
  ## Dependencies
2821
4081
 
2822
4082
  ### Used by
@@ -2852,6 +4112,27 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2852
4112
  | `leftClassName?` | `string` | — | |
2853
4113
  | `style?` | `CSSProperties` | — | |
2854
4114
 
4115
+ ## Types
4116
+
4117
+ ### SFooterBg
4118
+
4119
+ ```ts
4120
+ export type SFooterBg = 'white' | 'grey';
4121
+ ```
4122
+
4123
+ ### SFooterButton
4124
+
4125
+ ```ts
4126
+ export interface SFooterButton {
4127
+ label?: string;
4128
+ color?: SButtonColor;
4129
+ outline?: boolean;
4130
+ size?: SButtonSize;
4131
+ disabled?: boolean;
4132
+ onClick?: () => void;
4133
+ }
4134
+ ```
4135
+
2855
4136
  ## Dependencies
2856
4137
 
2857
4138
  ### Used by
@@ -2899,6 +4180,17 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2899
4180
  | `reset` | `() => void` | resetValidation 별칭 — sd-form sdReset 대응. 값 리셋은 소비자(value/onChange) 책임 |
2900
4181
  | `focusFirstInvalid` | `() => void` | 첫 번째 실패 필드로 포커스 이동 |
2901
4182
 
4183
+ ## Types
4184
+
4185
+ ### SFormValidationError
4186
+
4187
+ ```ts
4188
+ export interface SFormValidationError {
4189
+ /** 검증에 실패한 필드 name 목록 (DOM 순서) */
4190
+ names: string[];
4191
+ }
4192
+ ```
4193
+
2902
4194
  ---
2903
4195
 
2904
4196
  # SGhostButton
@@ -2930,6 +4222,20 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2930
4222
  |-------|------|-------------|
2931
4223
  | `onClick` | `(e: MouseEvent<HTMLButtonElement>) => void` | 클릭 (sdClick) |
2932
4224
 
4225
+ ## Types
4226
+
4227
+ ### SGhostButtonSize
4228
+
4229
+ ```ts
4230
+ export type SGhostButtonSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg';
4231
+ ```
4232
+
4233
+ ### SGhostButtonIntent
4234
+
4235
+ ```ts
4236
+ export type SGhostButtonIntent = 'default' | 'danger' | 'action' | 'subAction' | 'inverse';
4237
+ ```
4238
+
2933
4239
  ## Dependencies
2934
4240
 
2935
4241
  ### Used by
@@ -2938,6 +4244,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2938
4244
  - [SCalendar](../SCalendar)
2939
4245
  - [SChip](../SChip)
2940
4246
  - [SChipFilter](../SChipFilter)
4247
+ - [SDatePicker](../SDatePicker)
2941
4248
  - [SDateRangePicker](../SDateRangePicker)
2942
4249
  - [SFilePicker](../SFilePicker)
2943
4250
  - [SGnb](../SGnb)
@@ -2947,7 +4254,8 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
2947
4254
  - [SOverlayHeader](../SOverlayHeader)
2948
4255
  - [SPage](../SPage)
2949
4256
  - [SPopover](../SPopover)
2950
- - [SSelect](../SSelect)
4257
+ - [SSearchInput](../SSearchInput)
4258
+ - [STable](../STable)
2951
4259
  - [STimePicker](../STimePicker)
2952
4260
  - [STimeRangePicker](../STimeRangePicker)
2953
4261
  - [SToast](../SToast)
@@ -3003,6 +4311,51 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3003
4311
  | `onLauncherClick` | `() => void` | 앱런처(그리드) 버튼 클릭. 미지정 시 런처 버튼을 렌더하지 않는다. 접힘 레일(fix)에는 폴드 버튼만 남으므로 표시되지 않는다. |
3004
4312
  | `onMenuWidthChange` | `(width: number) => void` | 메뉴 폭이 확정될 때(드래그를 놓거나 방향키 조작). 드래그하는 동안에는 오지 않는다 |
3005
4313
 
4314
+ ## Types
4315
+
4316
+ ### SGnbType
4317
+
4318
+ ```ts
4319
+ /** 메뉴 스타일: box = 라운드/좁은 들여쓰기, belt = 풀폭 행/넓은 들여쓰기 */
4320
+ export type SGnbType = 'box' | 'belt';
4321
+ ```
4322
+
4323
+ ### SGnbHeader
4324
+
4325
+ ```ts
4326
+ /** 상단바 구조: fix = GNB 폭에 고정, full = 레이아웃 전폭 */
4327
+ export type SGnbHeader = 'fix' | 'full';
4328
+ ```
4329
+
4330
+ ### SGnbColor
4331
+
4332
+ ```ts
4333
+ /** 색상(테마): light / dark */
4334
+ export type SGnbColor = 'light' | 'dark';
4335
+ ```
4336
+
4337
+ ### SGnbMenuItem
4338
+
4339
+ ```ts
4340
+ /** GNB 메뉴 아이템 (재귀 트리). depth1 → depth2 → depth3. */
4341
+ export interface SGnbMenuItem {
4342
+ /** 표시 텍스트 */
4343
+ label: string;
4344
+ /** 고유 식별값 (선택 상태 비교 기준) */
4345
+ value: string;
4346
+ /** depth1 전용 아이콘명 */
4347
+ icon?: SIconName;
4348
+ /** 우측 Tag 뱃지 텍스트 */
4349
+ tag?: string;
4350
+ /** 뱃지 색. 미지정 시 blue. */
4351
+ tagColor?: STagColor;
4352
+ /** 비활성 여부 */
4353
+ disabled?: boolean;
4354
+ /** 하위 아이템 (있으면 펼침/접힘 대상) */
4355
+ children?: SGnbMenuItem[];
4356
+ }
4357
+ ```
4358
+
3006
4359
  ## Dependencies
3007
4360
 
3008
4361
  ### Depends on
@@ -3035,6 +4388,21 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3035
4388
  | `className?` | `string` | — | |
3036
4389
  | `style?` | `CSSProperties` | — | |
3037
4390
 
4391
+ ## Types
4392
+
4393
+ ### SGuideType
4394
+
4395
+ ```ts
4396
+ export type SGuideType = 'tip' | 'notion';
4397
+ ```
4398
+
4399
+ ### SGuideMessage
4400
+
4401
+ ```ts
4402
+ /** 중첩 배열로 depth 표현 (원본 renderListItem 대응). 예: ['상위', ['하위1', '하위2']] */
4403
+ export type SGuideMessage = string | SGuideMessage[];
4404
+ ```
4405
+
3038
4406
  ## Dependencies
3039
4407
 
3040
4408
  ### Depends on
@@ -3078,6 +4446,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3078
4446
  - [SDateRangePicker](../SDateRangePicker)
3079
4447
  - [SDraggableItem](../SDraggableItem)
3080
4448
  - [SDropdownButton](../SDropdownButton)
4449
+ - [SEditor](../SEditor)
3081
4450
  - [SExpansionItem](../SExpansionItem)
3082
4451
  - [SField](../SField)
3083
4452
  - [SFilePicker](../SFilePicker)
@@ -3091,6 +4460,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3091
4460
  - [SNumberInput](../SNumberInput)
3092
4461
  - [SPagination](../SPagination)
3093
4462
  - [SPopover](../SPopover)
4463
+ - [SSearchInput](../SSearchInput)
3094
4464
  - [SSectionHeaderCard](../SSectionHeaderCard)
3095
4465
  - [SSelect](../SSelect)
3096
4466
  - [SStepper](../SStepper)
@@ -3135,6 +4505,20 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3135
4505
  | `onLoad` | `(src: string) => void` | 로드 완료. 로드된 src 를 넘긴다 |
3136
4506
  | `onError` | `(event: SyntheticEvent<HTMLImageElement>) => void` | 로드 실패 |
3137
4507
 
4508
+ ## Types
4509
+
4510
+ ### SImageFit
4511
+
4512
+ ```ts
4513
+ export type SImageFit = (typeof IMAGE_FITS)[number];
4514
+ ```
4515
+
4516
+ ### IMAGE_FITS
4517
+
4518
+ ```ts
4519
+ export const IMAGE_FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'] as const;
4520
+ ```
4521
+
3138
4522
  ## Dependencies
3139
4523
 
3140
4524
  ### Depends on
@@ -3179,7 +4563,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3179
4563
  | `errorMessage?` | `string` | — | |
3180
4564
  | `addonLabel?` | `string` | — | |
3181
4565
  | `addonAlign?` | `SFieldAddonAlign` | — | 어드온 정렬 |
3182
- | `width?` | `number \| string` | — | |
4566
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리며 `maxLength`(= 스키마 상한)로 고른다. 상한이 `xl` 을 넘거나 상한이 없으면 `"100%"`. (`sellmate/field-width-grade` 가 검사한다) |
3183
4567
  | `disabled?` | `boolean` | `false` | |
3184
4568
  | `readOnly?` | `boolean` | `false` | |
3185
4569
  | `className?` | `string` | — | |
@@ -3196,6 +4580,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3196
4580
 
3197
4581
  ### Used by
3198
4582
 
4583
+ - [SEditor](../SEditor)
3199
4584
  - [SKeyValueTable](../SKeyValueTable)
3200
4585
 
3201
4586
  ### Depends on
@@ -3221,6 +4606,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3221
4606
  | `values?` | `Record<string, unknown>` | `{}` | field name을 key로 하는 값 객체 (`{ [name]: value }`). 지정 시 해당 field의 값으로 사용되며, field별 `options.value`보다 우선합니다. `onChange`의 `detail.values`와 함께 controlled 패턴으로 사용합니다. |
3222
4607
  | `search?` | `boolean` | `false` | 우측 검색 패널 |
3223
4608
  | `radius?` | `'default' \| 'useTop' \| 'full'` | `'default'` | border-radius 제어 |
4609
+ | `bordered?` | `boolean` | `true` | 바깥 테두리. 기본은 `true`. `SSectionHeaderCard` 의 `padding="none"` 안에 넣어 카드 가장자리까지 채울 때 `false` 로 끈다 — 카드가 이미 바깥 테두리를 그리므로, 켜 두면 1px 두 개가 나란히 놓여 그 변만 2px 로 보인다. |
3224
4610
  | `className?` | `string` | — | |
3225
4611
  | `style?` | `CSSProperties` | — | |
3226
4612
 
@@ -3231,6 +4617,90 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3231
4617
  | `onChange` | `(detail: SKeyValueChangeDetail) => void` | 값 변경 (sdChange) |
3232
4618
  | `onSearch` | `() => void` | 검색 클릭 (sdSearch) |
3233
4619
 
4620
+ ## Types
4621
+
4622
+ ### SKeyValueField
4623
+
4624
+ ```ts
4625
+ export type SKeyValueField = SKeyValueFieldBase & SKeyValueFieldByType;
4626
+ ```
4627
+
4628
+ ### SKeyValueChangeDetail
4629
+
4630
+ ```ts
4631
+ /**
4632
+ * `onChange` 콜백 detail.
4633
+ * 제네릭 `V`에 폼 값 형태(`{ [name]: value }`)를 넘기면 `name`/`value`/`values`가 정밀하게 타이핑됩니다.
4634
+ * 미지정 시 `value`는 {@link SKeyValueFieldValue} 유니온으로 잡힙니다.
4635
+ */
4636
+ export interface SKeyValueChangeDetail<
4637
+ V extends Record<string, SKeyValueFieldValue> = Record<string, SKeyValueFieldValue>,
4638
+ > {
4639
+ /** 변경된 field의 name */
4640
+ name: Extract<keyof V, string>;
4641
+ /** 변경된 field의 새 값 */
4642
+ value: V[Extract<keyof V, string>];
4643
+ /** 변경이 반영된 전체 값 객체 (`{ [name]: value }`). controlled 패턴에서 그대로 상태에 반영하면 됩니다. */
4644
+ values: V;
4645
+ }
4646
+ ```
4647
+
4648
+ ### SKeyValueFieldBase
4649
+
4650
+ ```ts
4651
+ /** field type과 무관한 공통 속성 (레이아웃·레이블·스팬 등) */
4652
+ export interface SKeyValueFieldBase {
4653
+ name: string;
4654
+ label?: string;
4655
+ required?: boolean;
4656
+ helpText?: string[];
4657
+ hideTh?: boolean;
4658
+ thRowSpan?: number;
4659
+ thColSpan?: number;
4660
+ tdRowSpan?: number;
4661
+ tdColSpan?: number;
4662
+ thWidth?: number | string;
4663
+ thClass?: string;
4664
+ tdClass?: string;
4665
+ /** 셀 커스텀 렌더 (React 확장). 지정 시 type/options는 무시되고 이 노드가 그대로 렌더됩니다. */
4666
+ render?: ReactNode;
4667
+ }
4668
+ ```
4669
+
4670
+ ### SKeyValueFieldByType
4671
+
4672
+ ```ts
4673
+ /**
4674
+ * `type`에 따라 `options`가 해당 컴포넌트의 props로 좁혀지는 discriminated union.
4675
+ * `type` 생략 시 `text`로 동작합니다.
4676
+ */
4677
+ export type SKeyValueFieldByType =
4678
+ | { type?: 'text'; options?: { value?: ReactNode } }
4679
+ | { type: 'input'; options?: FieldOptions<SInputProps> }
4680
+ | { type: 'textarea'; options?: FieldOptions<STextareaProps> }
4681
+ | { type: 'number-input'; options?: FieldOptions<SNumberInputProps> }
4682
+ | { type: 'select'; options?: FieldOptions<SSelectProps> }
4683
+ | { type: 'radio'; options?: FieldOptions<SRadioGroupProps> }
4684
+ | { type: 'checkbox'; options?: FieldOptions<SCheckboxProps> }
4685
+ | { type: 'switch'; options?: FieldOptions<SSwitchProps> }
4686
+ | { type: 'date-picker'; options?: FieldOptions<SDatePickerProps> }
4687
+ | { type: 'date-range-picker'; options?: FieldOptions<SDateRangePickerProps> }
4688
+ | { type: 'file-picker'; options?: FieldOptions<SFilePickerProps> };
4689
+ ```
4690
+
4691
+ ### SKeyValueFieldValue
4692
+
4693
+ ```ts
4694
+ /**
4695
+ * field type별 `onChange`로 올라오는 값 타입.
4696
+ * - `string`: input · textarea · select · radio · date-picker
4697
+ * - `number`: number-input
4698
+ * - `boolean`: checkbox · switch
4699
+ * - `string[]`: date-range-picker
4700
+ */
4701
+ export type SKeyValueFieldValue = string | number | boolean | string[];
4702
+ ```
4703
+
3234
4704
  ## Dependencies
3235
4705
 
3236
4706
  ### Depends on
@@ -3245,6 +4715,7 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3245
4715
  - [SNumberInput](../SNumberInput)
3246
4716
  - [SRadio](../SRadio)
3247
4717
  - [SSelect](../SSelect)
4718
+ - [SSwitch](../SSwitch)
3248
4719
  - [STextarea](../STextarea)
3249
4720
  - [STooltip](../STooltip)
3250
4721
 
@@ -3274,6 +4745,22 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3274
4745
  |-------|------|-------------|
3275
4746
  | `onFoldedChange` | `(folded: boolean) => void` | 접힘 상태 변경 |
3276
4747
 
4748
+ ## Types
4749
+
4750
+ ### SLayoutType
4751
+
4752
+ ```ts
4753
+ /** 메뉴 스타일: box(라운드) / belt(풀폭 행) — 자식 SGnb 의 메뉴 모양 */
4754
+ export type SLayoutType = SGnbType;
4755
+ ```
4756
+
4757
+ ### SLayoutHeader
4758
+
4759
+ ```ts
4760
+ /** 레이아웃 구조: fix(좌측 GNB + 페이지 가로 분할) / full(풀폭 상단바 + 아래에 메뉴|페이지) */
4761
+ export type SLayoutHeader = SGnbHeader;
4762
+ ```
4763
+
3277
4764
  ## Dependencies
3278
4765
 
3279
4766
  ### Used by
@@ -3302,6 +4789,14 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3302
4789
  | `className?` | `string` | — | |
3303
4790
  | `style?` | `CSSProperties` | — | |
3304
4791
 
4792
+ ## Types
4793
+
4794
+ ### SLinearProgressType
4795
+
4796
+ ```ts
4797
+ export type SLinearProgressType = 'primary' | 'error' | 'complete';
4798
+ ```
4799
+
3305
4800
  ---
3306
4801
 
3307
4802
  # SList
@@ -3317,7 +4812,6 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3317
4812
  | `children?` | `ReactNode` | — | 리스트 컨테이너 내부에 렌더링할 내용 |
3318
4813
  | `useGap?` | `boolean` | `false` | 리스트 아이템 사이 gap 토큰 적용 여부 |
3319
4814
  | `usePadding?` | `boolean` | `false` | 리스트 컨테이너 padding 토큰 적용 여부 |
3320
- | `separator?` | `boolean` | `false` | 아이템 하단 구분선 표시 여부. 테두리를 가진 아이템(`bordered`)에는 쓰지 않는다 |
3321
4815
 
3322
4816
  ## Dependencies
3323
4817
 
@@ -3326,6 +4820,10 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3326
4820
  - [SDraggableList](../SDraggableList)
3327
4821
  - [SExpansionList](../SExpansionList)
3328
4822
 
4823
+ ### Depends on
4824
+
4825
+ - [SDivider](../SDivider)
4826
+
3329
4827
  ### Graph
3330
4828
 
3331
4829
  ---
@@ -3355,6 +4853,47 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3355
4853
  | `size?` | `SListItemSize` | `'sm'` | 타이포그래피 크기 |
3356
4854
  | `disabled?` | `boolean` | `false` | 비활성 상태 여부 |
3357
4855
 
4856
+ ## Types
4857
+
4858
+ ### SListItemSlot
4859
+
4860
+ ```ts
4861
+ export type SListItemSlot = ReactNode | SListItemRenderProp;
4862
+ ```
4863
+
4864
+ ### SListItemSupportingTextPosition
4865
+
4866
+ ```ts
4867
+ export type SListItemSupportingTextPosition = 'right' | 'bottom';
4868
+ ```
4869
+
4870
+ ### SListItemInteraction
4871
+
4872
+ ```ts
4873
+ export type SListItemInteraction = 'chevron';
4874
+ ```
4875
+
4876
+ ### SListItemSize
4877
+
4878
+ ```ts
4879
+ export type SListItemSize = 'sm' | 'md';
4880
+ ```
4881
+
4882
+ ### SListItemRenderProp
4883
+
4884
+ ```ts
4885
+ export type SListItemRenderProp = (state: SListItemRenderState) => ReactNode;
4886
+ ```
4887
+
4888
+ ### SListItemRenderState
4889
+
4890
+ ```ts
4891
+ export interface SListItemRenderState {
4892
+ hovered: boolean;
4893
+ disabled: boolean;
4894
+ }
4895
+ ```
4896
+
3358
4897
  ## Dependencies
3359
4898
 
3360
4899
  ### Used by
@@ -3423,6 +4962,14 @@ Tailwind 유틸리티는 아래 스케일에 있는 값만 사용한다. 리터
3423
4962
  | `onClose` | `() => void` | 닫기(X) 버튼 클릭 (sdClose) — error 상태에서만 노출 |
3424
4963
  | `onButtonClick` | `() => void` | 버튼 클릭 (sdClick) |
3425
4964
 
4965
+ ## Types
4966
+
4967
+ ### LoadingModalState
4968
+
4969
+ ```ts
4970
+ export type LoadingModalState = 'loading' | 'error';
4971
+ ```
4972
+
3426
4973
  ## Dependencies
3427
4974
 
3428
4975
  ### Used by
@@ -3755,7 +5302,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3755
5302
  | `hint?` | `string` | — | |
3756
5303
  | `error?` | `boolean` | — | |
3757
5304
  | `errorMessage?` | `string` | — | |
3758
- | `width?` | `number \| string` | — | |
5305
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리며 `max`(= 스키마 상한)로 고른다. 상한이 `xl` 을 넘거나 상한이 없으면 `"100%"`. (`sellmate/field-width-grade` 가 검사한다) |
3759
5306
  | `className?` | `string` | — | |
3760
5307
  | `style?` | `CSSProperties` | — | |
3761
5308
 
@@ -3767,6 +5314,14 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3767
5314
  | `onFocus` | `(e: React.FocusEvent<HTMLInputElement>) => void` | 포커스 이벤트 (sdFocus) |
3768
5315
  | `onBlur` | `(e: React.FocusEvent<HTMLInputElement>) => void` | 블러 이벤트 (sdBlur) |
3769
5316
 
5317
+ ## Types
5318
+
5319
+ ### SNumberInputSize
5320
+
5321
+ ```ts
5322
+ export type SNumberInputSize = SFieldSize;
5323
+ ```
5324
+
3770
5325
  ## Dependencies
3771
5326
 
3772
5327
  ### Used by
@@ -3851,9 +5406,49 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3851
5406
  | Prop | Type | Default | Description |
3852
5407
  |------|------|---------|-------------|
3853
5408
  | `background?` | `SPageBackground` | `'frame'` | 페이지 배경 표면. frame=흰 콘텐츠 면, neutral=옅은 회색 면, screen=앱 바탕. 스크롤바 처리도 여기 묶여 있다 — 셋 다 구분선+트랙 배경이고, 트랙 색만 neutral 에서 흰색이 된다. |
3854
- | `scrollEndSpacing?` | `boolean` | `true` | 스크롤 끝 여백. 마지막 항목이 창 하단에 붙어 "여기서 끝"이 안 읽히는 것을 막는다. 기본으로 켜져 있고, 페이지네이션처럼 끝을 알려주는 것이 이미 있으면 끈다(목록 페이지). |
5409
+ | `scrollEndSpacing?` | `boolean` | `false` | 스크롤 끝 여백. 마지막 항목이 창 하단에 붙어 "여기서 끝"이 안 읽히는 것을 막는다. **기본은 꺼져 있다.** 페이지 스크롤 자체가 예외이기 때문이다 — 대부분의 화면은 본문이 창을 꽉 채우고 스크롤은 각 영역 안에서 일어난다. 여백을 조건 없이 붙이면 내용이 화면에 거의 딱 맞는 페이지까지 그 여백 때문에 스크롤되게 만든다. 페이지가 실제로 스크롤되는 화면(`contentHeight="auto"` + 내용이 창보다 김)에서만 켠다. 페이지네이션처럼 끝을 알려주는 것이 이미 있으면 켜지 않는다. |
5410
+ | `overlayScrollbar?` | `boolean` | `false` | true면 네이티브 스크롤바를 숨기고 SPage 위에 오버레이 스크롤바를 얹는다. 스크롤바가 레이아웃 폭을 차지하지 않아 내부 콘텐츠 폭이 줄어들지 않는다. |
5411
+ | `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` 이 무시된다. |
3855
5412
  | `header?` | `SPageHeaderProps` | — | 페이지 타이틀 영역(pageHeader 포팅). 주면 스크롤·페이지 패딩 밖으로 빼내 상단에 고정 배치한다. `children` 은 항상 본문이다 — header 유무와 무관하게 같은 자리에 같은 뜻으로 들어간다. |
3856
5413
 
5414
+ ## Types
5415
+
5416
+ ### SPageBackground
5417
+
5418
+ ```ts
5419
+ export type SPageBackground = (typeof PAGE_BACKGROUNDS)[number];
5420
+ ```
5421
+
5422
+ ### SPageContentHeight
5423
+
5424
+ ```ts
5425
+ export type SPageContentHeight = (typeof PAGE_CONTENT_HEIGHTS)[number];
5426
+ ```
5427
+
5428
+ ### PAGE_BACKGROUNDS
5429
+
5430
+ ```ts
5431
+ /**
5432
+ * 페이지 배경 — 표면의 역할로 이름을 붙인다(리터럴 색이 아니다).
5433
+ * - frame: 콘텐츠를 얹는 흰 표면 (sys.color.bg.frame)
5434
+ * - neutral: 한 단계 눌러앉은 회색 표면 (sys.color.bg.neutralLight)
5435
+ * - screen: 카드·패널이 떠 있는 앱 바탕 (sys.color.bg.screen)
5436
+ */
5437
+ export const PAGE_BACKGROUNDS = ['frame', 'neutral', 'screen'] as const;
5438
+ ```
5439
+
5440
+ ### PAGE_CONTENT_HEIGHTS
5441
+
5442
+ ```ts
5443
+ /**
5444
+ * 본문 높이 모드 — 페이지가 스크롤할지, 본문이 남은 높이를 채울지.
5445
+ * - auto: 콘텐츠가 흐르고, 넘치면 페이지가 스크롤한다
5446
+ * - fill: 본문이 남은 높이를 정확히 채우고 페이지는 스크롤하지 않는다.
5447
+ * 표가 자기 안에서 스크롤하고 페이지네이션이 하단에 고정되는 목록 화면용.
5448
+ */
5449
+ export const PAGE_CONTENT_HEIGHTS = ['auto', 'fill'] as const;
5450
+ ```
5451
+
3857
5452
  ## Dependencies
3858
5453
 
3859
5454
  ### Depends on
@@ -3936,6 +5531,21 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3936
5531
  | `onLeftLinkClick` | `() => void` | 하단 좌측 링크 클릭 |
3937
5532
  | `onButtonClick` | `() => void` | 하단 우측 버튼 클릭 |
3938
5533
 
5534
+ ## Types
5535
+
5536
+ ### SPopoverPlacement
5537
+
5538
+ ```ts
5539
+ export type SPopoverPlacement = 'top' | 'bottom' | 'left' | 'right';
5540
+ ```
5541
+
5542
+ ### SPopoverType
5543
+
5544
+ ```ts
5545
+ /** 색상 타입 — default: 다크 배경 / danger·warning·accent: 라이트 배경 (component.popover 토큰) */
5546
+ export type SPopoverType = 'default' | 'danger' | 'warning' | 'accent';
5547
+ ```
5548
+
3939
5549
  ## Dependencies
3940
5550
 
3941
5551
  ### Depends on
@@ -3975,6 +5585,25 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
3975
5585
  |-------|------|-------------|
3976
5586
  | `onSubmit` | `() => void` | 확인 버튼 클릭 (sdSubmit) |
3977
5587
 
5588
+ ## Types
5589
+
5590
+ ### SPopupType
5591
+
5592
+ ```ts
5593
+ export type SPopupType = 'default' | 'light';
5594
+ ```
5595
+
5596
+ ### SPopupSubmitButton
5597
+
5598
+ ```ts
5599
+ export interface SPopupSubmitButton {
5600
+ label?: string;
5601
+ color?: SButtonColor;
5602
+ outline?: boolean;
5603
+ size?: SButtonSize;
5604
+ }
5605
+ ```
5606
+
3978
5607
  ## Dependencies
3979
5608
 
3980
5609
  ### Depends on
@@ -4021,6 +5650,20 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4021
5650
  | `onCloseAutoFocus` | `(event: Event) => void` | 닫힐 때 포커스 복귀 처리 (기본은 Radix가 앵커로 포커스를 되돌림). e.preventDefault()로 막을 수 있다. |
4022
5651
  | `onPointerDownOutside` | `(event: Event) => void` | 바깥 영역 pointerdown 으로 닫힘이 시작될 때 (앵커 위 클릭·persistent 는 제외). |
4023
5652
 
5653
+ ## Types
5654
+
5655
+ ### SPortalPlacement
5656
+
5657
+ ```ts
5658
+ export type SPortalPlacement = 'top' | 'bottom' | 'left' | 'right';
5659
+ ```
5660
+
5661
+ ### SPortalAlign
5662
+
5663
+ ```ts
5664
+ export type SPortalAlign = 'start' | 'center' | 'end';
5665
+ ```
5666
+
4024
5667
  ## Dependencies
4025
5668
 
4026
5669
  ### Used by
@@ -4076,6 +5719,26 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4076
5719
  |-------|------|-------------|
4077
5720
  | `onValueChange` | `(val: SRadioValue) => void` | 선택 변경 (sdUpdate) — 선택된 val 전달 |
4078
5721
 
5722
+ ## Types
5723
+
5724
+ ### SRadioOption
5725
+
5726
+ ```ts
5727
+ export interface SRadioOption {
5728
+ /** 옵션 값 (sd-radio-group SRadioOption.value) */
5729
+ value: SRadioValue;
5730
+ label: string;
5731
+ disabled?: boolean;
5732
+ }
5733
+ ```
5734
+
5735
+ ### SRadioValue
5736
+
5737
+ ```ts
5738
+ /** 라디오 값 타입 (sd-radio SRadioValue) */
5739
+ export type SRadioValue = string | number | boolean;
5740
+ ```
5741
+
4079
5742
  ## Dependencies
4080
5743
 
4081
5744
  ### Used by
@@ -4109,71 +5772,177 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4109
5772
 
4110
5773
  | Event | Type | Description |
4111
5774
  |-------|------|-------------|
4112
- | `onValueChange` | `(value: string \| number) => void` | 변경 (sdUpdate) |
5775
+ | `onValueChange` | `(value: string \| number) => void` | 변경 (sdUpdate) |
5776
+
5777
+ ## Types
5778
+
5779
+ ### SRadioButtonOption
5780
+
5781
+ ```ts
5782
+ export interface SRadioButtonOption {
5783
+ value: string | number;
5784
+ label: string;
5785
+ disabled?: boolean;
5786
+ }
5787
+ ```
5788
+
5789
+ ### SRadioButtonSize
5790
+
5791
+ ```ts
5792
+ export type SRadioButtonSize = 'xs' | 'sm';
5793
+ ```
5794
+
5795
+ ## Dependencies
5796
+
5797
+ ### Used by
5798
+
5799
+ - [SChipFilter](../SChipFilter)
5800
+
5801
+ ### Graph
5802
+
5803
+ ---
5804
+
5805
+ # SScrollArea
5806
+
5807
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
5808
+
5809
+ ### SScrollArea
5810
+
5811
+ #### Props
5812
+
5813
+ | Prop | Type | Default | Description |
5814
+ |------|------|---------|-------------|
5815
+ | `axis?` | `SScrollAreaAxis` | `'both'` | 스크롤 방향. vertical=세로만, horizontal=가로만, both=양방향 |
5816
+ | `background?` | `boolean` | `false` | true면 스크롤바 트랙 배경을 채운다. 콘텐츠 영역 배경은 바뀌지 않는다. |
5817
+ | `bordered?` | `boolean` | `false` | true면 스크롤바와 콘텐츠 사이에 1px 구분선을 그린다. 컨테이너 테두리가 아니다. |
5818
+ | `maxHeight?` | `string` | — | viewport 최대 높이 (예: '400px'). 비우면 부모 크기를 따른다. |
5819
+ | `maxWidth?` | `string` | — | viewport 최대 너비 (예: '480px'). 비우면 부모 크기를 따른다. |
5820
+
5821
+ ## Types
5822
+
5823
+ ### SScrollAreaAxis
5824
+
5825
+ ```ts
5826
+ export type SScrollAreaAxis = (typeof SCROLL_AREA_AXES)[number];
5827
+ ```
5828
+
5829
+ ### SCROLL_AREA_AXES
5830
+
5831
+ ```ts
5832
+ export const SCROLL_AREA_AXES = ['vertical', 'horizontal', 'both'] as const;
5833
+ ```
5834
+
5835
+ ---
5836
+
5837
+ # SSearchInput
5838
+
5839
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
5840
+
5841
+ ### SSearchInput
5842
+
5843
+ #### Props
5844
+
5845
+ | Prop | Type | Default | Description |
5846
+ |------|------|---------|-------------|
5847
+ | `value?` | `string \| number` | — | 값 (제어) |
5848
+ | `size?` | `SSearchInputSize` | `'sm'` | 크기 |
5849
+ | `placeholder?` | `string` | `'결과 내 검색'` | 플레이스홀더 |
5850
+ | `clearable?` | `boolean` | `false` | 지우기 버튼 — 값이 있을 때만 나타난다 |
5851
+ | `disabled?` | `boolean` | `false` | 비활성 |
5852
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. 미지정 시 부모 폭을 채운다. |
5853
+ | `focused?` | `boolean` | — | 포커스 상태 (제어/반영) |
5854
+ | `hovered?` | `boolean` | — | 호버 상태 (제어/반영) |
5855
+ | `inputClass?` | `string` | — | 내부 input 요소 className |
5856
+ | `inputStyle?` | `CSSProperties` | — | 내부 input 요소 style |
5857
+ | `className?` | `string` | — | |
5858
+ | `style?` | `CSSProperties` | — | |
5859
+
5860
+ #### Events
5861
+
5862
+ | Event | Type | Description |
5863
+ |-------|------|-------------|
5864
+ | `onValueChange` | `(value: string) => void` | 값 변경 — 문자열 전달 |
5865
+ | `onChange` | `InputHTMLAttributes<HTMLInputElement>['onChange']` | 네이티브 onChange (form-agnostic 연동용, RHF 등) |
5866
+ | `onSearch` | `(value: string) => void` | 검색 실행 — Enter 키에서 현재 값과 함께 호출된다. 한글 조합 중의 Enter(IME 확정)는 검색으로 치지 않는다. |
5867
+
5868
+ ## Types
5869
+
5870
+ ### SSearchInputSize
5871
+
5872
+ ```ts
5873
+ export type SSearchInputSize = SFieldSize;
5874
+ ```
4113
5875
 
4114
5876
  ## Dependencies
4115
5877
 
4116
5878
  ### Used by
4117
5879
 
4118
- - [SChipFilter](../SChipFilter)
5880
+ - [SSelect](../SSelect)
5881
+
5882
+ ### Depends on
5883
+
5884
+ - [SField](../SField)
5885
+ - [SGhostButton](../SGhostButton)
5886
+ - [SIcon](../SIcon)
4119
5887
 
4120
5888
  ### Graph
4121
5889
 
4122
5890
  ---
4123
5891
 
4124
- # SScrollArea
5892
+ # SSectionHeaderCard
4125
5893
 
4126
5894
  > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4127
5895
 
4128
- ### SScrollArea
5896
+ ### SSectionHeaderCard
4129
5897
 
4130
5898
  #### Props
4131
5899
 
4132
5900
  | Prop | Type | Default | Description |
4133
5901
  |------|------|---------|-------------|
4134
- | `axis?` | `SScrollAreaAxis` | `'both'` | 스크롤 방향. vertical=세로만, horizontal=가로만, both=양방향 |
4135
- | `background?` | `boolean` | `false` | true면 스크롤바 트랙 배경을 채운다. 콘텐츠 영역 배경은 바뀌지 않는다. |
4136
- | `bordered?` | `boolean` | `false` | true면 스크롤바와 콘텐츠 사이에 1px 구분선을 그린다. 컨테이너 테두리가 아니다. |
4137
- | `maxHeight?` | `string` | — | viewport 최대 높이 (예: '400px'). 비우면 부모 크기를 따른다. |
4138
- | `maxWidth?` | `string` | — | viewport 최대 너비 (예: '480px'). 비우면 부모 크기를 따른다. |
4139
-
4140
- ---
5902
+ | `title?` | `ReactNode` | — | 헤더 제목 |
5903
+ | `titleSize?` | `SSectionHeaderCardTitleSize` | — | 제목 크기 |
5904
+ | `marker?` | `boolean` | — | 제목 앞 점 표시 여부 |
5905
+ | `required?` | `boolean` | — | 제목 뒤 필수 별 표시 여부 |
5906
+ | `helpText?` | `string[]` | — | 도움말 툴팁 메시지 |
5907
+ | `subtitle?` | `ReactNode` | — | 부제 |
5908
+ | `slot?` | `ReactNode` | — | 헤더 우측 슬롯 |
5909
+ | `thickness?` | `SSectionHeaderCardThickness` | — | 상단 border 색상 타입. false면 표시하지 않습니다. |
5910
+ | `headerClassName?` | `string` | — | 헤더 영역 클래스 |
5911
+ | `padding?` | `SSectionHeaderCardBodyPadding` | — | 바디 안쪽 여백. 기본은 `default`. 성격이 다른 요소가 세 종류 이상 섞인 영역에만 `wide`, 표를 가장자리까지 채울 때만 `none`. |
5912
+ | `background?` | `SSectionHeaderCardBodyBackground` | — | 본문 배경. 기본은 `frame` — 카드가 깐 흰 면을 그대로 쓴다. `neutral` 은 바탕을 한 단계 눌러앉혀, 흰 면 덩어리(표·리스트)가 여럿일 때 그것들이 **"면 위에 놓인 객체"로 읽히게** 한다. 위계를 한 단계 더 주고 싶을 때 고르는 선택지이며, **기본값이 틀린 것은 아니다** — 표·리스트는 테두리·라운드·헤더 줄과 간격을 이미 갖고 있어 흰 바탕에서도 경계가 읽힌다. 깔아도 **효과가 없는** 자리가 있다 — 덩어리가 가장자리까지 차는 경우(`padding="none"` 으로 표를 채우면 바탕이 완전히 가려진다), 덩어리에 회색 면이 섞인 경우(그 덩어리가 바탕에 묻힌다), 맨 텍스트나 폼 컨트롤만 있는 본문(떠오를 흰 면이 없다). |
5913
+ | `children?` | `SSectionHeaderCardChildren` | — | 바디 콘텐츠. 특정 컴포넌트 타입으로 제한하지 않습니다. |
4141
5914
 
4142
- # SSectionHeaderCard
5915
+ ## Types
4143
5916
 
4144
- > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
5917
+ ### SSectionHeaderCardTitleSize
4145
5918
 
4146
- ### SSectionHeaderCardBody
5919
+ ```ts
5920
+ export type SSectionHeaderCardTitleSize = 'xs' | 'sm';
5921
+ ```
4147
5922
 
4148
- #### Props
5923
+ ### SSectionHeaderCardThickness
4149
5924
 
4150
- | Prop | Type | Default | Description |
4151
- |------|------|---------|-------------|
4152
- | `children?` | `SSectionHeaderCardBodyChildren` | — | 바디 슬롯. 특정 컴포넌트 타입으로 제한하지 않습니다. |
4153
- | `padding?` | `SSectionHeaderCardBodyPadding` | `'default'` | 안쪽 여백. 기본은 `default`. 성격이 다른 요소가 세 종류 이상 섞인 영역에만 `wide`, 표를 가장자리까지 채울 때만 `none`. |
5925
+ ```ts
5926
+ export type SSectionHeaderCardThickness = false | 'default' | 'accent';
5927
+ ```
4154
5928
 
4155
- ### SSectionHeaderCardHeader
5929
+ ### SSectionHeaderCardBodyPadding
4156
5930
 
4157
- #### Props
5931
+ ```ts
5932
+ export type SSectionHeaderCardBodyPadding = 'default' | 'wide' | 'none';
5933
+ ```
4158
5934
 
4159
- | Prop | Type | Default | Description |
4160
- |------|------|---------|-------------|
4161
- | `title` | `ReactNode` | — | 헤더 제목 |
4162
- | `titleSize?` | `SSectionHeaderCardTitleSize` | `'xs'` | 제목 크기 |
4163
- | `marker?` | `boolean` | `false` | 제목 앞 점 표시 여부 |
4164
- | `required?` | `boolean` | `false` | 제목 뒤 필수 별 표시 여부 |
4165
- | `helpText?` | `string[]` | — | 도움말 툴팁 메시지 |
4166
- | `subtitle?` | `ReactNode` | — | 부제 |
4167
- | `slot?` | `ReactNode` | — | 헤더 우측 슬롯 |
4168
- | `thickness?` | `SSectionHeaderCardThickness` | `false` | 상단 border 색상 타입. false면 표시하지 않습니다. |
5935
+ ### SSectionHeaderCardBodyBackground
4169
5936
 
4170
- ### SSectionHeaderCard
5937
+ ```ts
5938
+ export type SSectionHeaderCardBodyBackground = 'frame' | 'neutral';
5939
+ ```
4171
5940
 
4172
- #### Props
5941
+ ### SSectionHeaderCardChildren
4173
5942
 
4174
- | Prop | Type | Default | Description |
4175
- |------|------|---------|-------------|
4176
- | `children?` | `SSectionHeaderCardChildren` | — | SSectionHeaderCard 슬롯. 특정 컴포넌트 타입으로 제한하지 않습니다. |
5943
+ ```ts
5944
+ export type SSectionHeaderCardChildren = ReactNode;
5945
+ ```
4177
5946
 
4178
5947
  ## Dependencies
4179
5948
 
@@ -4207,6 +5976,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4207
5976
  | `allSelectedLabel?` | `string` | `'전체'` | 전체선택 레이블 |
4208
5977
  | `placeholder?` | `string` | `'선택'` | placeholder |
4209
5978
  | `disabled?` | `boolean` | `false` | 비활성 |
5979
+ | `clearable?` | `boolean` | `false` | 선택값 지우기(×) 버튼 노출. 선택값이 있고 비활성이 아닐 때만 나타나며, 누르면 드롭다운을 열지 않고 값만 비운다. |
4210
5980
  | `error?` | `boolean` | `false` | 에러 상태 |
4211
5981
  | `rules?` | `Rule[]` | — | 유효성 규칙 — 닫힐 때 자동 검증 |
4212
5982
  | `labelTooltipProps?` | `Partial<STooltipProps>` | — | 레이블 툴팁 상세 옵션 |
@@ -4222,7 +5992,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4222
5992
  | `addonAlign?` | `SFieldAddonAlign` | — | 어드온 정렬 |
4223
5993
  | `hint?` | `string` | — | |
4224
5994
  | `errorMessage?` | `string` | — | |
4225
- | `width?` | `number \| string` | — | |
5995
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. 셀렉트는 `maxLength` 개념이 없어 목록 최장값을 담는 등급으로 고른다. |
4226
5996
  | `className?` | `string` | — | |
4227
5997
  | `style?` | `CSSProperties` | — | |
4228
5998
 
@@ -4240,19 +6010,39 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4240
6010
  | `focus` | `() => void` | 트리거 버튼에 포커스 (sdFocus) |
4241
6011
  | `open` | `() => void` | 드롭다운 열기 (sdOpen) |
4242
6012
 
6013
+ ## Types
6014
+
6015
+ ### SSelectOption
6016
+
6017
+ ```ts
6018
+ export interface SSelectOption {
6019
+ value: string | number;
6020
+ label: string;
6021
+ disabled?: boolean;
6022
+ children?: SSelectOption[];
6023
+ }
6024
+ ```
6025
+
6026
+ ### SSelectType
6027
+
6028
+ ```ts
6029
+ export type SSelectType = 'default' | 'multi' | 'default_depth' | 'multi_depth';
6030
+ ```
6031
+
4243
6032
  ## Dependencies
4244
6033
 
4245
6034
  ### Used by
4246
6035
 
6036
+ - [SChipFilter](../SChipFilter)
4247
6037
  - [SKeyValueTable](../SKeyValueTable)
4248
6038
  - [STable](../STable)
4249
6039
 
4250
6040
  ### Depends on
4251
6041
 
4252
6042
  - [SField](../SField)
4253
- - [SGhostButton](../SGhostButton)
4254
6043
  - [SIcon](../SIcon)
4255
6044
  - [SPortal](../SPortal)
6045
+ - [SSearchInput](../SSearchInput)
4256
6046
 
4257
6047
  ### Graph
4258
6048
 
@@ -4285,6 +6075,21 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4285
6075
  |-------|------|-------------|
4286
6076
  | `onValueChange` | `(value: number) => void` | 크기가 확정될 때. emitImmediately 가 아니면 드래그를 놓는 순간 한 번만 온다 |
4287
6077
 
6078
+ ## Types
6079
+
6080
+ ### SSplitterUnit
6081
+
6082
+ ```ts
6083
+ /** 모델·limits 를 읽는 단위. Quasar QSplitter 의 `unit` 과 같다. */
6084
+ export type SSplitterUnit = (typeof SSPLITTER_UNITS)[number];
6085
+ ```
6086
+
6087
+ ### SSPLITTER_UNITS
6088
+
6089
+ ```ts
6090
+ export const SSPLITTER_UNITS = ['%', 'px'] as const;
6091
+ ```
6092
+
4288
6093
  ---
4289
6094
 
4290
6095
  # SStepper
@@ -4319,6 +6124,32 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4319
6124
  | `showItemTooltip` | `() => void` | error 아이템 중 첫 번째에 툴팁을 연다. error 아이템이 없으면 아무 일도 일어나지 않는다. |
4320
6125
  | `hideItemTooltip` | `() => void` | 열려있는 커스텀 툴팁을 닫는다. 활성 단계(value prop)가 바뀌면 별도 호출 없이도 자동으로 닫힌다. |
4321
6126
 
6127
+ ## Types
6128
+
6129
+ ### SStepperItem
6130
+
6131
+ ```ts
6132
+ export type SStepperItem = {
6133
+ label: string;
6134
+ value?: string;
6135
+ group?: string;
6136
+ completed?: boolean;
6137
+ error?: boolean;
6138
+ };
6139
+ ```
6140
+
6141
+ ### SStepperSize
6142
+
6143
+ ```ts
6144
+ export type SStepperSize = (typeof STEPPER_SIZES)[number];
6145
+ ```
6146
+
6147
+ ### STEPPER_SIZES
6148
+
6149
+ ```ts
6150
+ export const STEPPER_SIZES = ['sm', 'lg'] as const;
6151
+ ```
6152
+
4322
6153
  ## Dependencies
4323
6154
 
4324
6155
  ### Depends on
@@ -4354,6 +6185,14 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4354
6185
  |-------|------|-------------|
4355
6186
  | `onValueChange` | `(value: boolean) => void` | 변경 (sdUpdate) |
4356
6187
 
6188
+ ## Dependencies
6189
+
6190
+ ### Used by
6191
+
6192
+ - [SKeyValueTable](../SKeyValueTable)
6193
+
6194
+ ### Graph
6195
+
4357
6196
  ---
4358
6197
 
4359
6198
  # STable
@@ -4371,20 +6210,23 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4371
6210
  | `rowKey?` | `string` | `'id'` | 행 식별 필드 |
4372
6211
  | `selectable?` | `boolean` | `false` | 행 선택 체크박스 |
4373
6212
  | `selected?` | `SRow[]` | `[]` | |
6213
+ | `sort?` | `STableSort \| null` | `null` | 정렬 상태 (controlled). `null`·미지정이면 정렬 없음. 컴포넌트는 정렬 상태를 갖지 않는다 — 서버 정렬이면 이 값이 곧 조회 조건이고, 뒤로가기·새로고침·링크 공유로 복원돼야 하므로 진실은 URL·store 쪽에 있어야 한다. 행을 실제로 정렬하는 것도 소비 앱 몫이다 (`STable` 은 받은 순서대로 그린다). |
4374
6214
  | `resizable?` | `boolean` | `false` | 컬럼 너비 조절 |
4375
6215
  | `width?` | `string` | — | |
4376
6216
  | `height?` | `string` | — | |
4377
6217
  | `stickyHeader?` | `boolean` | — | |
4378
6218
  | `stickyColumn?` | `STableStickyColumn` | — | 고정할 좌/우 컬럼 수 |
4379
6219
  | `radius?` | `'default' \| 'useTop' \| 'full'` | `'default'` | border-radius 제어 |
6220
+ | `bordered?` | `boolean` | `true` | 바깥 테두리. 기본은 `true`. `SSectionHeaderCard` 의 `padding="none"` 안에 넣어 카드 가장자리까지 채울 때 `false` 로 끈다 — 카드가 이미 바깥 테두리를 그리므로, 켜 두면 1px 두 개가 나란히 놓여 그 변만 2px 로 보인다. 페이지네이션 바의 테두리와 1px 겹침(`-mt-px`)도 함께 꺼진다. 본문 테두리가 없으면 겹칠 대상이 없어, 그대로 두면 페이지네이션만 테두리를 갖고 1px 어긋난다. |
4380
6221
  | `noDataLabel?` | `string` | `'데이터가 없습니다.'` | |
4381
6222
  | `noDataSlot?` | `ReactNode` | — | 데이터가 없을 때 body 영역 전체를 대체하는 슬롯. 지정하면 `noDataLabel` 대신 이 콘텐츠가 헤더 아래 영역을 채우며, 버튼 등 인터랙션도 동작한다. |
4382
6223
  | `isLoading?` | `boolean` | `false` | |
4383
- | `dense?` | `boolean` | `false` | |
6224
+ | `dense?` | `boolean` | `false` | 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로) |
4384
6225
  | `noHover?` | `boolean` | `false` | true면 행에 마우스를 올려도 hover 배경(grey_05)을 표시하지 않는다 |
4385
6226
  | `pagination?` | `STablePagination` | — | 페이지네이션 (있으면 하단 표시) |
4386
6227
  | `useInternalPagination?` | `boolean` | `false` | 테이블 내부에서 페이지네이션을 직접 관리 (rows를 내부 슬라이싱) |
4387
6228
  | `useRowsPerPageSelect?` | `boolean` | `false` | 페이지당 행 수 셀렉트 표시 |
6229
+ | `useDensityToggle?` | `boolean` | `false` | 페이지네이션 바에 밀도 토글(`좁게 보기` · `넓게 보기`) 표시. **페이지네이션이 있을 때만 나타난다** — 토글이 사는 곳이 그 바이기 때문이다. `onDenseChange` 와 함께 준다. 밀도는 컴포넌트가 갖지 않으므로, 핸들러 없이 켜면 눌러도 아무 일도 일어나지 않는다. |
4388
6230
  | `rowsPerPageOption?` | `SSelectOption[]` | `DEFAULT_ROWS_PER_PAGE_OPTION` | |
4389
6231
  | `useVirtualScroll?` | `boolean` | `false` | 가상 스크롤 |
4390
6232
  | `rowHeight?` | `number` | — | |
@@ -4399,6 +6241,8 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4399
6241
  | Event | Type | Description |
4400
6242
  |-------|------|-------------|
4401
6243
  | `onSelectedChange` | `(rows: SRow[]) => void` | |
6244
+ | `onSortChange` | `(sort: STableSort \| null) => void` | 정렬 헤더 클릭 (`asc → desc → 해제` 3단). 해제되면 `null` 이 온다. 다중 정렬은 1차 안에서 지원하지 않는다. |
6245
+ | `onDenseChange` | `(dense: boolean) => void` | 밀도 변경 (`useDensityToggle` 로 띄운 토글을 눌렀을 때). 컴포넌트는 밀도 상태를 갖지 않는다 — `dense` 가 곧 현재 상태이고, 그 진실은 페이지에 있다. 사용자가 고른 밀도를 다음 방문까지 기억해 두는 것(로컬 저장 등)도 페이지 몫이다. |
4402
6246
  | `onPageChange` | `(page: number) => void` | |
4403
6247
  | `onRowsPerPageChange` | `(perPage: number) => void` | |
4404
6248
  | `onVirtualUpdate` | `(range: { from: number; to: number }) => void` | |
@@ -4417,15 +6261,77 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4417
6261
  | `updateRowSelect` | `(row: SRow) => void` | 단일 행 선택 토글 (sd-table updateRowSelect) — onSelectedChange 발생 |
4418
6262
  | `toggleSelectAll` | `(checked: boolean, rows: SRow[]) => void` | 주어진 행들 전체 선택/해제 (sd-table toggleSelectAll) — onSelectedChange 발생 |
4419
6263
 
6264
+ ## Types
6265
+
6266
+ ### STableColumn
6267
+
6268
+ ```ts
6269
+ /**
6270
+ * `autoWidth` 스페이서 열은 값을 그리지 않으므로 `field` 를 생략할 수 있다.
6271
+ * 그 밖의 열은 값 접근자가 있어야 한다.
6272
+ */
6273
+ export type STableColumn = STableColumnBase &
6274
+ (
6275
+ { autoWidth: true; field?: STableColumnField } | { autoWidth?: false; field: STableColumnField }
6276
+ );
6277
+ ```
6278
+
6279
+ ### SRow
6280
+
6281
+ ```ts
6282
+ export type SRow = Record<string, any>;
6283
+ ```
6284
+
6285
+ ### STableSort
6286
+
6287
+ ```ts
6288
+ /** 정렬 상태 — 어느 열을 어느 방향으로 정렬했는가 */
6289
+ export interface STableSort {
6290
+ /** 정렬 기준 컬럼의 `name` */
6291
+ name: string;
6292
+ dir: 'asc' | 'desc';
6293
+ }
6294
+ ```
6295
+
6296
+ ### STableStickyColumn
6297
+
6298
+ ```ts
6299
+ export interface STableStickyColumn {
6300
+ left?: number;
6301
+ right?: number;
6302
+ }
6303
+ ```
6304
+
6305
+ ### STablePagination
6306
+
6307
+ ```ts
6308
+ export interface STablePagination {
6309
+ page: number;
6310
+ rowsPerPage: number;
6311
+ lastPage?: number;
6312
+ }
6313
+ ```
6314
+
6315
+ ### STableColumnField
6316
+
6317
+ ```ts
6318
+ /** 값 접근: 필드명 또는 접근 함수 */
6319
+ export type STableColumnField = string | ((row: SRow) => any);
6320
+ ```
6321
+
4420
6322
  ## Dependencies
4421
6323
 
4422
6324
  ### Depends on
4423
6325
 
4424
6326
  - [SCheckbox](../SCheckbox)
4425
6327
  - [SCircleProgress](../SCircleProgress)
6328
+ - [SDivider](../SDivider)
6329
+ - [SGhostButton](../SGhostButton)
4426
6330
  - [SIcon](../SIcon)
4427
6331
  - [SPagination](../SPagination)
4428
6332
  - [SSelect](../SSelect)
6333
+ - [STextLink](../STextLink)
6334
+ - [STooltip](../STooltip)
4429
6335
 
4430
6336
  ### Graph
4431
6337
 
@@ -4468,7 +6374,6 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4468
6374
  |------|------|---------|-------------|
4469
6375
  | `value` | `string` | — | 현재 선택된 탭 value |
4470
6376
  | `tabs` | `STabOption[]` | — | 탭 옵션 목록 |
4471
- | `size?` | `STabSize` | `'md'` | 탭 크기 (main 전용) |
4472
6377
  | `isSub?` | `boolean` | `false` | 서브 탭(밑줄형) 스타일 |
4473
6378
  | `vertical?` | `boolean` | `false` | 세로 배치 (sub 전용 — main 폴더형은 항상 가로) |
4474
6379
  | `className?` | `string` | — | |
@@ -4480,6 +6385,18 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4480
6385
  |-------|------|-------------|
4481
6386
  | `onValueChange` | `(value: string) => void` | 탭 변경 (sdUpdate) |
4482
6387
 
6388
+ ## Types
6389
+
6390
+ ### STabOption
6391
+
6392
+ ```ts
6393
+ export interface STabOption {
6394
+ label: string;
6395
+ value: string;
6396
+ badge?: string | number;
6397
+ }
6398
+ ```
6399
+
4483
6400
  ## Dependencies
4484
6401
 
4485
6402
  ### Depends on
@@ -4508,6 +6425,53 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4508
6425
  | `iconLeft?` | `boolean` | `true` | 아이콘을 레이블 왼쪽에 배치 |
4509
6426
  | `width?` | `string \| number` | — | 태그 너비 (숫자면 px, 문자열이면 그대로 적용). 미지정 시 콘텐츠 크기 |
4510
6427
 
6428
+ ## Types
6429
+
6430
+ ### STagShape
6431
+
6432
+ ```ts
6433
+ export type STagShape = (typeof TAG_SHAPES)[number];
6434
+ ```
6435
+
6436
+ ### STagSize
6437
+
6438
+ ```ts
6439
+ export type STagSize = (typeof TAG_SIZES)[number];
6440
+ ```
6441
+
6442
+ ### STagColor
6443
+
6444
+ ```ts
6445
+ export type STagColor = (typeof TAG_COLORS)[number];
6446
+ ```
6447
+
6448
+ ### TAG_SHAPES
6449
+
6450
+ ```ts
6451
+ export const TAG_SHAPES = ['square', 'pill'] as const;
6452
+ ```
6453
+
6454
+ ### TAG_SIZES
6455
+
6456
+ ```ts
6457
+ export const TAG_SIZES = ['xs', 'sm', 'md'] as const;
6458
+ ```
6459
+
6460
+ ### TAG_COLORS
6461
+
6462
+ ```ts
6463
+ export const TAG_COLORS = [
6464
+ 'grey',
6465
+ 'red',
6466
+ 'orange',
6467
+ 'yellow',
6468
+ 'green',
6469
+ 'blue',
6470
+ 'darkblue',
6471
+ 'indigo',
6472
+ ] as const;
6473
+ ```
6474
+
4511
6475
  ## Dependencies
4512
6476
 
4513
6477
  ### Used by
@@ -4540,6 +6504,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4540
6504
  | `label?` | `string` | `''` | 레이블 |
4541
6505
  | `icon?` | `SIconName` | — | 좌측 아이콘 |
4542
6506
  | `iconColor?` | `SColor` | — | 좌측 아이콘 색상. 팔레트 키(`grey_65`, `red_95` …) 또는 임의 CSS 색상 |
6507
+ | `iconRotate?` | `0 \| 90 \| 180 \| 270` | — | 좌측 아이콘 회전 각도. 같은 아이콘을 방향만 바꿔 쓸 때 (`SIcon` 의 `rotate` 로 그대로 간다) |
4543
6508
  | `labelClass?` | `string` | — | 레이블 span에 추가할 클래스 |
4544
6509
  | `rightArrow?` | `STextLinkArrow` | `'none'` | 우측 화살표 |
4545
6510
  | `underline?` | `boolean` | `false` | 밑줄 여부 |
@@ -4553,6 +6518,20 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4553
6518
  |-------|------|-------------|
4554
6519
  | `onClick` | `() => void` | |
4555
6520
 
6521
+ ## Types
6522
+
6523
+ ### STextLinkArrow
6524
+
6525
+ ```ts
6526
+ export type STextLinkArrow = 'none' | 'chevron' | 'caret';
6527
+ ```
6528
+
6529
+ ### STextLinkSize
6530
+
6531
+ ```ts
6532
+ export type STextLinkSize = 'sm' | 'md' | 'lg';
6533
+ ```
6534
+
4556
6535
  ## Dependencies
4557
6536
 
4558
6537
  ### Used by
@@ -4560,6 +6539,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4560
6539
  - [SChipFilter](../SChipFilter)
4561
6540
  - [SChipInput](../SChipInput)
4562
6541
  - [SPopover](../SPopover)
6542
+ - [STable](../STable)
4563
6543
 
4564
6544
  ### Depends on
4565
6545
 
@@ -4599,7 +6579,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4599
6579
  | `hint?` | `string` | — | |
4600
6580
  | `error?` | `boolean` | — | |
4601
6581
  | `errorMessage?` | `string` | — | |
4602
- | `width?` | `number \| string` | — | |
6582
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
4603
6583
  | `disabled?` | `boolean` | `false` | |
4604
6584
  | `readOnly?` | `boolean` | `false` | |
4605
6585
  | `className?` | `string` | — | |
@@ -4644,7 +6624,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4644
6624
  | `clearable?` | `boolean` | `false` | |
4645
6625
  | `useMeridiem?` | `boolean` | — | 오전/오후 선택 표시 여부. 지정하지 않으면 type="midday"일 때만 켜집니다. |
4646
6626
  | `minuteStep?` | `number` | `1` | |
4647
- | `width?` | `number \| string` | — | |
6627
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
4648
6628
  | `name?` | `string` | — | |
4649
6629
  | `rules?` | `Rule[]` | — | |
4650
6630
  | `status?` | `SFieldStatus` | — | |
@@ -4669,6 +6649,20 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4669
6649
  | `onValueChange` | `(time: string \| null) => void` | 선택 변경 (sdUpdate) |
4670
6650
  | `onOpenChange` | `(open: boolean) => void` | 열림/닫힘 변경 (sdDropDownShow) |
4671
6651
 
6652
+ ## Types
6653
+
6654
+ ### STimePickerType
6655
+
6656
+ ```ts
6657
+ export type STimePickerType = 'default' | 'midday';
6658
+ ```
6659
+
6660
+ ### STimePickerSize
6661
+
6662
+ ```ts
6663
+ export type STimePickerSize = 'sm' | 'md';
6664
+ ```
6665
+
4672
6666
  ## Dependencies
4673
6667
 
4674
6668
  ### Used by
@@ -4704,7 +6698,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4704
6698
  | `useMeridiem?` | `boolean` | — | 오전/오후 선택 표시 여부. 지정하지 않으면 type="midday"일 때만 켜집니다. |
4705
6699
  | `minuteStep?` | `number` | `1` | |
4706
6700
  | `rangeOrder?` | `STimeRangePickerRangeOrder` | `'strict'` | 범위 순서 정책. strict는 시작 시간이 종료 시간보다 늦어지지 않도록 보정합니다. |
4707
- | `width?` | `number \| string` | — | |
6701
+ | `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
4708
6702
  | `name?` | `string` | — | |
4709
6703
  | `rules?` | `Rule[]` | — | |
4710
6704
  | `status?` | `SFieldStatus` | — | |
@@ -4729,6 +6723,32 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4729
6723
  | `onValueChange` | `(range: STimeRangeValue) => void` | 선택 변경 (sdUpdate) |
4730
6724
  | `onOpenChange` | `(open: boolean) => void` | 열림/닫힘 변경 (sdDropDownShow) |
4731
6725
 
6726
+ ## Types
6727
+
6728
+ ### STimeRangeValue
6729
+
6730
+ ```ts
6731
+ export type STimeRangeValue = [string, string] | null;
6732
+ ```
6733
+
6734
+ ### STimeRangePickerType
6735
+
6736
+ ```ts
6737
+ export type STimeRangePickerType = STimePickerType;
6738
+ ```
6739
+
6740
+ ### STimeRangePickerSize
6741
+
6742
+ ```ts
6743
+ export type STimeRangePickerSize = STimePickerSize;
6744
+ ```
6745
+
6746
+ ### STimeRangePickerRangeOrder
6747
+
6748
+ ```ts
6749
+ export type STimeRangePickerRangeOrder = 'strict' | 'allow-cross-day';
6750
+ ```
6751
+
4732
6752
  ## Dependencies
4733
6753
 
4734
6754
  ### Depends on
@@ -4795,6 +6815,30 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4795
6815
  | `onClose` | `() => void` | 닫기 (sdClose) |
4796
6816
  | `onButtonClick` | `(e: MouseEvent) => void` | 버튼 클릭 (sdButtonClick) |
4797
6817
 
6818
+ ## Types
6819
+
6820
+ ### SToastPosition
6821
+
6822
+ ```ts
6823
+ export type SToastPosition =
6824
+ 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
6825
+ ```
6826
+
6827
+ ### SToastNotifyOptions
6828
+
6829
+ ```ts
6830
+ export interface SToastNotifyOptions extends Omit<SToastProps, 'onClose'> {
6831
+ /** 자동 닫힘 지연(ms). 0이면 자동 닫힘 없음 (없으면 defaultDuration) */
6832
+ duration?: number;
6833
+ }
6834
+ ```
6835
+
6836
+ ### SToastType
6837
+
6838
+ ```ts
6839
+ export type SToastType = 'default' | 'danger' | 'caution' | 'complete' | 'accent' | 'info';
6840
+ ```
6841
+
4798
6842
  ## Dependencies
4799
6843
 
4800
6844
  ### Depends on
@@ -4830,6 +6874,14 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4830
6874
  |-------|------|-------------|
4831
6875
  | `onValueChange` | `(value: boolean) => void` | 변경 (sdUpdate) |
4832
6876
 
6877
+ ## Types
6878
+
6879
+ ### SToggleSize
6880
+
6881
+ ```ts
6882
+ export type SToggleSize = 'xs' | 'sm';
6883
+ ```
6884
+
4833
6885
  ---
4834
6886
 
4835
6887
  # STooltip
@@ -4874,6 +6926,26 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4874
6926
  | `show` | `() => void` | 툴팁을 표시합니다. |
4875
6927
  | `hide` | `() => void` | 툴팁을 숨깁니다. |
4876
6928
 
6929
+ ## Types
6930
+
6931
+ ### STooltipTrigger
6932
+
6933
+ ```ts
6934
+ export type STooltipTrigger = 'hover' | 'click' | 'none';
6935
+ ```
6936
+
6937
+ ### STooltipPlacement
6938
+
6939
+ ```ts
6940
+ export type STooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
6941
+ ```
6942
+
6943
+ ### STooltipType
6944
+
6945
+ ```ts
6946
+ export type STooltipType = 'default' | 'danger' | 'warning' | 'accent';
6947
+ ```
6948
+
4877
6949
  ## Dependencies
4878
6950
 
4879
6951
  ### Used by
@@ -4883,6 +6955,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4883
6955
  - [SKeyValueTable](../SKeyValueTable)
4884
6956
  - [SSectionHeaderCard](../SSectionHeaderCard)
4885
6957
  - [SStepper](../SStepper)
6958
+ - [STable](../STable)
4886
6959
 
4887
6960
  ### Depends on
4888
6961
 
@@ -4913,6 +6986,7 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4913
6986
  | `border?` | `boolean` | `true` | 아이템 하단 border 표시 여부 |
4914
6987
  | `indeterminate?` | `boolean` | `false` | 일부 선택 상태 |
4915
6988
  | `selectable?` | `boolean` | `true` | 체크박스 표시 여부 |
6989
+ | `size?` | `STreeSize` | `'sm'` | 타이포그래피 크기 |
4916
6990
  | `leading?` | `ReactNode \| ((state: STreeItemRenderState) => ReactNode)` | — | 사용자 지정 leading 콘텐츠 |
4917
6991
  | `trailing?` | `ReactNode \| ((state: STreeItemRenderState) => ReactNode)` | — | 사용자 지정 trailing 콘텐츠 |
4918
6992
 
@@ -4935,6 +7009,8 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4935
7009
  | `expandedIds?` | `string[]` | — | 펼쳐진 노드 ID 목록 |
4936
7010
  | `defaultExpandedIds?` | `string[]` | — | 비제어 펼침 초기값 |
4937
7011
  | `selectable?` | `boolean` | `true` | 체크박스 표시 여부 |
7012
+ | `size?` | `STreeSize` | `'sm'` | 타이포그래피 크기 |
7013
+ | `useAll?` | `boolean` | `false` | 체크박스 사용 시 최상단 전체 선택 아이템 표시 여부 |
4938
7014
  | `guideline?` | `boolean` | `true` | depth 연결선 표시 여부 |
4939
7015
  | `border?` | `boolean` | `true` | 아이템 하단 border 표시 여부 |
4940
7016
  | `cascadeSelection?` | `boolean` | `true` | 부모 선택 시 하위 노드까지 함께 토글 |
@@ -4948,6 +7024,44 @@ function OrderModal({ open, onOpenChange, onClose, modalRef, orderId }: OrderMod
4948
7024
  | `onValueChange` | `(value: string[], node: STreeNode) => void` | 선택 변경 |
4949
7025
  | `onExpandedChange` | `(expandedIds: string[], node: STreeNode) => void` | 펼침 변경 |
4950
7026
 
7027
+ ## Types
7028
+
7029
+ ### STreeNode
7030
+
7031
+ ```ts
7032
+ export interface STreeNode {
7033
+ /** 노드 고유 ID */
7034
+ id: string;
7035
+ /** 노드 라벨 */
7036
+ label: ReactNode;
7037
+ /** 하위 노드 */
7038
+ children?: STreeNode[];
7039
+ /** 비활성 상태 */
7040
+ disabled?: boolean;
7041
+ }
7042
+ ```
7043
+
7044
+ ### STreeSize
7045
+
7046
+ ```ts
7047
+ export type STreeSize = 'sm' | 'md';
7048
+ ```
7049
+
7050
+ ### STreeItemRenderState
7051
+
7052
+ ```ts
7053
+ export interface STreeItemRenderState {
7054
+ node: STreeNode;
7055
+ depth: number;
7056
+ size: STreeSize;
7057
+ expanded: boolean;
7058
+ selected: boolean;
7059
+ indeterminate: boolean;
7060
+ disabled: boolean;
7061
+ hasChildren: boolean;
7062
+ }
7063
+ ```
7064
+
4951
7065
  ## Dependencies
4952
7066
 
4953
7067
  ### Depends on