sellmate-design-system-react 9.0.0-beta.21 → 9.0.0-beta.24

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.
@@ -1161,6 +1161,25 @@ const columns: STableColumn[] = [
1161
1161
 
1162
1162
  `SSplitter` 의 구분선은 평소 자리만 잡고 칠해지지 않다가, 경계에 커서를 올리거나 포커스를 주면 그때 드러난다 — 조절 가능한 자리라는 신호다. **항상 보이는 선이 필요하면 `SDivider` 를 쓴다.** 선 색·두께·주변 여백은 토큰이 정하므로 직접 주지 않는다.
1163
1163
 
1164
+ **세로 구분선의 길이는 `height` prop 으로 준다 — `className` 의 `h-*` 로 주지 않는다.**
1165
+
1166
+ ```tsx
1167
+ // 줄 높이를 그대로 채운다 (기본)
1168
+ <div className="flex items-center gap-sd-8">
1169
+ <span>총 주문 3건</span>
1170
+ <SDivider vertical />
1171
+ <span>총 품목 12건</span>
1172
+ </div>
1173
+
1174
+ // 양옆 글자보다 짧은 선이 필요할 때만 길이를 정한다
1175
+ <SDivider vertical height={20} />
1176
+
1177
+ // 하지 말 것 — 선이 줄 맨 위에 붙는다
1178
+ <SDivider vertical className="h-sd-20" />
1179
+ ```
1180
+
1181
+ 길이를 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 채우는데, `stretch` 는 높이가 `auto` 일 때만 늘린다. `className` 으로 높이를 정하면 `stretch` 가 조용히 무효가 되어 선이 위로 솟는다. `height` 는 길이와 교차축 가운데 정렬을 함께 적용하므로 이 함정이 없다 — 위·아래 정렬이 필요하면 `className="self-start"` 처럼 명시한다. (`sellmate/divider-vertical-height` 규칙이 잡는다.)
1182
+
1164
1183
  ```tsx
1165
1184
  <SSplitter defaultValue={30} limits={[20, 60]}>
1166
1185
  <SSplitter.Before>내비게이션</SSplitter.Before>
@@ -1206,9 +1225,19 @@ const columns: STableColumn[] = [
1206
1225
 
1207
1226
  `SEditor` 는 **서식이 값의 일부일 때만** 쓴다. 값을 HTML 문자열로 주고받으므로 저장·검색·비교가 평문보다 비싸고, 화면에 다시 보여줄 때도 HTML 로 렌더해야 한다. 서식이 필요 없는 메모·사유는 `STextarea` 다 — "입력창이 커 보여서" 고르는 컴포넌트가 아니다. 반대로 공지·안내문·상품 상세처럼 **작성자가 정한 강조와 목록이 그대로 보여야 하는 글**이면 `STextarea` 로는 표현할 수 없다.
1208
1227
 
1209
- `SEditor` 도 `SInput`·`STextarea` 와 같은 폼 필드다 — `label`·`hint`·`rules`·`errorMessage` 를 자기 prop 으로 받고 `SForm` 제출 검증에도 들어간다. 빈 문서는 빈 문자열로 나오므로 `required` 규칙이 그대로 걸린다. 툴바 구성은 `toolbar` 줄이거나 늘릴 수 있고, 서식 입력이 필요 없는 자리에 굳이 놓아야 한다면 `toolbar={false}` 가 아니라 `STextarea` 를 고른다.
1228
+ `SEditor` 도 `SInput`·`STextarea` 와 같은 폼 필드다 — `label`·`hint`·`rules`·`errorMessage` 를 자기 prop 으로 받고 `SForm` 제출 검증에도 들어간다. 빈 문서는 빈 문자열로 나오므로 `required` 규칙이 그대로 걸린다. **툴바를 끄는 길은 없다** 서식 입력이 필요 없는 자리라면 서식 없는 `SEditor` 가 아니라 `STextarea` 를 고른다.
1229
+
1230
+ 툴바는 **프리셋 둘 중 하나뿐이다.** 기본은 쓸 수 있는 것을 모두 보이고, **`simple` 을 켜면 글자에 거는 서식만 남는다** — 목록·정렬·인용·코드·링크·이미지·구분선이 빠지고 선택했을 때 뜨는 판도 같은 범위로 줄어든다. 받은 글의 **문단 구조까지 작성자를 따라가면 곤란한 자리**(좁은 칸의 메모·사유·짧은 안내문)가 `simple` 이다. 항목을 직접 조합하는 prop 은 없다 — 화면마다 다른 툴바가 서면 그 자체가 학습 비용이 된다.
1231
+
1232
+ 본문에서 따옴표·하이픈·화살표는 **자동으로 치환된다**(`"` → `“”`, `--` → `—`, `->` → `→`). 끌 수 없으므로, 상품 코드·규격 문자열처럼 **입력한 그대로 남아야 하는 값**은 `SEditor` 본문이 아니라 `SInput` 으로 따로 받는다.
1233
+
1234
+ 툴바는 **자리를 지킨다**(고정). 긴 글을 쓰는 동안 막대가 화면 밖으로 나가지 않으므로, 툴바를 따로 감싸거나 위치를 주지 않는다.
1235
+
1236
+ 툴바에는 **글자 크기 드롭다운**과 **글자색 팔레트**가 들어 있다 — 작성자가 문단마다 크기·색을 직접 지정할 수 있고, 크기 목록 맨 위 `기본` 은 지정을 떼는 자리다. 크기 눈금은 화면 타이포(§2-1)가 아니라 워드프로세서의 눈금이라 본문보다 훨씬 큰 단계까지 있다. **눈금도 팔레트도 좁히는 prop 이 없다** — 화면마다 고를 수 있는 것이 다르면 같은 글이 어디에 붙느냐에 따라 다르게 보이기 때문이다. 그래서 **작성자가 화면 리듬을 벗어나면 곤란한 자리(상품 상세 설명·반복 노출되는 안내문 등)라면 `SEditor` 가 맞는 자리인지 먼저 본다** — 서식이 값의 일부가 아니라면 `STextarea` 다. 고른 크기·색은 저장되는 HTML 에 그대로 남아 나중에 되돌릴 수 없다.
1237
+
1238
+ 본문에는 **표**도 들어간다 — 툴바의 `표` 드롭다운에서 격자를 끌어 크기를 고르고(최대 8행 × 10열), 행·열을 늘리고, 칸을 병합한다. **열 너비는 균등 고정이고 바꿀 수 없다**: 너비를 저장하면 그 값이 px 로 박혀 작성한 화면보다 좁은 곳에서 표가 넘친다. 대신 어떤 폭에서도 표가 상자 안에 들어오도록 열을 고르게 나눈다 — 열이 많은 표는 좁은 칸에서 글자가 잘게 접히므로, **열이 넷을 넘어가는 표라면 `SEditor` 본문이 아니라 `STable` 이 맞는 자리인지 본다.** 표는 작성자가 쓰는 글의 일부일 때만 여기에 있고, 데이터를 줄 세워 보여 주는 것은 `STable` 이다.
1210
1239
 
1211
- 글을 선택하면 **그 위에 서식 판이 뜬다**(버블 메뉴). 툴바까지 커서를 옮기지 않고 바로 굵게·색·링크를 걸기 위한 것이라 기본으로 켜져 있고, 읽기 전용·비활성일 때는 뜨지 않는다. 판은 줄이라 줄바꿈하지 않으므로 **좁은 칸에 놓인 에디터라면 `bubbleMenu` 항목을 줄이거나 `false` 끈다** 그대로 두면 필드 밖으로 넘친다. 뜨는 자리는 DS 가 잡는다, 직접 감싸거나 위치를 주지 않는다.
1240
+ 글을 선택하면 **그 위에 서식 판이 뜬다**(버블 메뉴). 툴바까지 커서를 옮기지 않고 바로 굵게·색·링크를 거는 길이다. **끄고 켜는 prop 없다** — 편집할 수 있으면 언제나 뜨고, 읽기 전용·비활성일 때는 뜨지 않는다. 화면마다 있고 없고가 달라지면 자체가 학습 비용이 되기 때문이다. 판의 구성은 `simple` 막대와 함께 정한다. 뜨는 자리는 DS 가 잡는다, 직접 감싸거나 위치를 주지 않는다.
1212
1241
 
1213
1242
  `SEditor` 는 화면에 처음 놓일 때 **에디터 엔진을 따로 불러온다** — 앱 초기 번들에는 들어가지 않는다. 그동안은 같은 크기의 빈 편집 영역이 자리를 지키므로 레이아웃은 흔들리지 않지만, **마운트하자마자 `ref.current.getHTML()` 로 값을 읽거나 툴바를 누를 수는 없다.** 열자마자 커서를 놓고 싶으면 `ref.current.focus()` 를 그냥 부르면 된다 — 준비되는 순간 대신 실행된다.
1214
1243
 
@@ -1669,24 +1698,28 @@ export default function ProductListPage() {
1669
1698
 
1670
1699
  행 높이를 줄이는 것은 `dense` 다. 세로 여백만 줄고 좌우 패딩은 그대로라, 값이 잘리지 않으면서 한 화면에 들어가는 행 수가 늘어난다.
1671
1700
 
1672
- **어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 목록 페이지는 밀도를 고정하지 말고 `useDensityToggle` 고를 있게 둔다페이지네이션 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다.
1701
+ **어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 `dense` 밀도를 고정하는 스위치가 아니라 **시작 밀도이자 밀도 토글의 스위치**다켜면 하단 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다. 켜고 끄는 별도 prop 은 없다.
1673
1702
 
1674
1703
  ```tsx
1675
- // 사용자가 고른 밀도는 다음 방문에도 남는 것이 자연스럽다 — 저장은 페이지 몫이다
1676
- const [dense, setDense] = useState(() => loadPref('list.dense', true));
1704
+ // 좁게 시작하고, 사용자가 바꾸는 밀도는 표가 알아서 들고 간다
1705
+ <STable dense useRowsPerPageSelect pagination={{ currentPage, lastPage }} />;
1706
+
1707
+ // 사용자가 고른 밀도를 다음 방문에도 남기려는 화면만 받아서 저장한다.
1708
+ // 저장한 값은 시작 밀도로만 돌려준다 — 되돌려 넣지 않는다
1709
+ const [initialDense] = useState(() => loadPref('list.dense', true));
1677
1710
 
1678
1711
  <STable
1679
- dense={dense}
1680
- onDenseChange={next => { setDense(next); savePref('list.dense', next); }}
1681
- useDensityToggle
1712
+ dense={initialDense}
1713
+ onDenseChange={next => savePref('list.dense', next)}
1682
1714
  useRowsPerPageSelect
1683
1715
  pagination={{ currentPage, lastPage }}
1684
1716
  />;
1685
1717
  ```
1686
1718
 
1687
- - **밀도는 `STable` 이 갖지 않는다.** `dense` 곧 현재 상태이고, `onDenseChange` 없이 `useDensityToggle` 켜면 눌러도 아무 일도 일어나지 않는다.
1688
- - **토글은 페이지네이션이 있을 때만 나타난다** 사는 곳이 바이기 때문이다. 페이지네이션 없는 표에서 밀도를 고르게 하려면 `STableBar` 쪽에 직접 둔다.
1689
- - 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. `dense` `넓게 보기` 다.
1719
+ - **누른 뒤의 밀도는 `STable` 이 내부 상태로 들고 간다.** `onDenseChange` 없이도 토글은 동작한다. 저장이 필요한 화면만 받아서 저장하면 된다.
1720
+ - **`dense` `onDenseChange` 값을 되돌려 넣지 않는다.** 토글을 붙일지는 prop 정하므로, 넓게 순간 `dense` `false` 되면 토글이 사라져 다시 좁힐 길이 없다. prop 값을 바꿔 넘기는 것은 외부 버튼 등으로 밀도를 **되돌릴 때**만 쓴다 — 내부 밀도가 그 값으로 맞춰진다.
1721
+ - **`dense` 페이지네이션이 없어도 토글이 나온다** 하단 바를 토글만 담아 그린다. `dense` 아니면 토글도 없다.
1722
+ - 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. 좁게 보고 있으면 `넓게 보기` 다.
1690
1723
 
1691
1724
  ### 4-3. 폼 페이지 (등록/수정)
1692
1725
 
@@ -1902,7 +1935,7 @@ export default function ProductDetailPage() {
1902
1935
  - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
1903
1936
  - [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
1904
1937
  - [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
1905
- - [ ] 목록 페이지 표에 `useDensityToggle` 로 밀도를 고를 있게 뒀는가, `onDenseChange` 함께 줬는가 (§4-2 — 핸들러 없이 켜면 눌러도 아무 일도 없다)
1938
+ - [ ] 목록 페이지 표에 `dense` 로 밀도 토글을 띄웠는가, 값을 `onDenseChange` 결과로 되돌려 넣지는 않았는가 (§4-2 — 되돌려 넣으면 넓게 순간 토글이 사라진다)
1906
1939
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
1907
1940
  - [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
1908
1941
  - [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)
@@ -2663,14 +2696,15 @@ export interface SChipFilterChangeDetail {
2663
2696
 
2664
2697
  ```ts
2665
2698
  /** 필터 하나의 정의. type에 따라 쓸 수 있는 속성이 달라진다 —
2666
- * options는 single·multi·keyword, presets·selectable·maxRange는 date·period,
2667
- * render는 custom 에만 있다 */
2699
+ * options는 select·select-multi·keyword, presets·maxRange·radioButton은
2700
+ * datepicker-range·datepicker-statistics, render는 custom 에만 있다 */
2668
2701
  export type SChipFilterField =
2669
- | SChipFilterSingleField
2670
- | SChipFilterMultiField
2702
+ | SChipFilterSelectField
2703
+ | SChipFilterSelectMultiField
2704
+ | SChipFilterDatePickerDayField
2705
+ | SChipFilterDatePickerRangeField
2706
+ | SChipFilterDatePickerStatisticsField
2671
2707
  | SChipFilterKeywordField
2672
- | SChipFilterDateField
2673
- | SChipFilterPeriodField
2674
2708
  | SChipFilterCustomField;
2675
2709
  ```
2676
2710
 
@@ -2698,21 +2732,57 @@ export type SChipFilterValue =
2698
2732
  | undefined;
2699
2733
  ```
2700
2734
 
2701
- ### SChipFilterSingleField
2735
+ ### SChipFilterSelectField
2702
2736
 
2703
2737
  ```ts
2704
2738
  /** 후보 하나를 고른다 */
2705
- export interface SChipFilterSingleField extends SChipFilterOptionsField {
2706
- type: 'single';
2739
+ export interface SChipFilterSelectField extends SChipFilterOptionsField {
2740
+ type: 'select';
2707
2741
  }
2708
2742
  ```
2709
2743
 
2710
- ### SChipFilterMultiField
2744
+ ### SChipFilterSelectMultiField
2711
2745
 
2712
2746
  ```ts
2713
2747
  /** 후보 여럿을 고른다 */
2714
- export interface SChipFilterMultiField extends SChipFilterOptionsField {
2715
- type: 'multi';
2748
+ export interface SChipFilterSelectMultiField extends SChipFilterOptionsField {
2749
+ type: 'select-multi';
2750
+ }
2751
+ ```
2752
+
2753
+ ### SChipFilterDatePickerDayField
2754
+
2755
+ ```ts
2756
+ /** 날짜 하나를 고른다 — 캘린더 트리거 하나만 놓인다. 프리셋으로 고르게 하거나 시작~종료를
2757
+ * 받아야 하면 datepicker-range 다 */
2758
+ export interface SChipFilterDatePickerDayField extends SChipFilterFieldBase {
2759
+ type: 'datepicker-day';
2760
+ /** 캘린더 트리거의 placeholder */
2761
+ placeholder?: string;
2762
+ /** 선택 가능 범위 */
2763
+ selectable?: [string, string];
2764
+ }
2765
+ ```
2766
+
2767
+ ### SChipFilterDatePickerRangeField
2768
+
2769
+ ```ts
2770
+ /** 기간(시작~종료)을 고른다. presets를 주면 프리셋 라디오 목록으로 고르고,
2771
+ * 주지 않으면 기간 피커 하나만 놓인다 */
2772
+ export interface SChipFilterDatePickerRangeField extends SChipFilterPresetsField {
2773
+ type: 'datepicker-range';
2774
+ /** presets를 필터 바에 세그먼트 라디오로 펼쳐 놓는다(팝오버 없음).
2775
+ * 기본 false — 칩 클릭 시 팝오버 안에 세로 라디오 목록(+사용자 지정 선택 시 기간 피커) */
2776
+ radioButton?: boolean;
2777
+ }
2778
+ ```
2779
+
2780
+ ### SChipFilterDatePickerStatisticsField
2781
+
2782
+ ```ts
2783
+ /** 집계 단위(일·월·분기·반기·연)와 그 단위의 값을 함께 고른다 */
2784
+ export interface SChipFilterDatePickerStatisticsField extends SChipFilterPresetsField {
2785
+ type: 'datepicker-statistics';
2716
2786
  }
2717
2787
  ```
2718
2788
 
@@ -2737,29 +2807,6 @@ export interface SChipFilterKeywordField extends SChipFilterOptionsField {
2737
2807
  }
2738
2808
  ```
2739
2809
 
2740
- ### SChipFilterDateField
2741
-
2742
- ```ts
2743
- /** 날짜 하나 또는 기간을 고른다 */
2744
- export interface SChipFilterDateField extends SChipFilterPresetsField {
2745
- type: 'date';
2746
- /** presets 없이 단일 캘린더 트리거로 동작할 때의 placeholder */
2747
- placeholder?: string;
2748
- /** presets를 필터 바에 세그먼트 라디오로 펼쳐 놓는다(팝오버 없음).
2749
- * 기본 false — 칩 클릭 시 팝오버 안에 세로 라디오 목록(+사용자 지정 선택 시 기간 피커) */
2750
- radioButton?: boolean;
2751
- }
2752
- ```
2753
-
2754
- ### SChipFilterPeriodField
2755
-
2756
- ```ts
2757
- /** 집계 단위(일·월·분기·반기·연)와 그 단위의 값을 함께 고른다 */
2758
- export interface SChipFilterPeriodField extends SChipFilterPresetsField {
2759
- type: 'period';
2760
- }
2761
- ```
2762
-
2763
2810
  ### SChipFilterCustomField
2764
2811
 
2765
2812
  ```ts
@@ -2797,7 +2844,7 @@ export interface SChipFilterKeywordValue {
2797
2844
  ### SChipFilterPeriodValue
2798
2845
 
2799
2846
  ```ts
2800
- /** period 필드 값 — 선택 단위(unit)와 그 단위의 입력값(value)을 함께 보관한다 */
2847
+ /** datepicker-statistics 필드 값 — 선택 단위(unit)와 그 단위의 입력값(value)을 함께 보관한다 */
2801
2848
  export interface SChipFilterPeriodValue {
2802
2849
  unit: SChipFilterPeriodUnit;
2803
2850
  value?: string | number | SDateRangeValue | null;
@@ -2814,35 +2861,13 @@ export type SChipFilterCustomValue = Record<string, unknown>;
2814
2861
  ### SChipFilterOptionsField
2815
2862
 
2816
2863
  ```ts
2817
- /** 후보 목록에서 고르는 필터 — single·multi·keyword */
2864
+ /** 후보 목록에서 고르는 필터 — select·select-multi·keyword */
2818
2865
  export interface SChipFilterOptionsField extends SChipFilterFieldBase {
2819
2866
  /** 고를 수 있는 후보 목록 */
2820
2867
  options?: SChipFilterOption[];
2821
2868
  }
2822
2869
  ```
2823
2870
 
2824
- ### SChipFilterKeywordInput
2825
-
2826
- ```ts
2827
- /** keyword 필드의 입력 방식 — 무엇을 키워드 하나의 끝으로 볼지 */
2828
- export type SChipFilterKeywordInput = 'tag' | 'csv';
2829
- ```
2830
-
2831
- ### SChipFilterPresetsField
2832
-
2833
- ```ts
2834
- /** 프리셋으로 기간을 고르는 필터 — date·period */
2835
- export interface SChipFilterPresetsField extends SChipFilterFieldBase {
2836
- /** 프리셋 라디오 목록. date에서 지정하지 않으면 단일 캘린더 트리거로 동작하고,
2837
- * period에서 지정하지 않으면 일별·월별·분기별·반기별·연도별·사용자 지정 기본 목록을 쓴다 */
2838
- presets?: SChipFilterDatePreset[];
2839
- /** 선택 가능 범위 */
2840
- selectable?: [string, string];
2841
- /** "사용자 지정" 프리셋으로 기간을 고를 때의 최대 선택 일수 */
2842
- maxRange?: number;
2843
- }
2844
- ```
2845
-
2846
2871
  ### SChipFilterFieldBase
2847
2872
 
2848
2873
  ```ts
@@ -2859,9 +2884,17 @@ export interface SChipFilterFieldBase {
2859
2884
  * required도 같은 효과를 낸다 — 처음부터 바에 보이는 것은 fixed이거나 required인 필드뿐이고,
2860
2885
  * 나머지는 전부 "필터 추가"에서 골라야 나타난다 */
2861
2886
  fixed?: boolean;
2862
- /** 초기값 및 clearable 클릭 시 되돌아갈 값. required 여부와 무관하게 적용된다 — 값이 비어 있으면
2863
- * 마운트(또는 "필터 추가"로 활성화) 시 이 값이 자동으로 채워진다. required인데 지정하지 않으면
2864
- * 타입별 내장 기본값(single: 번째 옵션, date: 오늘 날짜)을 대신 쓴다 */
2887
+ /** 초기값 및 clearable 클릭 시 되돌아갈 값.
2888
+ *
2889
+ * **`fixed` 또는 `required` 필드에서만 쓴다.** 필드에 지정하면 무시하고 콘솔에 알린다 —
2890
+ * 뺄 수 있는 필터는 처음에 바에 없는데, 값 맵에는 기본값이 채워져 **보이지도 지울 수도 없는
2891
+ * 조건이 검색에 걸린다.** 칩을 지워 값을 비워도 "검색 초기화"가 그 값을 되돌려 놓아 필터가
2892
+ * 조용히 되살아난다. 처음부터 값이 정해져 있어야 하는 필터라면 그것이 `fixed`·`required` 다.
2893
+ *
2894
+ * required인데 지정하지 않으면 타입별 내장 기본값(select: 첫 번째 옵션,
2895
+ * datepicker-day·datepicker-range: 오늘 날짜)을 대신 쓴다. 인라인(radioButton) datepicker-range
2896
+ * 필드는 팝오버 없이 바에 바로 노출되어 빈 상태로 둘 수 없으므로, 이 규칙과 무관하게 언제나
2897
+ * 오늘이 채워진다 */
2865
2898
  defaultValue?: SChipFilterValue;
2866
2899
  /** 이 필터만 비활성. 바에 남아 있되 팝오버가 열리지 않고 clearable도 눌리지 않는다.
2867
2900
  * 바 전체를 잠그려면 SChipFilterProps.disabled를 쓴다 — 둘은 OR로 합쳐진다 */
@@ -2869,6 +2902,28 @@ export interface SChipFilterFieldBase {
2869
2902
  }
2870
2903
  ```
2871
2904
 
2905
+ ### SChipFilterPresetsField
2906
+
2907
+ ```ts
2908
+ /** 프리셋으로 기간을 고르는 필터 — datepicker-range·datepicker-statistics */
2909
+ export interface SChipFilterPresetsField extends SChipFilterFieldBase {
2910
+ /** 프리셋 라디오 목록. datepicker-range에서 지정하지 않으면 프리셋 없이 기간 피커 하나만 놓이고,
2911
+ * datepicker-statistics에서 지정하지 않으면 일별·월별·분기별·반기별·연도별·사용자 지정 기본 목록을 쓴다 */
2912
+ presets?: SChipFilterDatePreset[];
2913
+ /** 선택 가능 범위 */
2914
+ selectable?: [string, string];
2915
+ /** "사용자 지정" 프리셋으로 기간을 고를 때의 최대 선택 일수 */
2916
+ maxRange?: number;
2917
+ }
2918
+ ```
2919
+
2920
+ ### SChipFilterKeywordInput
2921
+
2922
+ ```ts
2923
+ /** keyword 필드의 입력 방식 — 무엇을 키워드 하나의 끝으로 볼지 */
2924
+ export type SChipFilterKeywordInput = 'tag' | 'csv';
2925
+ ```
2926
+
2872
2927
  ### SChipFilterMatchMode
2873
2928
 
2874
2929
  ```ts
@@ -2894,7 +2949,7 @@ export interface SChipFilterOption {
2894
2949
  ### SChipFilterDatePreset
2895
2950
 
2896
2951
  ```ts
2897
- /** date/period 필드의 프리셋 라디오 항목 (오늘/지난 7일/일별/월별/사용자 지정 등) */
2952
+ /** datepicker-range·datepicker-statistics 필드의 프리셋 라디오 항목 (오늘/지난 7일/일별/월별/사용자 지정 등) */
2898
2953
  export interface SChipFilterDatePreset {
2899
2954
  /** 프리셋 식별자 */
2900
2955
  value: string;
@@ -2902,7 +2957,8 @@ export interface SChipFilterDatePreset {
2902
2957
  label: string;
2903
2958
  /** true면 "사용자 지정" — 선택 시 날짜/기간 피커가 추가로 노출된다. resolve는 무시된다. */
2904
2959
  custom?: boolean;
2905
- /** custom이 아닐 때 실제 값을 계산한다. 단일 날짜(string) 또는 기간([start,end]) 모두 가능 */
2960
+ /** custom이 아닐 때 실제 값을 계산한다. 단일 날짜(string) 또는 기간([start,end]) 모두 가능
2961
+ * datepicker-range 에서는 'd' 를 그 하루짜리 기간 ['d', 'd'] 와 같은 값으로 본다 */
2906
2962
  resolve?: () => string | SDateRangeValue;
2907
2963
  }
2908
2964
  ```
@@ -3337,6 +3393,7 @@ export type SDateRangePickerSize = SFieldSize;
3337
3393
  | Prop | Type | Default | Description |
3338
3394
  |------|------|---------|-------------|
3339
3395
  | `vertical?` | `boolean` | `false` | true면 수직 분할선, false면 수평 분할선 |
3396
+ | `height?` | `number \| string` | — | 수직 분할선의 길이. 숫자는 px, 문자열은 CSS 값(토큰 var 참조 등)으로 쓴다. 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 그대로 채운다. 값을 주면 그 높이로 고정하고 교차축 가운데(`self-center`)에 맞춘다 — `align-self: stretch` 는 높이가 auto 일 때만 늘리므로, 높이를 정하는 순간 정렬을 함께 정해야 한다. 그래서 높이는 `className="h-*"` 가 아니라 이 prop 으로 준다. 수평 분할선에는 적용되지 않는다 (두께는 1px 로 고정). |
3340
3397
 
3341
3398
  ## Dependencies
3342
3399
 
@@ -3503,6 +3560,7 @@ export interface SDraggableGroupMoveEvent {
3503
3560
  export interface SDraggableListRenderState {
3504
3561
  onDragHandleMouseDown: (event: MouseEvent<HTMLDivElement>) => void;
3505
3562
  selected: boolean;
3563
+ disabled: boolean;
3506
3564
  depth: number;
3507
3565
  }
3508
3566
  ```
@@ -3647,16 +3705,12 @@ export interface SDropdownButtonItem {
3647
3705
  | `defaultValue?` | `string` | — | |
3648
3706
  | `htmlRef` | `RefObject<string>` | — | 지금 화면에 있는 HTML. 껍데기(SEditor)가 규칙 검증·폼 제출에 쓴다 |
3649
3707
  | `placeholder` | `string` | — | |
3650
- | `typography` | `boolean` | — | |
3651
3708
  | `editable` | `boolean` | — | |
3652
3709
  | `disabled` | `boolean` | — | |
3653
3710
  | `minHeight?` | `number \| string` | — | |
3654
3711
  | `maxHeight?` | `number \| string` | — | |
3655
- | `toolbar` | `SEditorToolbarItem[] \| false` | — | |
3656
- | `bubbleMenu` | `SEditorToolbarItem[] \| false` | — | 선택 영역 위에 뜨는 서식 판. `false` 그리지 않는다 |
3657
- | `fontSizes` | `number[]` | — | |
3658
- | `colors` | `SEditorColorOption[]` | — | |
3659
- | `highlights` | `SEditorColorOption[]` | — | |
3712
+ | `toolbar` | `SEditorToolbarItem[]` | — | |
3713
+ | `bubbleMenu` | `SEditorToolbarItem[]` | — | 선택 영역 위에 뜨는 서식 편집할 있으면 언제나 뜬다 (SEditor 주석 참고) |
3660
3714
  | `editorClass?` | `string` | — | |
3661
3715
  | `editorStyle?` | `CSSProperties` | — | |
3662
3716
 
@@ -3678,9 +3732,6 @@ export interface SDropdownButtonItem {
3678
3732
  | `state` | `EditorToolbarState` | — | 눌림 표시 — 에디터가 아직 없으면 `IDLE_TOOLBAR_STATE` |
3679
3733
  | `editor` | `Editor \| null` | — | 없으면 버튼을 눌러도 아무 일도 하지 않는다 (그때는 disabled 로 함께 잠근다) |
3680
3734
  | `items` | `SEditorToolbarItem[]` | — | |
3681
- | `fontSizes` | `readonly number[]` | — | |
3682
- | `colors` | `SEditorColorOption[]` | — | |
3683
- | `highlights` | `SEditorColorOption[]` | — | |
3684
3735
  | `disabled` | `boolean` | — | 편집 불가(비활성·읽기전용·엔진 로딩 중) — 모든 버튼을 잠근다 |
3685
3736
  | `variant?` | `'bar' \| 'bubble'` | `'bar'` | `'bar'` 는 편집 영역 위에 붙는 막대, `'bubble'` 은 선택 영역 위에 뜨는 판이다. 그리는 버튼은 같고 담는 상자와 줄바꿈만 다르다. |
3686
3737
 
@@ -3701,12 +3752,7 @@ export interface SDropdownButtonItem {
3701
3752
  | `placeholder?` | `string` | `'내용을 입력해 주세요.'` | 빈 문서에 보일 안내 문구 |
3702
3753
  | `minHeight?` | `number \| string` | `200` | 편집 영역 최소 높이 (숫자=px) |
3703
3754
  | `maxHeight?` | `number \| string` | — | 편집 영역 최대 높이 (숫자=px). 넘으면 편집 영역 안에서만 스크롤한다 |
3704
- | `toolbar?` | `SEditorToolbarItem[] \| false` | `SEDITOR_DEFAULT_TOOLBAR` | 툴바 구성. `false` 툴바 없이 본문만 (읽기 화면·간단 메모용) |
3705
- | `bubbleMenu?` | `SEditorToolbarItem[] \| false` | `SEDITOR_DEFAULT_BUBBLE_MENU` | 글을 선택했을 때 그 위에 뜨는 서식 판의 구성. `false` 면 뜨지 않는다. 읽기 전용·비활성일 때는 어차피 뜨지 않는다. 좁은 칸에 놓인 에디터라면 판이 필드 밖으로 넘칠 수 있으니 항목을 줄이거나 `false` 로 끈다. |
3706
- | `fontSizes?` | `number[]` | `[...SEDITOR_FONT_SIZES]` | 글자 크기 드롭다운 선택지 (px) |
3707
- | `colors?` | `SEditorColorOption[]` | `SEDITOR_DEFAULT_COLORS` | 글자색 팔레트 |
3708
- | `highlights?` | `SEditorColorOption[]` | `SEDITOR_DEFAULT_HIGHLIGHTS` | 형광펜(배경색) 팔레트 |
3709
- | `typography?` | `boolean` | `false` | 따옴표·하이픈·화살표 자동 치환 (`"` → `“”`, `--` → `—`, `->` → `→`). 상품 코드·규격 문자열이 입력한 그대로 남아야 하는 화면이 많아 기본은 끔이다. **마운트 시점에만 반영된다** — 값이 바뀌어도 이미 만들어진 에디터에는 적용되지 않는다. |
3755
+ | `simple?` | `boolean` | `false` | **글자에 거는 서식만** 남긴다 목록·정렬·인용·코드·링크·이미지·구분선이 빠진다. 막대와 버블 메뉴 양쪽에 함께 걸린다 (한쪽에만 남으면 아무것도 막지 못한다). 받은 글의 문단 구조까지 작성자를 따라가면 곤란한 자리 — 좁은 칸의 메모·사유·짧은 안내문 — 에 쓴다. 반대로 서식이 아예 필요 없다면 `SEditor` 가 아니라 `STextarea` 다. |
3710
3756
  | `rules?` | `Rule[]` | — | 유효성 규칙 — blur 시 자동 검증 |
3711
3757
  | `status?` | `SFieldStatus` | — | 필드 상태 ('default' | 'pass' | 'error') |
3712
3758
  | `focused?` | `boolean` | — | 포커스 상태 (제어/반영) |
@@ -3775,17 +3821,6 @@ export interface TiptapApi {
3775
3821
  export type SEditorToolbarItem = SEditorToolbarAction | '|';
3776
3822
  ```
3777
3823
 
3778
- ### SEditorColorOption
3779
-
3780
- ```ts
3781
- export interface SEditorColorOption {
3782
- /** 팔레트 칸의 접근성 레이블·툴팁 */
3783
- label: string;
3784
- /** 팔레트 키(`red_75` …) 또는 CSS 색상 문자열 */
3785
- color: SColor;
3786
- }
3787
- ```
3788
-
3789
3824
  ### EditorToolbarState
3790
3825
 
3791
3826
  ```ts
@@ -3798,8 +3833,6 @@ export type EditorToolbarState = ReturnType<typeof readEditorState>;
3798
3833
  export interface SEditorExtensionOptions {
3799
3834
  /** 빈 문서에 보일 문구를 그때그때 읽어 오는 게터 */
3800
3835
  getPlaceholder: () => string;
3801
- /** 따옴표·하이픈·화살표 자동 치환 (Typography) */
3802
- typography: boolean;
3803
3836
  }
3804
3837
  ```
3805
3838
 
@@ -3822,9 +3855,6 @@ export const SEDITOR_TOOLBAR_ITEMS = [
3822
3855
  'strike',
3823
3856
  'code',
3824
3857
  'color',
3825
- 'highlight',
3826
- 'superscript',
3827
- 'subscript',
3828
3858
  'alignLeft',
3829
3859
  'alignCenter',
3830
3860
  'alignRight',
@@ -3838,6 +3868,7 @@ export const SEDITOR_TOOLBAR_ITEMS = [
3838
3868
  'horizontalRule',
3839
3869
  'link',
3840
3870
  'image',
3871
+ 'table',
3841
3872
  'undo',
3842
3873
  'redo',
3843
3874
  ] as const;
@@ -6261,12 +6292,11 @@ export const STEPPER_SIZES = ['sm', 'lg'] as const;
6261
6292
  | `noDataLabel?` | `string` | `'데이터가 없습니다.'` | |
6262
6293
  | `noDataSlot?` | `ReactNode` | — | 데이터가 없을 때 body 영역 전체를 대체하는 슬롯. 지정하면 `noDataLabel` 대신 이 콘텐츠가 헤더 아래 영역을 채우며, 버튼 등 인터랙션도 동작한다. |
6263
6294
  | `isLoading?` | `boolean` | `false` | |
6264
- | `dense?` | `boolean` | `false` | 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로) |
6295
+ | `dense?` | `boolean` | `false` | 행 높이를 좁게 (세로 여백만 줄인다 — 좌우 패딩은 그대로). **시작 밀도이자 밀도 토글의 스위치다.** 켜면 하단 바 우측에 `좁게 보기` · `넓게 보기` 토글이 붙는다 — 페이지네이션이 없으면 이 바를 토글만 담아 그린다. 누른 뒤의 밀도는 테이블이 내부 상태로 들고 가므로 `onDenseChange` 를 받지 않아도 토글은 동작하고, 넓게 본 뒤에도 토글은 그대로 남는다(붙일지는 이 prop 이 정한다). 이 prop 값이 바뀌면 내부 밀도도 그 값으로 맞춰진다. |
6265
6296
  | `noHover?` | `boolean` | `false` | true면 행에 마우스를 올려도 hover 배경(grey_05)을 표시하지 않는다 |
6266
6297
  | `pagination?` | `STablePagination` | — | 페이지네이션 (있으면 하단 표시) |
6267
6298
  | `useInternalPagination?` | `boolean` | `false` | 테이블 내부에서 페이지네이션을 직접 관리 (rows를 내부 슬라이싱) |
6268
6299
  | `useRowsPerPageSelect?` | `boolean` | `false` | 페이지당 행 수 셀렉트 표시 |
6269
- | `useDensityToggle?` | `boolean` | `false` | 페이지네이션 바에 밀도 토글(`좁게 보기` · `넓게 보기`) 표시. **페이지네이션이 있을 때만 나타난다** — 토글이 사는 곳이 그 바이기 때문이다. `onDenseChange` 와 함께 준다. 밀도는 컴포넌트가 갖지 않으므로, 핸들러 없이 켜면 눌러도 아무 일도 일어나지 않는다. |
6270
6300
  | `rowsPerPageOption?` | `SSelectOption[]` | `DEFAULT_ROWS_PER_PAGE_OPTION` | |
6271
6301
  | `useVirtualScroll?` | `boolean` | `false` | 가상 스크롤 |
6272
6302
  | `rowHeight?` | `number` | — | |
@@ -6282,7 +6312,7 @@ export const STEPPER_SIZES = ['sm', 'lg'] as const;
6282
6312
  |-------|------|-------------|
6283
6313
  | `onSelectedChange` | `(rows: SRow[]) => void` | |
6284
6314
  | `onSortChange` | `(sort: STableSort \| null) => void` | 정렬 헤더 클릭 (`asc → desc → 해제` 3단). 해제되면 `null` 이 온다. 다중 정렬은 1차 안에서 지원하지 않는다. |
6285
- | `onDenseChange` | `(dense: boolean) => void` | 밀도 변경 (`useDensityToggle` 띄운 토글을 눌렀을 때). 컴포넌트는 밀도 상태를 갖지 않는다 `dense` 가 곧 현재 상태이고, 그 진실은 페이지에 있다. 사용자가 고른 밀도를 다음 방문까지 기억해 두는 것(로컬 저장 등) 페이지 몫이다. |
6315
+ | `onDenseChange` | `(dense: boolean) => void` | 밀도 변경 (하단 바의 밀도 토글을 눌렀을 때). 표시는 테이블이 알아서 바꾸므로 받지 않아도 되고, 사용자가 고른 밀도를 다음 방문까지 기억해 두려는(로컬 저장 등) 페이지만 받으면 된다. |
6286
6316
  | `onPageChange` | `(page: number) => void` | |
6287
6317
  | `onRowsPerPageChange` | `(perPage: number) => void` | |
6288
6318
  | `onVirtualUpdate` | `(range: { from: number; to: number }) => void` | |
package/dist/llms.txt CHANGED
@@ -1162,6 +1162,25 @@ const columns: STableColumn[] = [
1162
1162
 
1163
1163
  `SSplitter` 의 구분선은 평소 자리만 잡고 칠해지지 않다가, 경계에 커서를 올리거나 포커스를 주면 그때 드러난다 — 조절 가능한 자리라는 신호다. **항상 보이는 선이 필요하면 `SDivider` 를 쓴다.** 선 색·두께·주변 여백은 토큰이 정하므로 직접 주지 않는다.
1164
1164
 
1165
+ **세로 구분선의 길이는 `height` prop 으로 준다 — `className` 의 `h-*` 로 주지 않는다.**
1166
+
1167
+ ```tsx
1168
+ // 줄 높이를 그대로 채운다 (기본)
1169
+ <div className="flex items-center gap-sd-8">
1170
+ <span>총 주문 3건</span>
1171
+ <SDivider vertical />
1172
+ <span>총 품목 12건</span>
1173
+ </div>
1174
+
1175
+ // 양옆 글자보다 짧은 선이 필요할 때만 길이를 정한다
1176
+ <SDivider vertical height={20} />
1177
+
1178
+ // 하지 말 것 — 선이 줄 맨 위에 붙는다
1179
+ <SDivider vertical className="h-sd-20" />
1180
+ ```
1181
+
1182
+ 길이를 주지 않으면 `align-self: stretch` 로 부모 줄 높이를 채우는데, `stretch` 는 높이가 `auto` 일 때만 늘린다. `className` 으로 높이를 정하면 `stretch` 가 조용히 무효가 되어 선이 위로 솟는다. `height` 는 길이와 교차축 가운데 정렬을 함께 적용하므로 이 함정이 없다 — 위·아래 정렬이 필요하면 `className="self-start"` 처럼 명시한다. (`sellmate/divider-vertical-height` 규칙이 잡는다.)
1183
+
1165
1184
  ```tsx
1166
1185
  <SSplitter defaultValue={30} limits={[20, 60]}>
1167
1186
  <SSplitter.Before>내비게이션</SSplitter.Before>
@@ -1207,9 +1226,19 @@ const columns: STableColumn[] = [
1207
1226
 
1208
1227
  `SEditor` 는 **서식이 값의 일부일 때만** 쓴다. 값을 HTML 문자열로 주고받으므로 저장·검색·비교가 평문보다 비싸고, 화면에 다시 보여줄 때도 HTML 로 렌더해야 한다. 서식이 필요 없는 메모·사유는 `STextarea` 다 — "입력창이 커 보여서" 고르는 컴포넌트가 아니다. 반대로 공지·안내문·상품 상세처럼 **작성자가 정한 강조와 목록이 그대로 보여야 하는 글**이면 `STextarea` 로는 표현할 수 없다.
1209
1228
 
1210
- `SEditor` 도 `SInput`·`STextarea` 와 같은 폼 필드다 — `label`·`hint`·`rules`·`errorMessage` 를 자기 prop 으로 받고 `SForm` 제출 검증에도 들어간다. 빈 문서는 빈 문자열로 나오므로 `required` 규칙이 그대로 걸린다. 툴바 구성은 `toolbar` 줄이거나 늘릴 수 있고, 서식 입력이 필요 없는 자리에 굳이 놓아야 한다면 `toolbar={false}` 가 아니라 `STextarea` 를 고른다.
1229
+ `SEditor` 도 `SInput`·`STextarea` 와 같은 폼 필드다 — `label`·`hint`·`rules`·`errorMessage` 를 자기 prop 으로 받고 `SForm` 제출 검증에도 들어간다. 빈 문서는 빈 문자열로 나오므로 `required` 규칙이 그대로 걸린다. **툴바를 끄는 길은 없다** 서식 입력이 필요 없는 자리라면 서식 없는 `SEditor` 가 아니라 `STextarea` 를 고른다.
1230
+
1231
+ 툴바는 **프리셋 둘 중 하나뿐이다.** 기본은 쓸 수 있는 것을 모두 보이고, **`simple` 을 켜면 글자에 거는 서식만 남는다** — 목록·정렬·인용·코드·링크·이미지·구분선이 빠지고 선택했을 때 뜨는 판도 같은 범위로 줄어든다. 받은 글의 **문단 구조까지 작성자를 따라가면 곤란한 자리**(좁은 칸의 메모·사유·짧은 안내문)가 `simple` 이다. 항목을 직접 조합하는 prop 은 없다 — 화면마다 다른 툴바가 서면 그 자체가 학습 비용이 된다.
1232
+
1233
+ 본문에서 따옴표·하이픈·화살표는 **자동으로 치환된다**(`"` → `“”`, `--` → `—`, `->` → `→`). 끌 수 없으므로, 상품 코드·규격 문자열처럼 **입력한 그대로 남아야 하는 값**은 `SEditor` 본문이 아니라 `SInput` 으로 따로 받는다.
1234
+
1235
+ 툴바는 **자리를 지킨다**(고정). 긴 글을 쓰는 동안 막대가 화면 밖으로 나가지 않으므로, 툴바를 따로 감싸거나 위치를 주지 않는다.
1211
1236
 
1212
- 글을 선택하면 **그 위에 서식 판이 뜬다**(버블 메뉴). 툴바까지 커서를 옮기지 않고 바로 굵게·색·링크를 걸기 위한 것이라 기본으로 켜져 있고, 읽기 전용·비활성일 때는 뜨지 않는다. 판은 줄이라 줄바꿈하지 않으므로 **좁은 칸에 놓인 에디터라면 `bubbleMenu` 항목을 줄이거나 `false` 끈다**그대로 두면 필드 밖으로 넘친다. 뜨는 자리는 DS 잡는다, 직접 감싸거나 위치를 주지 않는다.
1237
+ 툴바에는 **글자 크기 드롭다운**과 **글자색 팔레트**가 들어 있다 — 작성자가 문단마다 크기·색을 직접 지정할 수 있고, 크기 목록 맨 위 `기본` 은 지정을 떼는 자리다. 크기 눈금은 화면 타이포(§2-1) 아니라 워드프로세서의 눈금이라 본문보다 훨씬 단계까지 있다. **눈금도 팔레트도 좁히는 prop 없다** 화면마다 고를 있는 것이 다르면 같은 글이 어디에 붙느냐에 따라 다르게 보이기 때문이다. 그래서 **작성자가 화면 리듬을 벗어나면 곤란한 자리(상품 상세 설명·반복 노출되는 안내문 등)라면 `SEditor` 맞는 자리인지 먼저 본다** 서식이 값의 일부가 아니라면 `STextarea` 다. 고른 크기·색은 저장되는 HTML 그대로 남아 나중에 되돌릴 수 없다.
1238
+
1239
+ 본문에는 **표**도 들어간다 — 툴바의 `표` 드롭다운에서 격자를 끌어 크기를 고르고(최대 8행 × 10열), 행·열을 늘리고, 칸을 병합한다. **열 너비는 균등 고정이고 바꿀 수 없다**: 너비를 저장하면 그 값이 px 로 박혀 작성한 화면보다 좁은 곳에서 표가 넘친다. 대신 어떤 폭에서도 표가 상자 안에 들어오도록 열을 고르게 나눈다 — 열이 많은 표는 좁은 칸에서 글자가 잘게 접히므로, **열이 넷을 넘어가는 표라면 `SEditor` 본문이 아니라 `STable` 이 맞는 자리인지 본다.** 표는 작성자가 쓰는 글의 일부일 때만 여기에 있고, 데이터를 줄 세워 보여 주는 것은 `STable` 이다.
1240
+
1241
+ 글을 선택하면 **그 위에 서식 판이 뜬다**(버블 메뉴). 툴바까지 커서를 옮기지 않고 바로 굵게·색·링크를 거는 길이다. **끄고 켜는 prop 은 없다** — 편집할 수 있으면 언제나 뜨고, 읽기 전용·비활성일 때는 뜨지 않는다. 화면마다 있고 없고가 달라지면 그 자체가 학습 비용이 되기 때문이다. 판의 구성은 `simple` 이 막대와 함께 정한다. 뜨는 자리는 DS 가 잡는다, 직접 감싸거나 위치를 주지 않는다.
1213
1242
 
1214
1243
  `SEditor` 는 화면에 처음 놓일 때 **에디터 엔진을 따로 불러온다** — 앱 초기 번들에는 들어가지 않는다. 그동안은 같은 크기의 빈 편집 영역이 자리를 지키므로 레이아웃은 흔들리지 않지만, **마운트하자마자 `ref.current.getHTML()` 로 값을 읽거나 툴바를 누를 수는 없다.** 열자마자 커서를 놓고 싶으면 `ref.current.focus()` 를 그냥 부르면 된다 — 준비되는 순간 대신 실행된다.
1215
1244
 
@@ -1670,24 +1699,28 @@ export default function ProductListPage() {
1670
1699
 
1671
1700
  행 높이를 줄이는 것은 `dense` 다. 세로 여백만 줄고 좌우 패딩은 그대로라, 값이 잘리지 않으면서 한 화면에 들어가는 행 수가 늘어난다.
1672
1701
 
1673
- **어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 목록 페이지는 밀도를 고정하지 말고 `useDensityToggle` 고를 있게 둔다페이지네이션 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다.
1702
+ **어느 쪽이 편한지는 화면이 아니라 사용자가 안다.** 그래서 `dense` 밀도를 고정하는 스위치가 아니라 **시작 밀도이자 밀도 토글의 스위치**다켜면 하단 바 우측(rows per page 셀렉트 왼쪽)에 `좁게 보기` · `넓게 보기` 링크가 붙는다. 켜고 끄는 별도 prop 은 없다.
1674
1703
 
1675
1704
  ```tsx
1676
- // 사용자가 고른 밀도는 다음 방문에도 남는 것이 자연스럽다 — 저장은 페이지 몫이다
1677
- const [dense, setDense] = useState(() => loadPref('list.dense', true));
1705
+ // 좁게 시작하고, 사용자가 바꾸는 밀도는 표가 알아서 들고 간다
1706
+ <STable dense useRowsPerPageSelect pagination={{ currentPage, lastPage }} />;
1707
+
1708
+ // 사용자가 고른 밀도를 다음 방문에도 남기려는 화면만 받아서 저장한다.
1709
+ // 저장한 값은 시작 밀도로만 돌려준다 — 되돌려 넣지 않는다
1710
+ const [initialDense] = useState(() => loadPref('list.dense', true));
1678
1711
 
1679
1712
  <STable
1680
- dense={dense}
1681
- onDenseChange={next => { setDense(next); savePref('list.dense', next); }}
1682
- useDensityToggle
1713
+ dense={initialDense}
1714
+ onDenseChange={next => savePref('list.dense', next)}
1683
1715
  useRowsPerPageSelect
1684
1716
  pagination={{ currentPage, lastPage }}
1685
1717
  />;
1686
1718
  ```
1687
1719
 
1688
- - **밀도는 `STable` 이 갖지 않는다.** `dense` 곧 현재 상태이고, `onDenseChange` 없이 `useDensityToggle` 켜면 눌러도 아무 일도 일어나지 않는다.
1689
- - **토글은 페이지네이션이 있을 때만 나타난다** 사는 곳이 바이기 때문이다. 페이지네이션 없는 표에서 밀도를 고르게 하려면 `STableBar` 쪽에 직접 둔다.
1690
- - 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. `dense` `넓게 보기` 다.
1720
+ - **누른 뒤의 밀도는 `STable` 이 내부 상태로 들고 간다.** `onDenseChange` 없이도 토글은 동작한다. 저장이 필요한 화면만 받아서 저장하면 된다.
1721
+ - **`dense` `onDenseChange` 값을 되돌려 넣지 않는다.** 토글을 붙일지는 prop 정하므로, 넓게 순간 `dense` `false` 되면 토글이 사라져 다시 좁힐 길이 없다. prop 값을 바꿔 넘기는 것은 외부 버튼 등으로 밀도를 **되돌릴 때**만 쓴다 — 내부 밀도가 그 값으로 맞춰진다.
1722
+ - **`dense` 페이지네이션이 없어도 토글이 나온다** 하단 바를 토글만 담아 그린다. `dense` 아니면 토글도 없다.
1723
+ - 라벨과 아이콘은 현재 상태가 아니라 **누르면 되는 상태**를 가리킨다. 좁게 보고 있으면 `넓게 보기` 다.
1691
1724
 
1692
1725
  ### 4-3. 폼 페이지 (등록/수정)
1693
1726
 
@@ -1903,7 +1936,7 @@ export default function ProductDetailPage() {
1903
1936
  - [ ] 필터·폼·상세 정보를 `SKeyValueTable` 로 만들었는가 (컨트롤을 `div` 로 나열하지 않았는가). 필터를 `SChipFilter` 로 했다면 §3-7-11 의 판정을 거쳤는가
1904
1937
  - [ ] 섹션 구분에 `SSectionHeaderCard` 를 썼는가 (직접 만든 카드가 아니라)
1905
1938
  - [ ] 목록의 주요 액션이 `STableBar` 의 `rightActions` 에 있는가 (페이지 제목 줄이 아니라)
1906
- - [ ] 목록 페이지 표에 `useDensityToggle` 로 밀도를 고를 있게 뒀는가, `onDenseChange` 함께 줬는가 (§4-2 — 핸들러 없이 켜면 눌러도 아무 일도 없다)
1939
+ - [ ] 목록 페이지 표에 `dense` 로 밀도 토글을 띄웠는가, 값을 `onDenseChange` 결과로 되돌려 넣지는 않았는가 (§4-2 — 되돌려 넣으면 넓게 순간 토글이 사라진다)
1907
1940
  - [ ] 상태 표시에 `STag size="sm"` 을 썼는가
1908
1941
  - [ ] 테이블에서 양을 나타내는 컬럼(금액·수량·개수 등)이 전부 `align: 'right'` 인가
1909
1942
  - [ ] 번호·코드·전화번호·일자 컬럼에 `align: 'center'` 를 **명시**했는가 (생략하면 좌측이 된다)