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

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 (50) hide show
  1. package/AGENTS.md +176 -111
  2. package/README.md +38 -8
  3. package/dist/components/SChipFilter/README.md +69 -0
  4. package/dist/components/SChipFilter/SChipFilter.d.ts +160 -0
  5. package/dist/components/SChipFilter/index.d.ts +1 -0
  6. package/dist/components/SCircleProgress/README.md +3 -0
  7. package/dist/components/SCircleProgress/SCircleProgress.d.ts +7 -1
  8. package/dist/components/SDatePicker/README.md +2 -0
  9. package/dist/components/SDateRangePicker/README.md +21 -0
  10. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +16 -0
  11. package/dist/components/SDateRangePicker/index.d.ts +1 -1
  12. package/dist/components/SGhostButton/README.md +4 -0
  13. package/dist/components/SGuide/README.md +1 -1
  14. package/dist/components/SGuide/SGuide.d.ts +1 -1
  15. package/dist/components/SIcon/README.md +4 -0
  16. package/dist/components/SIcon/icons.gen.d.ts +1 -0
  17. package/dist/components/SImage/README.md +44 -0
  18. package/dist/components/SImage/SImage.d.ts +41 -0
  19. package/dist/components/SImage/index.d.ts +1 -0
  20. package/dist/components/SList/README.md +1 -0
  21. package/dist/components/SList/SList.d.ts +2 -0
  22. package/dist/components/SListItem/README.md +2 -2
  23. package/dist/components/SListItem/SListItem.d.ts +2 -2
  24. package/dist/components/SPage/README.md +21 -0
  25. package/dist/components/SPage/SPage.d.ts +10 -0
  26. package/dist/components/SPage/SPageHeader.d.ts +32 -0
  27. package/dist/components/SPage/index.d.ts +1 -0
  28. package/dist/components/SPortal/README.md +1 -1
  29. package/dist/components/SPortal/SPortal.d.ts +7 -1
  30. package/dist/components/SRadio/README.md +2 -0
  31. package/dist/components/SRadioButton/README.md +13 -0
  32. package/dist/components/SSelect/README.md +2 -2
  33. package/dist/components/SSelect/SSelect.d.ts +2 -2
  34. package/dist/components/STag/README.md +2 -0
  35. package/dist/components/STextLink/README.md +2 -0
  36. package/dist/components/SToggle/README.md +1 -0
  37. package/dist/components/SToggle/SToggle.d.ts +4 -1
  38. package/dist/components/STooltip/README.md +2 -0
  39. package/dist/index.cjs +1830 -319
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +1826 -319
  43. package/dist/index.js.map +1 -1
  44. package/dist/lib/floating-width.d.ts +22 -0
  45. package/dist/llms-full.txt +343 -118
  46. package/dist/llms.txt +180 -114
  47. package/dist/styles.css +218 -16
  48. package/dist/theme.css +12 -5
  49. package/eslint/scale.gen.mjs +1 -1
  50. package/package.json +7 -4
package/AGENTS.md CHANGED
@@ -29,10 +29,10 @@
29
29
  | **버튼·링크** | `SButton` `SGhostButton` `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
30
30
  | **입력 (폼)** | `SForm` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
31
31
  | **날짜·시간** | `SCalendar` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
32
- | **표·목록** | `STable` `STableBar` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
33
- | **레이아웃** | `SLayout` `SGnb` `SPage` `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
32
+ | **표·목록** | `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
33
+ | **레이아웃** | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
34
34
  | **내비게이션** | `STabs` `SPagination` `SStepper` |
35
- | **표시·상태** | `STag` `SBadge` `SIcon` `SCallout` `SGuide` |
35
+ | **표시·상태** | `STag` `SBadge` `SIcon` `SImage` `SCallout` `SGuide` |
36
36
  | **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
37
37
  | **오버레이** | `STooltip` `SPopover` `SPopup` `SDrawer` `SPortal` |
38
38
  | **모달** | `SModal.confirm()` `SModal.create()` + `SActionModal` `SConfirmModal` `SModalOutlet`(앱 루트 1회) |
@@ -136,9 +136,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
136
136
 
137
137
  | 층 | 무엇인가 | 컴포넌트 |
138
138
  | --- | --- | --- |
139
- | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage` |
140
- | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
141
- | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SField` `SInput` `SNumberInput` `STextarea` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SLinearProgress` `SCircleProgress` |
139
+ | **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SPage`(제목 영역은 `header` prop) |
140
+ | **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
141
+ | **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `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` |
142
142
  | **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
143
143
  | **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
144
144
 
@@ -186,9 +186,9 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
186
186
  블록 순서는 화면 종류와 무관하게 같다. **필요한 것만 남기되 순서를 바꾸지 않는다.**
187
187
 
188
188
  ```text
189
- 1. 페이지 제목 (+ 가이드·매뉴얼 링크. 액션 버튼은 오지 않는다 §4-2)
189
+ 1. 페이지 제목 `SPage` 의 `header` prop (+ 가이드·매뉴얼 링크는 slot. 액션 버튼은 오지 않는다 §4-2)
190
190
  2. 상시 안내 SCallout
191
- 3. 필터 SKeyValueTable
191
+ 3. 필터 SKeyValueTable · SChipFilter (§3-7-11)
192
192
  4. 툴바 STableBar (건수 요약 + 액션)
193
193
  5. 본문 STable · 섹션 카드들 · SList …
194
194
  6. 페이지네이션 SPagination (STable 이 pagination prop 으로 직접 그린다)
@@ -214,7 +214,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
214
214
 
215
215
  | 층 (§2-0) | 역할 | 클래스 | 크기 |
216
216
  | --- | --- | --- | --- |
217
- | 셸 | 페이지 제목 (h1) | `typo-heading-lg` | 18px |
217
+ | 셸 | 페이지 제목 (`SPage` 의 `header.title`) | `typo-heading-lg` | 18px |
218
218
  | 블록 | 섹션 제목 | `typo-heading-sm` | 14px |
219
219
  | 블록 내부 | 하위 제목 (섹션 안을 더 나눌 때) | `typo-heading-xs` | 12px |
220
220
  | — | 본문 | `typo-body-sm-default` | 12px |
@@ -222,7 +222,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
222
222
 
223
223
  페이지 제목만 18px 로 크게 두고 그 아래는 14 / 12 로 촘촘하게 간다. 중간 크기(16px)는 기본 골격에서 쓰지 않는다.
224
224
 
225
- - **섹션 제목의 타이포를 직접 주지 않는다.** `SSectionHeaderCard.Header` `title` 이미 넣는다 — 그 위에 `typo-heading-sm` 을 또 씌우지 않는다. 직접 쓰는 경우는 섹션 카드 없이 제목만 세울 때뿐이다.
225
+ - **페이지·섹션 제목의 타이포를 직접 주지 않는다.** `SPage` 의 `header.title`, `SSectionHeaderCard.Header` `title` 이미 넣는다 — 그 위에 `typo-heading-lg`/`typo-heading-sm` 을 또 씌우지 않는다.
226
226
  - **하위 제목이 필요하면 먼저 섹션을 나눌 수 없는지 본다.** 한 섹션 안에서 제목이 두 단으로 갈린다는 것은 대개 섹션이 둘이라는 뜻이다 (§3-7-8).
227
227
  - 본문 안에서 한 단어를 강조할 때는 `typo-body-sm-medium` 을 쓴다. `typo-body-sm-bold` 는 제목 성격의 짧은 라벨에만 쓴다. <!-- TODO(디자인): 강조 굵기 기준 확정 -->
228
228
 
@@ -426,6 +426,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
426
426
  | 컨트롤에 라벨·필수·에러를 붙인다 | `SField` | §3-7-5 |
427
427
  | 입력 여러 개를 묶어 한 번에 검증한다 | `SForm` | §4-3 |
428
428
  | 폼·필터를 표 형태로 배치한다 | `SKeyValueTable` | §4 |
429
+ | 필요한 검색 조건만 칩으로 골라 붙이게 한다 | `SChipFilter` | §3-7-11 |
429
430
 
430
431
  #### B. 정보를 읽게 보여준다
431
432
 
@@ -441,6 +442,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
441
442
  | 상태·분류를 라벨로 찍는다 | `STag` | §3-1 |
442
443
  | 색 점만으로 상태를 찍는다 | `SBadge` | §3-1 |
443
444
  | 아이콘을 넣는다 | `SIcon` | |
445
+ | 사진·썸네일을 보여준다 (로딩·실패 상태 포함) | `SImage` | §3-7-12 |
444
446
  | 문장 안에서 다른 화면으로 보낸다 | `STextLink` | §3-5-6 |
445
447
 
446
448
  #### C. 동작을 실행시킨다
@@ -458,6 +460,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
458
460
  | 앱 셸(상단바 + 내비 + 본문)을 세운다 | `SLayout` | §4-1 |
459
461
  | 좌측 내비게이션을 만든다 | `SGnb` | §4-1 |
460
462
  | 페이지 본문을 담는다 (패딩·스크롤) | `SPage` | §4-1 |
463
+ | 페이지 제목(+ 서브 텍스트·뒤로가기·우측 슬롯)을 만든다 | `SPage` 의 `header` prop | §4-1 |
461
464
  | 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
462
465
  | 제목 없이 흰 면으로만 묶는다 | `SCard` | §3-7-8 |
463
466
  | 가로선으로 끊는다 | `SDivider` | §3-6 |
@@ -1024,6 +1027,16 @@ const columns: STableColumn[] = [
1024
1027
 
1025
1028
  - **항목 하나의 속성을 나열하는 것은 목록이 아니다.** `라벨: 값` 이 세로로 쌓이는 것은 `SKeyValueTable` 이다 (§4-4).
1026
1029
  - `SList` 는 레이아웃만 담당한다. 펼침·단일 선택 동작이 필요하면 `SExpansionList` 다 (§3-7-7).
1030
+ - **항목 사이 구분선은 리스트가 그린다.** `SListItem`·`SExpansionItem`·`SDraggableItem` 셋 다 스스로 구분선을 그리지 않으므로, 목록을 감싸는 `SList`·`SExpansionList`·`SDraggableList` 에 `separator` 를 준다 — 아이템에 `border-b` 를 직접 붙이지 않는다. 테두리형(`bordered`)은 테두리가 구분 역할을 하므로 `separator` 대신 `useGap` 으로 띄운다.
1031
+ - **`SListItem` 은 기본이 표시 전용이다.** 눌러서 이동·선택하게 하려면 `clickable` 을 준다 — hover·`selected`·`interaction="chevron"` 표현이 전부 여기에 딸려 있어서, `clickable` 없이 `selected` 만 주면 선택 표시가 나오지 않는다. `SExpansionList` 는 선택을 자기가 관리하므로 자식 아이템을 알아서 클릭 가능하게 만든다.
1032
+
1033
+ ```tsx
1034
+ ✅ <SList separator><SListItem title="일반 문의" />…</SList> {/* 줄로 구분되는 목록 */}
1035
+ ✅ <SList useGap><SListItem title="일반 문의" bordered />…</SList> {/* 카드처럼 떨어진 목록 */}
1036
+ ✅ <SListItem title="일반 문의" clickable selected onClick={…} /> {/* 눌러서 고르는 목록 */}
1037
+ ❌ <SList><SListItem title="일반 문의" className="border-b" />…</SList>
1038
+ ❌ <SListItem title="일반 문의" selected /> {/* clickable 없으면 선택 표시가 안 나온다 */}
1039
+ ```
1027
1040
 
1028
1041
  #### 3-7-7. 펼치는 셋 — SExpansionItem vs SExpansionList vs STree
1029
1042
 
@@ -1033,7 +1046,7 @@ const columns: STableColumn[] = [
1033
1046
  | **한 번에 하나만** 열려야 한다 (아코디언) | `SExpansionList` + `SExpansionItem` |
1034
1047
  | **부모-자식 계층 자체**를 보여줘야 한다 (2단 이상, 연결선) | `STree` |
1035
1048
 
1036
- `SExpansionList` 는 depth 별 단일 확장·선택을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다.
1049
+ `SExpansionList` 는 depth 별 단일 확장·선택을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다. 항목 사이 구분선은 여기서도 리스트가 그린다 — `separator` 를 준다 (§3-7-6).
1037
1050
 
1038
1051
  #### 3-7-8. SCard vs SSectionHeaderCard
1039
1052
 
@@ -1059,6 +1072,37 @@ const columns: STableColumn[] = [
1059
1072
 
1060
1073
  `STooltip`·`SPopover`·`SSelect`·날짜 피커가 내부에서 쓰는 저수준 레이어다. 앵커에 붙여 띄우는 동작이 필요하면 **먼저 §3-3 에서 대응 컴포넌트를 찾는다.** `SPortal` 을 직접 쓰는 것은 그 넷 중 어느 것도 아닌 새로운 부착형 레이어를 만들 때뿐이고, 그때도 모달 안에서 열릴 수 있다면 소속 컨테이너를 맞춰야 한다.
1061
1074
 
1075
+ #### 3-7-11. 필터 둘 — SKeyValueTable vs SChipFilter
1076
+
1077
+ 둘 다 §2-0 블록 순서의 **3번 자리(필터)** 에 놓이고, 한 화면에 둘을 같이 두지 않는다.
1078
+
1079
+ | 상황 | 컴포넌트 |
1080
+ | --- | --- |
1081
+ | 조건이 정해져 있고 **항상 다 보여야** 한다 (기본 형태) | `SKeyValueTable` |
1082
+ | 조건 후보가 많아 **쓸 것만 골라 붙이고** 나머지는 숨겨야 한다 | `SChipFilter` |
1083
+
1084
+ - **기본은 `SKeyValueTable` 이다** (§4-2). 조건이 대여섯 개 이하로 고정이면 표로 펼쳐 두는 편이 한눈에 읽힌다.
1085
+ - `SChipFilter` 는 조건을 **칩 한 줄**로 접고, "필터 추가" 로 필요한 것만 꺼내 쓰게 한다. 칩을 누르면 편집 팝오버가 열리고, 날짜는 프리셋(오늘·지난 7일·사용자 지정)으로 고른다. 조건 후보가 많은 목록 화면에서 필터가 화면을 세로로 잡아먹는 것을 막는 용도다.
1086
+ - 검색 실행 시점이 다르다 — `SKeyValueTable` 필터는 앱이 검색 버튼을 직접 놓지만, `SChipFilter` 는 편집 팝오버가 닫히거나 "검색" 을 누를 때 `onSearch` 가 값 맵과 함께 호출된다. 값이 바뀌지 않았으면 호출되지 않는다.
1087
+ - 함께 걸어야 하는 조건(예: 기간 중 하나는 필수)이 있으면 `fields` 를 그룹으로 넘긴다. 규칙을 못 채운 동안 경고 툴팁이 떠 있고 `onSearch` 가 막힌다.
1088
+
1089
+ #### 3-7-12. 이미지 — SImage
1090
+
1091
+ 사진·썸네일은 `<img>` 를 직접 쓰지 않고 `SImage` 를 쓴다. **로딩·실패 상태를 컴포넌트가 이미 처리하기 때문이다.**
1092
+
1093
+ - **로딩 스피너와 실패 표시를 앱이 만들지 않는다.** `src` 가 없거나 로드에 실패하면 빈 이미지 아이콘이, 로딩 중에는 스피너가 자동으로 놓인다. 크기는 상자 높이에 비례하므로 썸네일이든 큰 미리보기든 따로 맞출 것이 없다.
1094
+ - **크기는 `ratio` 와 너비로 준다.** `ratio` 기본값이 `1`(정사각형)이라 너비만 주면 정사각형이 된다. `className`/`style` 로 높이를 직접 주면 그쪽이 이기고 `ratio` 는 무시된다.
1095
+ - 상자를 채우는 방식은 `fit`(기본 `cover`), 붙는 위치는 `position` 이다. 안쪽 `<img>` 에 네이티브 속성이 필요하면 `imgProps` 로 넘긴다 — 목록에서 고스트 드래그를 막는 `imgProps={{ draggable: false }}` 가 대표적이다.
1096
+ - 썸네일이 많은 목록에서는 `loadingShowDelay` 를 준다. 캐시된 이미지가 즉시 로드될 때 스피너가 한 프레임 번쩍이는 것을 막는다.
1097
+
1098
+ ```tsx
1099
+ ✅ <SImage src={item.thumbnailUrl} alt={item.name} style={{ width: 64 }} /> {/* 정사각 썸네일 */}
1100
+ ✅ <SImage src={banner} ratio={16 / 9} className="w-full" /> {/* 가로형 배너 */}
1101
+
1102
+ ❌ <img src={item.thumbnailUrl} /> {/* 로딩·실패 상태가 없다 */}
1103
+ ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
1104
+ ```
1105
+
1062
1106
  ---
1063
1107
 
1064
1108
  ## 4. 페이지 레시피 — 표준 골격
@@ -1069,11 +1113,12 @@ const columns: STableColumn[] = [
1069
1113
  >
1070
1114
  > **핵심 원칙 — 표 형태의 정보는 `SKeyValueTable` 로 만든다.** 필터·등록/수정 폼·상세 정보가 모두 여기 해당한다.
1071
1115
  > `SField` 컨트롤을 `div` 로 직접 나열해 폼을 만들지 않는다.
1116
+ > (필터만 예외가 하나 있다 — 조건 후보가 많아 골라 붙이게 해야 하면 `SChipFilter` 다. §3-7-11)
1072
1117
 
1073
1118
  ### 4-1. 앱 셸 (모든 페이지 공통)
1074
1119
 
1075
1120
  ```tsx
1076
- import { SLayout, SGnb, SPage, type SGnbMenuItem } from 'sellmate-design-system-react';
1121
+ import { SLayout, SGnb, SPage, type SGnbMenuItem, type SPageHeaderProps } from 'sellmate-design-system-react';
1077
1122
 
1078
1123
  const MENU: SGnbMenuItem[] = [
1079
1124
  { label: '주문', value: 'orders', icon: 'bill' },
@@ -1082,20 +1127,32 @@ const MENU: SGnbMenuItem[] = [
1082
1127
 
1083
1128
  export default function AppShell({
1084
1129
  children,
1130
+ header,
1085
1131
  scrollEndSpacing,
1086
- }: { children: React.ReactNode; scrollEndSpacing?: boolean }) {
1132
+ }: { children: React.ReactNode; header?: SPageHeaderProps; scrollEndSpacing?: boolean }) {
1087
1133
  return (
1088
1134
  <SLayout type="box" header="fix">
1089
1135
  {/* type/header/folded 는 SLayout 에만 준다 — SGnb 는 context 에서 읽는다 */}
1090
1136
  <SGnb items={MENU} value={current} onValueChange={navigate} logo={<Logo />} />
1091
1137
  {/* 콘텐츠 패딩은 SPage 가 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
1092
1138
  {/* 스크롤 끝 여백도 SPage 가 넣는다. 끄는 건 페이지네이션 있는 목록뿐이라 페이지가 정한다 */}
1093
- <SPage background="frame" scrollEndSpacing={scrollEndSpacing}>{children}</SPage>
1139
+ {/* header 는 페이지마다 달라 AppShell 이 그대로 받아 넘긴다 — 페이지 제목은 여기서 만들지 않는다 */}
1140
+ <SPage background="frame" scrollEndSpacing={scrollEndSpacing} header={header}>
1141
+ {children}
1142
+ </SPage>
1094
1143
  </SLayout>
1095
1144
  );
1096
1145
  }
1097
1146
  ```
1098
1147
 
1148
+ **페이지는 `AppShell` 을 직접 호출하며 자기 `header` 를 넘긴다** — SPage 는 셸 안에 하나뿐이므로, 페이지 제목이 페이지마다 다르다는 사실은 이렇게 프레임 컴포넌트를 통해 흘려보낸다(§4-2·§4-3·§4-4 참고).
1149
+
1150
+ **페이지 헤더(`header`) 사용 규칙 — 이 앱에서는 값이 아니라 값의 일관성이 규칙이다.**
1151
+
1152
+ - **`fix` 는 앱 전체에서 하나로 고정한다.** 어떤 페이지는 `fix: true`(바), 다른 페이지는 `fix: false`(투명) 로 섞어 쓰지 않는다. 프로젝트에서 하나를 고르면(예: 전부 `fix: true`) 모든 `header` 가 그 값을 쓴다.
1153
+ - **`slot`·`onBack` 도 페이지 성격이 실제로 다른 경우가 아니면 있는 대로 통일한다.** "목록 페이지엔 없고 상세·등록 페이지엔 있다"처럼 화면 종류에 따라 갈리는 것은 허용되지만, 같은 종류의 화면끼리는 임의로 넣었다 뺐다 하지 않는다.
1154
+ - **`slot` 은 `ReactNode` 를 그대로 받지만, 원칙은 `size="sm"` 버튼 위주로만 채운다.** `STextLink`·복잡한 커스텀 마크업을 슬롯에 넣지 않는다 — 그 이상이 필요하면 페이지 헤더가 아니라 §4-2 의 `STableBar` 처럼 본문 쪽 액션 자리를 쓴다.
1155
+
1099
1156
  **GNB 폭을 사용자가 조절하게 하려면 `SGnb` 에 `resizable` 을 준다.** 메뉴 오른쪽 경계가 조절선이 되고, 레일 폭은 고정된 채 메뉴 컬럼만 늘고 준다. 범위는 컴포넌트가 정하므로 숫자를 직접 주지 않는다.
1100
1157
 
1101
1158
  ```tsx
@@ -1171,18 +1228,20 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1171
1228
 
1172
1229
  ### 4-2. 목록 페이지 (필터 + 테이블)
1173
1230
 
1174
- 구조: **페이지 헤더(제목 + 가이드 링크) → 필터(`SKeyValueTable`) → `STableBar` → `STable`**
1231
+ 구조: **페이지 헤더(`AppShell` `header` prop, 가이드 등 부가 액션은 slot) → 필터(`SKeyValueTable`) → `STableBar` → `STable`**
1232
+
1233
+ 필터 자리는 `SChipFilter` 로 바꿔 놓을 수 있다 — 조건 후보가 많아 쓸 것만 골라 붙이게 하는 화면이면 그쪽이다 (§3-7-11). 나머지 골격은 같다.
1175
1234
 
1176
1235
  액션 버튼의 위치가 핵심이다:
1177
1236
 
1178
- - **페이지 제목 줄에는 액션 버튼을 두지 않는다.** 가이드·매뉴얼 링크 부가 정보만 온다.
1237
+ - **페이지 제목 줄에는 페이지의 주요 액션을 두지 않는다.** 부가적인 것만 `header.slot` `SButton size="sm"` 으로 온다 (§4-1 "페이지 헤더 사용 규칙").
1179
1238
  - **주요 액션(등록 등)은 `STableBar` 의 `rightActions`** 에 둔다.
1180
1239
  - **선택 상태 액션(선택 삭제 등)은 `STableBar` 의 `actions`** 에 둔다. `actions` 슬롯은 건수 요약이 있으면 앞에 구분선(`SDivider`)을 **자동으로** 넣으므로 직접 구분선을 만들지 않는다.
1181
- - **페이지네이션이 있으면 스크롤 끝 여백을 끈다** — 셸의 `SPage` 에 `scrollEndSpacing={false}` 를 넘긴다 (§2-2). 페이지네이션이 이미 "여기서 끝"을 알려준다.
1240
+ - **페이지네이션이 있으면 스크롤 끝 여백을 끈다** — `AppShell` 에 `scrollEndSpacing={false}` 를 넘긴다 (§2-2). 페이지네이션이 이미 "여기서 끝"을 알려준다.
1182
1241
 
1183
1242
  ```tsx
1184
1243
  import {
1185
- SButton, STextLink, SKeyValueTable, STableBar, STable, STag,
1244
+ SButton, SKeyValueTable, STableBar, STable, STag,
1186
1245
  type STableColumn, type SRow, type SKeyValueField,
1187
1246
  } from 'sellmate-design-system-react';
1188
1247
 
@@ -1220,55 +1279,58 @@ export default function ProductListPage() {
1220
1279
  const [selected, setSelected] = useState<SRow[]>([]);
1221
1280
 
1222
1281
  return (
1223
- <div className="flex flex-col gap-sd-12">
1224
- {/* 페이지 헤더 — 액션 버튼 없음. 가이드/매뉴얼 링크 자리 */}
1225
- <div className="flex items-center justify-between">
1226
- <h1 className="typo-heading-lg m-0">상품 목록</h1>
1227
- <STextLink label="이용 가이드" rightArrow="chevron" onClick={openGuide} />
1228
- </div>
1282
+ <AppShell
1283
+ header={{
1284
+ title: '상품 목록',
1285
+ // 페이지의 주요 액션이 아니라 부가 액션 — slot 은 sm 버튼으로만 채운다
1286
+ slot: <SButton size="sm" color="neutral" outline label="이용 가이드" onClick={openGuide} />,
1287
+ }}
1288
+ scrollEndSpacing={false} // 페이지네이션이 있으므로 끈다
1289
+ >
1290
+ <div className="flex flex-col gap-sd-12">
1291
+ {/* 필터 — search 를 켜면 우측에 검색 패널이 붙는다 */}
1292
+ <SKeyValueTable
1293
+ fields={filterFields}
1294
+ values={filters}
1295
+ search
1296
+ onChange={({ values }) => setFilters(values)}
1297
+ onSearch={fetchList}
1298
+ />
1229
1299
 
1230
- {/* 필터search 켜면 우측에 검색 패널이 붙는다 */}
1231
- <SKeyValueTable
1232
- fields={filterFields}
1233
- values={filters}
1234
- search
1235
- onChange={({ values }) => setFilters(values)}
1236
- onSearch={fetchList}
1237
- />
1238
-
1239
- {/* 툴바 — 좌: 건수 + (구분선 자동) + 선택 액션 / 우: 주요 액션 */}
1240
- <STableBar
1241
- total={total}
1242
- selected={selected.length}
1243
- actions={
1244
- /* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
1245
- <SButton size="sm" color="danger" outline label="선택 삭제"
1246
- disabled={!selected.length} onClick={removeSelected} />
1247
- }
1248
- rightActions={
1249
- /* 이 페이지의 유일한 primary 채움 (§3-5-1) */
1250
- <SButton size="sm" label="상품 등록" onClick={goCreate} />
1251
- }
1252
- />
1253
-
1254
- <STable
1255
- columns={columns}
1256
- rows={rows}
1257
- rowKey="id"
1258
- selectable
1259
- selected={selected}
1260
- onSelectedChange={setSelected}
1261
- pagination={{ currentPage, lastPage }}
1262
- isLoading={isLoading}
1263
- />
1264
- </div>
1300
+ {/* 툴바좌: 건수 + (구분선 자동) + 선택 액션 / 우: 주요 액션 */}
1301
+ <STableBar
1302
+ total={total}
1303
+ selected={selected.length}
1304
+ actions={
1305
+ /* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
1306
+ <SButton size="sm" color="danger" outline label="선택 삭제"
1307
+ disabled={!selected.length} onClick={removeSelected} />
1308
+ }
1309
+ rightActions={
1310
+ /* 이 페이지의 유일한 primary 채움 (§3-5-1) */
1311
+ <SButton size="sm" label="상품 등록" onClick={goCreate} />
1312
+ }
1313
+ />
1314
+
1315
+ <STable
1316
+ columns={columns}
1317
+ rows={rows}
1318
+ rowKey="id"
1319
+ selectable
1320
+ selected={selected}
1321
+ onSelectedChange={setSelected}
1322
+ pagination={{ currentPage, lastPage }}
1323
+ isLoading={isLoading}
1324
+ />
1325
+ </div>
1326
+ </AppShell>
1265
1327
  );
1266
1328
  }
1267
1329
  ```
1268
1330
 
1269
1331
  ### 4-3. 폼 페이지 (등록/수정)
1270
1332
 
1271
- 구조: **페이지 제목 → `SForm` + `SKeyValueTable` → 하단 버튼**
1333
+ 구조: **페이지 제목(`AppShell` 의 `header` prop) → `SForm` + `SKeyValueTable` → 하단 버튼**
1272
1334
 
1273
1335
  - 필드를 `div` 로 나열하지 않고 **`SKeyValueTable` 의 행으로 구성**한다.
1274
1336
  - 검증 규칙은 각 field 의 `options.rules` 로 넘긴다. `SForm` 이 하위 컨트롤을 자동 수집해 submit 시 일괄 검증하고, 실패 시 첫 실패 필드로 포커스를 옮긴다.
@@ -1306,33 +1368,33 @@ export default function ProductCreatePage() {
1306
1368
  const [values, setValues] = useState<Record<string, unknown>>({});
1307
1369
 
1308
1370
  return (
1309
- <div className="flex flex-col gap-sd-12">
1310
- <h1 className="typo-heading-lg m-0">상품 등록</h1>
1311
-
1312
- <SForm ref={formRef} formClass="flex flex-col gap-sd-12" onSubmit={save}>
1313
- <SKeyValueTable
1314
- fields={fields}
1315
- values={values}
1316
- onChange={({ values }) => setValues(values)}
1317
- />
1318
-
1319
- {/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
1320
- <div className="flex items-center justify-between">
1321
- <SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
1322
- <div className="flex items-center gap-sd-8">
1323
- <SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
1324
- <SButton type="submit" label="저장" />
1371
+ <AppShell header={{ title: '상품 등록', onBack: goBack }}>
1372
+ <div className="flex flex-col gap-sd-12">
1373
+ <SForm ref={formRef} formClass="flex flex-col gap-sd-12" onSubmit={save}>
1374
+ <SKeyValueTable
1375
+ fields={fields}
1376
+ values={values}
1377
+ onChange={({ values }) => setValues(values)}
1378
+ />
1379
+
1380
+ {/* 하단 버튼은 양끝으로 벌린다. 부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
1381
+ <div className="flex items-center justify-between">
1382
+ <SButton type="button" color="neutral" outline label="취소" onClick={goBack} />
1383
+ <div className="flex items-center gap-sd-8">
1384
+ <SCheckbox label="계속 등록하기" value={keepOpen} onValueChange={v => setKeepOpen(v as boolean)} />
1385
+ <SButton type="submit" label="저장" />
1386
+ </div>
1325
1387
  </div>
1326
- </div>
1327
- </SForm>
1328
- </div>
1388
+ </SForm>
1389
+ </div>
1390
+ </AppShell>
1329
1391
  );
1330
1392
  }
1331
1393
  ```
1332
1394
 
1333
1395
  ### 4-4. 상세(조회) 페이지
1334
1396
 
1335
- 구조: **페이지 헤더(제목) → 섹션별 `SSectionHeaderCard` + `SKeyValueTable` → 하단 버튼**
1397
+ 구조: **페이지 헤더(`AppShell` 의 `header` prop, 목록에서 들어오는 뒤로가기는 onBack) → 섹션별 `SSectionHeaderCard` + `SKeyValueTable` → 하단 버튼**
1336
1398
 
1337
1399
  - 조회 값은 `type: 'text'` 행으로 표시한다. **상태·분류 태그도 별도 영역이 아니라 표의 한 행**으로 넣는다 (`render` 에 `STag`).
1338
1400
  - 행이 많아지면 **유형별로 섹션을 나누고, 각 섹션을 `SSectionHeaderCard` 로 감싼다.**
@@ -1366,38 +1428,39 @@ const priceFields: SKeyValueField[][] = [
1366
1428
 
1367
1429
  export default function ProductDetailPage() {
1368
1430
  return (
1369
- <div className="flex flex-col gap-sd-12">
1370
- <h1 className="typo-heading-lg m-0">클래식 셔츠</h1>
1371
-
1372
- <SSectionHeaderCard>
1373
- <SSectionHeaderCard.Header title="기본 정보" marker thickness="accent" />
1374
- <SSectionHeaderCard.Body>
1375
- <SKeyValueTable fields={basicFields} values={product} />
1376
- </SSectionHeaderCard.Body>
1377
- </SSectionHeaderCard>
1378
-
1379
- <SSectionHeaderCard>
1380
- {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1381
- <SSectionHeaderCard.Header
1382
- title="가격 정보"
1383
- marker
1384
- helpText={['부가세 포함 금액입니다.']}
1385
- slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1386
- />
1387
- <SSectionHeaderCard.Body>
1388
- <SKeyValueTable fields={priceFields} values={product} />
1389
- </SSectionHeaderCard.Body>
1390
- </SSectionHeaderCard>
1391
-
1392
- {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
1393
- <div className="flex items-center justify-between">
1394
- <SButton color="neutral" outline label="목록" onClick={goList} />
1395
- <div className="flex items-center gap-sd-8">
1396
- <SButton color="danger" outline label="삭제" onClick={confirmDelete} />
1397
- <SButton label="수정" onClick={goEdit} />
1431
+ // 목록에서 들어온 상세 페이지 — onBack 으로 뒤로가기를 준다
1432
+ <AppShell header={{ fix: true, title: '클래식 셔츠', onBack: goList }}>
1433
+ <div className="flex flex-col gap-sd-12">
1434
+ <SSectionHeaderCard>
1435
+ <SSectionHeaderCard.Header title="기본 정보" marker thickness="accent" />
1436
+ <SSectionHeaderCard.Body>
1437
+ <SKeyValueTable fields={basicFields} values={product} />
1438
+ </SSectionHeaderCard.Body>
1439
+ </SSectionHeaderCard>
1440
+
1441
+ <SSectionHeaderCard>
1442
+ {/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
1443
+ <SSectionHeaderCard.Header
1444
+ title="가격 정보"
1445
+ marker
1446
+ helpText={['부가세 포함 금액입니다.']}
1447
+ slot={<SButton size="sm" color="secondary" label="이력" onClick={openHistory} />}
1448
+ />
1449
+ <SSectionHeaderCard.Body>
1450
+ <SKeyValueTable fields={priceFields} values={product} />
1451
+ </SSectionHeaderCard.Body>
1452
+ </SSectionHeaderCard>
1453
+
1454
+ {/* 액션은 하단 — 목록(되돌리기)은 왼쪽 끝, 실행 액션은 오른쪽 끝 */}
1455
+ <div className="flex items-center justify-between">
1456
+ <SButton color="neutral" outline label="목록" onClick={goList} />
1457
+ <div className="flex items-center gap-sd-8">
1458
+ <SButton color="danger" outline label="삭제" onClick={confirmDelete} />
1459
+ <SButton label="수정" onClick={goEdit} />
1460
+ </div>
1398
1461
  </div>
1399
1462
  </div>
1400
- </div>
1463
+ </AppShell>
1401
1464
  );
1402
1465
  }
1403
1466
  ```
@@ -1438,8 +1501,10 @@ export default function ProductDetailPage() {
1438
1501
  - [ ] 자체 스크롤하는 패널의 하단에 `pb-[var(--cmp-pageBody-padding-scrollEnd)]` 이 있는가, 페이지네이션 있는 목록에서 `scrollEndSpacing={false}` 를 넘겼는가
1439
1502
  - [ ] 같은 컴포넌트를 나열할 때 §2-2 그룹 간격을 썼는가 (체크박스 가로 `gap-sd-24` 등)
1440
1503
  - [ ] 페이지가 §4의 표준 골격에서 시작했는가
1504
+ - [ ] `header.fix` 가 프로젝트 전체와 같은 값인가 (다른 페이지와 다르게 섞어 쓰지 않았는가, §4-1)
1505
+ - [ ] `header.slot` 을 채웠다면 `SButton size="sm"` 위주인가 (§4-1 "페이지 헤더 사용 규칙")
1441
1506
  - [ ] 앱 셸이나 그 바깥에 `min-width`·`overflow-x` 를 직접 걸지 않았는가 (최소 너비는 `SLayout` 이 보장한다, §4-1)
1442
- - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가)
1507
+ - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
1443
1508
  - [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
1444
1509
  - [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
1445
1510
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
package/README.md CHANGED
@@ -11,8 +11,11 @@ Sellmate 디자인 시스템의 React 컴포넌트 라이브러리 (React + Type
11
11
 
12
12
  ## 설치
13
13
 
14
+ > ⚠️ **이 패키지는 현재 베타입니다.** `beta` dist-tag 로만 배포되므로 태그를 붙여 설치해야 합니다.
15
+ > 태그 없이 설치하면 마지막 정식 버전(8.x)이 잡힙니다. 자세한 정책은 [버전 정책](#버전-정책) 참조.
16
+
14
17
  ```bash
15
- npm install sellmate-design-system-react
18
+ npm install sellmate-design-system-react@beta
16
19
  npx sellmate-ds init
17
20
  ```
18
21
 
@@ -317,25 +320,52 @@ export default [...sellmate.configs.strict];
317
320
  - **단일 라이트 테마** — 다크모드는 지원하지 않습니다.
318
321
  - **타입 포함** — 모든 컴포넌트에 TypeScript 타입 정의(`.d.ts`)가 함께 제공됩니다.
319
322
 
323
+ ## 버전 정책
324
+
325
+ **현재 베타 단계입니다.** 컴포넌트 API 가 아직 자리를 잡는 중이라 `9.0.0-beta.N` 으로 나가고, `beta` dist-tag 로만 배포합니다.
326
+
327
+ - 베타 기간에는 breaking 이 또 나와도 `beta.N` 만 올라갑니다. 아직 아무도 `9.0.0` 을 받지 않았으므로 major 를 올릴 이유가 없습니다.
328
+ - 프리릴리스는 `^8` 같은 캐럿 범위에 절대 걸리지 않습니다. 기존 소비 앱이 조용히 깨지지 않습니다.
329
+ - 소비 앱은 `sellmate-design-system-react@beta` 로 명시적으로 받아야 합니다.
330
+
331
+ ### 정식 9.0.0 으로 언제 전환하나
332
+
333
+ 날짜가 아니라 상태로 판단합니다. 셋을 모두 만족할 때 전환합니다.
334
+
335
+ 1. **2주 연속 BREAKING 커밋 0** — 지금 major 를 만드는 건 전부 컴포넌트 API 재설계입니다. 이게 멎은 것이 API 가 자리 잡았다는 유일한 실증입니다.
336
+ 2. **소비 앱이 이 패키지만으로 프로덕션 화면을 다 그릴 수 있을 때** — `AGENTS.md` §0-1 인덱스에 없어서 소비 앱이 직접 만든 컴포넌트가 남아 있으면, 그게 나중에 들어오면서 또 API 를 흔듭니다.
337
+ 3. **토큰 스킴이 값만 바뀌는 단계일 때** — 토큰 JSON 의 키 구조가 바뀌는 동안은 `theme.css` 재생성이 breaking 을 계속 만듭니다.
338
+
339
+ 전환할 때 할 일은 두 가지입니다. `npm run version:stable -- --release-as 9.0.0` 으로 꼬리를 떼고, **`package.json` 의 `publishConfig.tag` 를 지웁니다**(= `latest`). 둘 중 하나만 하면 `prepublishOnly` 가드가 배포를 막습니다.
340
+
341
+ 정식 전환 후 major 인플레를 막는 실제 레버는 번호 체계가 아니라 **릴리스 묶음**입니다. breaking 을 나올 때마다 즉시 내지 말고 정해진 주기(격주·월 1회)에 모아서 내면 같은 변경량이 major 한 번으로 압축됩니다.
342
+
320
343
  ## 버전 업 / 릴리스
321
344
 
322
345
  Stencil 쪽 `lerna version` 과 동일하게 **Conventional Commits 기반**으로 버전을 올리고 `CHANGELOG.md` 를 누적합니다. 이 패키지는 lerna 관리 대상이 아니라 전용 스크립트를 씁니다.
323
346
 
324
347
  ```bash
325
- # 마지막 릴리스 이후 커밋으로 상승 폭을 자동 추론 → package.json/CHANGELOG 갱신 + 릴리스 커밋
326
- npm run version
348
+ # 베타 이어가기 상승 자동 추론 → package.json/CHANGELOG 갱신 + 릴리스 커밋
349
+ npm run version # 9.0.0-beta.0 → 9.0.0-beta.1
327
350
 
328
351
  # 실제로 바꾸지 않고 결과만 미리보기
329
352
  npm run version:dry
330
353
 
331
- # 루트에서도 실행 가능
332
- npm run version:rn # (design-system 루트)
354
+ # 정식 채널용 (베타 졸업 후에 씁니다)
355
+ npm run version:stable
356
+ npm run version:stable:dry
357
+
358
+ # 루트에서도 실행 가능 (design-system 루트)
359
+ npm run version:rn
360
+ npm run version:rn:stable
333
361
  ```
334
362
 
335
363
  - 마지막 `chore(release): react-native <ver>` 커밋 이후 `react-native/` 를 건드린 커밋을 모읍니다.
336
- - `BREAKING CHANGE → major`, `feat → minor`, 그 외(`fix` 등) `→ patch` 로 버전을 올립니다.
337
- - 주요 옵션: `--bump <major|minor|patch>` · `--release-as <x.y.z>` · `--pre-major`(0.x 유지) · `--tag` · `--no-commit` · `--dry-run` · `--force` (`node scripts/version.mjs --help`).
338
- - push하지 않습니다. 커밋/태그 확인 직접 push 하세요.
364
+ - `BREAKING CHANGE → major`, `feat → minor`, 그 외(`fix` 등) `→ patch` 로 버전을 올립니다. `--prerelease` 를 주면 이 상승 폭은 **정식 버전이 아직 없을 때만** core 에 반영됩니다(8.1.0 → 9.0.0-beta.0). 이미 프리릴리스면 `beta.N` 만 올라갑니다.
365
+ - 현재 버전이 프리릴리스인데 `--prerelease` `--release-as` 없으면 스크립트가 막습니다 베타를 실수로 정식으로 올리는 사고 방지입니다.
366
+ - **git 태그(`sellmate-design-system-react@<ver>`)npm scripts `--tag` 기본으로 주므로 자동으로 붙습니다.** 건너뛰려면 `npm run version -- --no-tag`.
367
+ - 주요 옵션: `--bump <major|minor|patch>` · `--release-as <x.y.z>`(프리릴리스 꼬리 허용) · `--prerelease [id]` · `--pre-major`(0.x 유지) · `--tag` / `--no-tag` · `--no-commit` · `--dry-run` · `--force` (`node scripts/version.mjs --help`).
368
+ - push 는 하지 않습니다. 커밋/태그 확인 후 직접 push 하세요 — 태그는 `git push origin <태그명>` 으로 따로 올려야 합니다.
339
369
 
340
370
  ## 라이선스
341
371
 
@@ -0,0 +1,69 @@
1
+ # SChipFilter
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SChipFilter
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `fields?` | `SChipFilterField[] \| SChipFilterGroup[]` | — | 필터 정의 목록. 그룹으로 묶으려면 SChipFilterGroup[]을 넘긴다 — 그룹이 시작될 때마다 앞에 구분선이 자동으로 붙는다(showLabel·인접 그룹·인라인 date 필터와 중첩되지 않도록 처리됨). |
12
+ | `value?` | `SChipFilterValueMap` | — | 필터 값 맵 |
13
+ | `activeKeys?` | `string[]` | — | 노출 필터 key 목록. 지정하면 제어 컴포넌트로 동작 — "필터 추가"로 고른 필드를 이 배열에 직접 넣어줘야 칩이 나타난다(onActiveKeysChange에서 받은 keys로 갱신). 지정하지 않으면 필드별 defaultActive 값을 기준으로 컴포넌트가 내부 상태로 관리하는 비제어 방식으로 동작한다 |
14
+ | `label?` | `string` | `'검색 필터'` | 좌측 태그 텍스트 |
15
+ | `showLabel?` | `boolean` | `false` | 좌측 태그(label)·구분선 표시 여부 |
16
+ | `showAddButton?` | `boolean` | `true` | 필터 추가 버튼 표시 여부 |
17
+ | `showReset?` | `boolean` | `true` | 검색 초기화 링크 표시 여부 |
18
+ | `disabled?` | `boolean` | `false` | 바 비활성 상태 |
19
+ | `className?` | `string` | — | |
20
+ | `style?` | `CSSProperties` | — | |
21
+
22
+ #### Events
23
+
24
+ | Event | Type | Description |
25
+ |-------|------|-------------|
26
+ | `onValueChange` | `(value: SChipFilterValueMap) => void` | 전체 값 변경 — 편집 중인 값이 바뀔 때마다(선택할 때마다) 호출된다. 실제 검색 실행은 onSearch를 쓴다 |
27
+ | `onFilterChange` | `(detail: SChipFilterChangeDetail) => void` | 개별 필터 값 변경 |
28
+ | `onSearch` | `(value: SChipFilterValueMap) => void` | 실제 검색을 실행할 시점 — 편집 팝오버의 "검색" 버튼을 누르거나 팝오버가 닫힐 때(바깥 클릭·Esc·다른 칩으로 전환 포함) 그 시점의 전체 값 맵과 함께 호출된다. 팝오버가 없는 필드(인라인 date 프리셋·custom, clearable ×, 검색 초기화)는 값이 바뀌는 즉시 호출된다. dirty 체크가 기본 적용되어 있어 — 마지막으로 실제 검색이 실행된 값 맵과 비교해 하나라도 달라진 게 없으면(예: 팝오버를 열었다 아무것도 안 바꾸고 닫는 경우) 호출되지 않는다. fields를 그룹으로 넘겼다면 rule을 만족하지 못한 그룹이 있는 동안엔 onSearch가 호출되지 않는다 — 해당 그룹의 경고 툴팁은 이 시점과 무관하게 값이 비어 있는 동안 항상 실시간으로 떠 있다(별도로 validate()를 호출할 필요 없음) |
29
+ | `onActiveKeysChange` | `(keys: string[]) => void` | 노출 필터 key 변경(칩 추가·제거) — 비제어 방식에서도 참고용으로 호출된다. activeKeys를 직접 제어할 때는 이 값을 그대로 activeKeys에 반영해야 한다 |
30
+ | `onReset` | `() => void` | "검색 초기화" 클릭 — 모든 필드가 기본값(또는 null)으로 리셋된 뒤 호출된다 |
31
+ | `onAddFilter` | `(key: string) => void` | "필터 추가" 목록에서 항목을 골랐을 때 — activeKeys를 직접 제어 중이면 이 콜백에서 (또는 onActiveKeysChange에서) key를 activeKeys에 추가해줘야 칩이 실제로 나타난다. activeKeys를 넘기지 않았다면(비제어) 별도 처리 없이도 컴포넌트가 알아서 칩을 노출한다 |
32
+
33
+ #### Methods (ref)
34
+
35
+ | Method | Type | Description |
36
+ |--------|------|-------------|
37
+ | `open` | `(key: string) => void` | 특정 필터 편집 팝오버를 엽니다. |
38
+ | `reset` | `() => void` | 모든 필터 값을 초기화합니다. |
39
+ | `validate` | `() => boolean` | fields를 그룹(SChipFilterGroup[])으로 넘겼을 때, 현재 값 기준으로 각 그룹의 rule을 만족하는지 반환합니다. 경고 툴팁은 이 호출과 무관하게 rule 위반 상태인 동안 항상 실시간으로 떠 있으므로, 이 메서드는 그 상태를 그대로 읽어오는 용도다(예: 검색 버튼을 눌러도 되는지 사전 확인) |
40
+
41
+ ## Dependencies
42
+
43
+ ### Depends on
44
+
45
+ - [SDatePicker](../SDatePicker)
46
+ - [SDateRangePicker](../SDateRangePicker)
47
+ - [SGhostButton](../SGhostButton)
48
+ - [SIcon](../SIcon)
49
+ - [SRadio](../SRadio)
50
+ - [SRadioButton](../SRadioButton)
51
+ - [STag](../STag)
52
+ - [STextLink](../STextLink)
53
+ - [STooltip](../STooltip)
54
+
55
+ ### Graph
56
+
57
+ ```mermaid
58
+ graph TD;
59
+ SChipFilter --> SDatePicker
60
+ SChipFilter --> SDateRangePicker
61
+ SChipFilter --> SGhostButton
62
+ SChipFilter --> SIcon
63
+ SChipFilter --> SRadio
64
+ SChipFilter --> SRadioButton
65
+ SChipFilter --> STag
66
+ SChipFilter --> STextLink
67
+ SChipFilter --> STooltip
68
+ style SChipFilter fill:#f9f,stroke:#333,stroke-width:4px
69
+ ```