sellmate-design-system-react 9.0.0-beta.95 → 9.0.0-beta.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 표 행 펼침·접힘 모션 — `STreeTable` 의 하위 행과 `STable` 의 상세 영역(`renderExpansion`)이 같은
3
+ * 서랍 움직임을 쓴다. 하나를 바꾸면 두 표가 함께 바뀐다.
4
+ *
5
+ * 표 행(`<tr>`)은 높이를 0 부터 늘리고 줄일 수 없어서, 높이 대신 **보이는 위치**를 움직인다(FLIP).
6
+ * 행은 처음부터 제자리에 넣어 두고, 아래 행은 늘어난 높이만큼 위에서 출발해 제자리로 내려오며,
7
+ * 펼쳐지는 덩어리는 바로 위 행 아랫변 밑으로 나온 부분만 보이게 잘라 낸다. 접힘은 그 반대다.
8
+ * 행 높이는 그대로라 가상 스크롤의 실측도 흔들리지 않는다.
9
+ */
10
+ /** 행 식별 값 */
11
+ export type RowMotionKey = string | number;
12
+ /**
13
+ * 펼침·접힘 모션. 접힘이 더 짧고 ease-in 인 것은 닫는 쪽이 빨리 비켜 줘야 다음 조작이 기다리지 않기 때문이다.
14
+ * 해당하는 토큰이 아직 없어 여기에 둔다.
15
+ */
16
+ export declare const EXPAND_MOTION: {
17
+ readonly duration: 220;
18
+ readonly easing: "ease-out";
19
+ };
20
+ export declare const COLLAPSE_MOTION: {
21
+ readonly duration: 140;
22
+ readonly easing: "ease-in";
23
+ };
24
+ export declare function sameKeys(a: ReadonlySet<RowMotionKey>, b: ReadonlySet<RowMotionKey>): boolean;
25
+ /** 모션을 줄인 환경이거나 Web Animations 가 없는 환경(SSR·테스트용 DOM)에서는 움직이지 않고 바로 그린다 */
26
+ export declare function motionAllowed(): boolean;
27
+ /** 진행 중인 펼침·접힘 모션 하나 */
28
+ export interface RowMotion {
29
+ /** 펼쳐지거나 접히는 하위 행 */
30
+ keys: ReadonlySet<RowMotionKey>;
31
+ /** 끝났을 때 열린 정도 — 펼침 1, 접힘 0 */
32
+ to: 0 | 1;
33
+ /** 접힘이 끝까지 가서 남겨 둔 행을 뺄 차례인가 */
34
+ done: boolean;
35
+ /** 끝까지 가면 true, 도중에 취소되면 false */
36
+ finished: Promise<boolean>;
37
+ /** 지금 열린 정도 (0 닫힘 ~ 1 열림) — 도중에 되돌릴 때 여기서 이어 간다 */
38
+ openness: () => number;
39
+ cancel: () => void;
40
+ }
41
+ /**
42
+ * 펼쳐지는 행 덩어리(하위 행·상세 영역)를 열린 정도 `from` 에서 `to` 로 움직인다.
43
+ *
44
+ * 열린 정도 s 에서 덩어리 아래 행은 `덩어리 높이 × (1 - s)` 만큼 위에 있고, 덩어리 안 행도 같이 올라가
45
+ * 바로 위 행 아랫변 밑으로 나온 부분만 보인다. 그래서 잘려 나가는 자리와 올라오는 행이 겹치지 않는다.
46
+ * 여러 부모가 한 번에 바뀌면 덩어리가 여럿이고, 아래 행은 지나온 덩어리 높이를 모두 더한 만큼 움직인다.
47
+ */
48
+ export declare function startRowMotion({ scrollEl, isInBlock, blockKeys, from, to, }: {
49
+ scrollEl: HTMLElement;
50
+ /** 그려진 행(`<tr>`)이 펼쳐지는 덩어리에 드는가 */
51
+ isInBlock: (tr: HTMLElement) => boolean;
52
+ /** 펼쳐지는 덩어리의 키 — 도중에 되돌리는지 가릴 때 쓴다 */
53
+ blockKeys: ReadonlySet<RowMotionKey>;
54
+ from: number;
55
+ to: 0 | 1;
56
+ }): RowMotion | null;
@@ -580,6 +580,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
580
580
  | 나열한 항목을 펼쳐 하위 내용을 보여준다 | `SExpansionList` + `SExpansionItem` | §3-7-7 |
581
581
  | 부모-자식 계층을 들여쓰기로 보여준다 | `STree` | §3-7-7 — 폼 값으로 드롭다운에서 고르는 것이면 `SSelect` `type="tree"` |
582
582
  | 계층을 이루는 행들을 여러 열로 보여준다 (카테고리 > 상품 > 옵션) | `STreeTable` | §3-7-7 — 한 줄에 라벨 하나면 `STree` |
583
+ | 표의 행마다 그 건의 상세를 펼쳐 보여준다 (한 행 = 한 건) | `STable` `renderExpansion` | §3-7-7 — 행끼리 계층이면 `STreeTable` |
583
584
  | 사용자가 순서를 드래그로 바꾸게 한다 | `SDraggableList` + `SDraggableItem` | §3-7-6 |
584
585
  | 날짜별 일정을 한 달치 격자로 펼쳐 보여준다 | `SCalendarBoard` | §3-7-4 |
585
586
  | 항목끼리 크기를 눈으로 견주게 한다 (채널별 매출·상품별 판매량) | `SBarChart` | 정확한 값을 읽어야 하면 `STable` — 그래프는 "어느 쪽이 큰가" 를 답하지 "얼마인가" 를 답하지 않는다 |
@@ -1101,6 +1102,7 @@ const columns: STableColumn<Order>[] = [
1101
1102
  - **컨트롤이 들어가는 컬럼에는 `contentType: 'control'` 을 함께 준다.** 좌우 패딩이 텍스트용(넓게)에서 컨트롤용(좁게)으로 바뀌어, 같은 컬럼 폭에서도 요소가 쓸 폭이 넓어진다. 기본값은 `text` 다.
1102
1103
  - 요소가 둘 이상이면 요소 폭의 합에 **`gap` 까지** 더한다 (행 내부 인라인 액션 간격은 `gap-sd-4` 고정, §3-5-5).
1103
1104
  - `SSelect` · `SInput` 처럼 셀 폭을 채우는 컨트롤은 **컬럼 폭이 곧 컨트롤 폭**이다. 실제 선택값·입력값이 말줄임 없이 읽히는 폭인지 확인한다.
1105
+ - **셀 안 컨트롤을 누른 것은 행 클릭(`onRowClick`)·더블클릭이 아니다.** 버튼·링크·입력과 선택 체크박스 열은 그 컨트롤의 동작만 일으킨다 — 셀이나 컨트롤에 `stopPropagation` 을 직접 넣지 않는다.
1104
1106
  - 폭을 넉넉히 줄 수 없는 자리는 폭을 줄이는 게 아니라 **요소를 바꾼다** — 라벨 버튼 대신 아이콘만 있는 `SGhostButton`, `size="xs"` (§3-5-2, §3-5-5).
1105
1107
 
1106
1108
  ```tsx
@@ -1349,6 +1351,7 @@ tableRef.current.scrollToRow(row); // 복원
1349
1351
  | 본문 속 이동 링크 | `STextLink` |
1350
1352
  | 메인 액션 + 부가 메뉴 | `SDropdownButton` |
1351
1353
  | **GNB system 패널의 한 줄 액션** | **`SSystemActionButton`** — 행 전체가 버튼인 표면형 (아래 3-5-7) |
1354
+ | **본문·목록 사이에 끼어 서는 작은 부가 버튼** (추천 질문·출처 바로가기) | **`SButton pill color="neutral" outline`** (아래 3-5-6) |
1352
1355
 
1353
1356
  #### 3-5-1. 채움 버튼은 페이지당 개수 제한이 있다
1354
1357
 
@@ -1490,6 +1493,7 @@ tableRef.current.scrollToRow(row); // 복원
1490
1493
  - 라벨은 **동사(+목적어)** — `저장` · `적치 지시` · `송장 재출력`
1491
1494
  - 완료된 작업 버튼은 제거하지 않고 **`disabled` 로 남긴다.** 자리가 사라지면 행 높이가 흔들린다.
1492
1495
  - **모달 보조 액션은 `color="neutral" outline`** 이 표준이다.
1496
+ - **`pill` 은 본문·목록 사이에 끼어 서는 작은 부가 버튼에만 준다** — 추천 질문·출처 바로가기처럼 누르면 동작하는 알약. 보통 `color="neutral" outline size="xs"` 와 함께 쓴다. 화면의 주 액션·폼 제출·하단 버튼은 모서리를 바꾸지 않는다. 값·필터 토큰이면 `SChip`, 누를 수 없는 표시면 `STag` 다. 알약 모양을 `className="rounded-full"` 로 직접 만들지 않는다.
1493
1497
 
1494
1498
  **하단 버튼 배치 (모든 화면 공통)** — 그룹을 모아 정렬하지 않고 **양끝으로 벌린다**(`justify-between`).
1495
1499
 
@@ -1943,7 +1947,8 @@ const [from, setFrom] = useState<string | null>(null);
1943
1947
  직접 쓰는 경우는 하나뿐이다 — **디자인 시스템에 없는 컨트롤**에 다른 필드와 똑같은 라벨·필수·에러 모양을 붙일 때.
1944
1948
 
1945
1949
  - **라벨은 `controlId` 로 컨트롤에 잇는다.** 컨트롤에 `id` 를 주고 같은 값을 `controlId` 로 넘기면 라벨이 그 컨트롤의 이름이 되고, 라벨을 누르면 컨트롤에 포커스가 간다. 컨트롤이 `input`·`textarea`·`select`·`button` 이 아니라 `role` 로 만든 요소면 `aria-labelledby={fieldLabelId(id)}` 를 직접 건다.
1946
- - **라벨이 보이는 입력에 `aria-label` 을 다시 적지 않는다.** DS 입력 컴포넌트는 `label` 을 알아서 컨트롤에 잇는다. 화면의 라벨과 읽히는 이름을 두 번 적으면 어긋난다. `aria-label`(`SCheckbox`·`SChipInput` 은 `ariaLabel`)은 **라벨 글자 없이 서는 컨트롤**에만 준다 — 표의 행 선택 체크박스처럼.
1950
+ - **라벨이 보이는 입력에 `aria-label` 을 다시 적지 않는다.** DS 입력 컴포넌트는 `label` 을 알아서 컨트롤에 잇는다. 화면의 라벨과 읽히는 이름을 두 번 적으면 어긋난다. `aria-label`(`SCheckbox`·`SChipInput`·`SSelect` 는 `ariaLabel`)은 **라벨 글자 없이 서는 컨트롤**에만 준다 — 목록 위 정렬 셀렉트처럼. 라벨 없이 서는 `SSelect` 에 `ariaLabel` 을 빠뜨리면 고른 값만 읽혀 무엇을 고르는 칸인지 알 수 없다.
1951
+ - **`selectable` 표에는 `rowSelectLabel` 을 준다.** 행 체크박스는 그 행을 가리키는 글자로 이름이 붙어야 한다(`row => \`${row.orderNo} 선택\``). 주지 않으면 모든 행이 "행 선택" 으로 읽혀 구분되지 않는다. 헤더는 기본 "전체 선택" 이고, 무엇의 전체인지 밝혀야 하면 `selectAllLabel` 로 바꾼다. `STreeTable` 도 같다.
1947
1952
 
1948
1953
  ```tsx
1949
1954
  ✅ <SField label="색상" controlId={id}><input id={id} type="color" /></SField>
@@ -1999,7 +2004,7 @@ const [from, setFrom] = useState<string | null>(null);
1999
2004
  ❌ <div onKeyDown={e => { e.stopPropagation(); … }}> {/* 리스트가 이미 걸러 준다 */}
2000
2005
  ```
2001
2006
 
2002
- #### 3-7-7. 펼치는 넷 — SExpansionItem vs SExpansionList vs STree vs STreeTable
2007
+ #### 3-7-7. 펼치는 다섯 — SExpansionItem vs SExpansionList vs STree vs STreeTable vs STable 펼침
2003
2008
 
2004
2009
  | 판별 | 사용 |
2005
2010
  | --- | --- |
@@ -2007,12 +2012,14 @@ const [from, setFrom] = useState<string | null>(null);
2007
2012
  | **한 번에 하나만** 열려야 한다 (아코디언) | `SExpansionList` + `SExpansionItem` |
2008
2013
  | **부모-자식 계층 자체**를 보여줘야 한다 (2단 이상, 연결선) | `STree` |
2009
2014
  | 계층을 이루는 행마다 **열이 둘 이상**이다 (재고·가격처럼 행끼리 비교하는 값) | `STreeTable` |
2015
+ | 표의 **행마다 그 건의 상세**를 펼친다 (한 행 = 한 건, 상세는 열로 나뉘지 않는 내용) | `STable` + `renderExpansion` |
2010
2016
 
2011
2017
  `STreeTable` 은 `STable` 에 계층을 붙인 것이다. 헤더·셀·밀도·정렬 버튼·sticky·가상 스크롤은 `STable` 과 같고, 컬럼 규칙(§3-4)도 그대로 따른다. 다른 점만 적는다.
2012
2018
 
2013
2019
  - **하위 행은 각 행의 `children` 에 같은 모양으로 넣는다.** 필드 이름이 다르면 `childrenKey` 로 알려 준다. 평평한 목록에 `parentId` 를 달아 넘기지 않는다 — 계층으로 바꿔서 넘긴다.
2014
2020
  - **펼침 버튼·들여쓰기는 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `treeColumn` 으로 옮긴다. 그 컬럼은 왼쪽 정렬로 두고, 셀 내용은 `render`·`format` 으로 바꾼다 — `renderCell` 은 펼침 버튼을 넣을 자리가 없어 에러가 난다.
2015
2021
  - **셀 병합은 가로(`colSpan`)만 한다.** 트리 컬럼이 아닌 컬럼은 `renderCell` 로 `<td>` 를 바꿀 수 있고, 한 행 안에서 끝나는 `colSpan` 은 접고 펴도 그대로다. **`rowSpan` 으로 세로 병합하지 않는다** — 행이 접히면 사라진 행 몫까지 병합이 남아 아래 행의 칸을 덮고, 덮인 행의 칸은 옆 컬럼으로 밀려난다. 부모와 하위 행에 같은 값이 반복되면 부모 행에만 쓰고 하위 행은 비워 둔다.
2022
+ - **"한 행 = 한 건, 펼치면 그 건의 상세" 는 `STreeTable` 이 아니다.** 상세를 가짜 하위 행으로 끼워 넣지 않고 `STable` 의 `renderExpansion` 을 쓴다 (§3-7-7 아래).
2016
2023
  - **펼침 영역은 트리 컬럼 셀 전체다.** 하위 행이 있는 행은 펼침 버튼뿐 아니라 그 셀 어디를 눌러도 접고 편다. 다른 셀을 누르면 행 클릭(`onRowClick`)이다 — 트리 컬럼 셀을 누른 것은 행 클릭으로 가지 않는다.
2017
2024
  - **펼침·접힘 모션은 컴포넌트가 넣는다.** 하위 행이 부모 아래에서 서랍처럼 펼쳐지고 접히며, 아래 행들도 함께 내려가고 올라온다. 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)에 애니메이션·`transform`·`clip-path` 를 직접 주지 않는다 — 컴포넌트가 행의 위치를 움직이는 것과 겹친다. `onExpandedChange` 는 모션을 기다리지 않고 누른 즉시 온다.
2018
2025
  - **트리 컬럼에 링크·버튼을 넣어도 된다.** 셀 안의 링크·버튼·입력을 누르면 그 동작만 일어나고 펼침은 일어나지 않는다 — `stopPropagation` 을 직접 넣지 않는다. 상품명을 상세 링크로 거는 것처럼 쓴다.
@@ -2028,6 +2035,20 @@ const [from, setFrom] = useState<string | null>(null);
2028
2035
  ❌ <STable rows={flatten(categories)} columns={[{ …, render: row => `${' '.repeat(row.depth)}${row.name}` }]} /> {/* 계층을 공백으로 흉내 내지 않는다 */}
2029
2036
  ```
2030
2037
 
2038
+ `STable` 의 행 펼침(`renderExpansion`)은 행마다 **그 건의 상세 영역** 하나를 연다. 계층이 아니므로 `STreeTable` 을 쓰지 않는다.
2039
+
2040
+ - **상세 영역은 행이 아니다.** 선택·체크박스·잠금 표시·행 클릭·hover 와 무관하고 체크박스 열까지 덮는 표 전체 폭 한 칸이다. 상세를 가짜 하위 행으로 만들어 `isRowSelectable` 로 잠그지 않는다.
2041
+ - **펼침 버튼은 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `expansionColumn` 으로 옮긴다. 그 셀 전체가 펼침 영역이며 행 클릭(`onRowClick`)으로 가지 않는다. 펼침 버튼만 담는 좁은 컬럼을 따로 세우지 않고, 그 컬럼에는 `renderCell` 을 쓰지 않는다.
2042
+ - **여러 행을 함께 펼칠 수 있다.** 행 클릭으로도 펴야 하면 `expandedKeys`·`onExpandedChange` 로 들고 `onRowClick` 에서 키를 넣고 뺀다.
2043
+ - **펼침·접힘 모션은 컴포넌트가 넣는다.** `STreeTable` 하위 행과 같은 서랍 움직임이고 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)이나 상세 내용에 펼침 애니메이션·`transform` 을 직접 주지 않는다.
2044
+ - **상세의 여백은 컴포넌트가 넣는다.** 셀과 같은 좌우·위아래 여백이 이미 들어가 있으므로 내용에 바깥 패딩을 직접 주지 않는다.
2045
+ - `renderCell` 의 `rowSpan` 세로 병합과 함께 쓰지 않는다 — 펼친 상세가 병합 사이에 끼어든다.
2046
+
2047
+ ```tsx
2048
+ ✅ <STable<Report> columns={columns} rows={reports} renderExpansion={report => <ReportDetail report={report} />} />
2049
+ ❌ <STreeTable rows={reports.map(r => ({ ...r, children: [{ id: `${r.id}:detail`, detail: r }] }))} … /> {/* 상세를 가짜 하위 행으로 만들지 않는다 */}
2050
+ ```
2051
+
2031
2052
  `SExpansionList` 는 depth 별 **단일 확장**을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다. 항목 사이 구분선은 여기서도 리스트가 알아서 그린다 — 따로 줄 prop 이 없다 (§3-7-6).
2032
2053
 
2033
2054
  ##### 펼침 ≠ 선택
@@ -6083,6 +6104,7 @@ export type SBarcodeInputSize = SFieldSize;
6083
6104
  | `icon?` | `SIconName` | — | 레이블 왼쪽 아이콘 |
6084
6105
  | `rightIcon?` | `SIconName` | — | 레이블 오른쪽 아이콘 |
6085
6106
  | `label?` | `string` | — | 버튼 텍스트 (문자열만 — 아이콘은 icon/rightIcon 사용) |
6107
+ | `pill?` | `boolean` | `false` | 모서리를 완전히 둥글게(알약 모양). 색·크기·아이콘은 그대로다. 본문·목록 사이에 끼어 서는 **누르면 동작하는 작은 부가 버튼**에 쓴다 — 출처 배지·추천 질문처럼. 보통 `color="neutral" outline` 과 함께 쓴다(흰 면 + 옅은 테두리). 화면의 주 액션·폼 제출 버튼은 모서리를 바꾸지 않는다. 값·필터 토큰이면 `SChip`, 누를 수 없는 표시면 `STag` 다. |
6086
6108
 
6087
6109
  ## Types
6088
6110
 
@@ -11395,6 +11417,7 @@ export type SSectionHeaderCardChildren = ReactNode;
11395
11417
  | `serverSearch?` | `boolean` | `false` | 검색을 서버로 넘긴다 — 내부 필터링을 하지 않고 `options` 를 받은 그대로 보여준다. `onSearchChange` 로 온 검색어에 맞는 목록을 소비 앱이 다시 내려줘야 한다. 켜면 검색바가 옵션 수와 무관하게 항상 나온다 — 검색 결과가 줄었다고 검색바가 사라지면 검색어를 지울 수단이 없어지기 때문이다. |
11396
11418
  | `searchDebounce?` | `number` | `DEFAULT_SEARCH_DEBOUNCE` | `serverSearch` 에서 검색어를 서버로 넘기기 전 기다리는 시간(ms) |
11397
11419
  | `label?` | `string` | — | |
11420
+ | `ariaLabel?` | `string` | — | 스크린리더용 접근성 레이블. **`label` 이 없을 때만 준다** — 표의 셀이나 필터 줄처럼 라벨 글자 없이 서는 셀렉트는 이것이 없으면 고른 값만 읽혀 무엇을 고르는 칸인지 알 수 없다. 라벨처럼 고른 값 앞에 이어 읽힌다("정렬 최신순"). `label` 이 있으면 그 글자가 이미 이름이 되므로 이 값은 쓰지 않는다. |
11398
11421
  | `labelPosition?` | `SFieldLabelPosition` | — | 레이블 위치 — `top` 이면 라벨이 컨트롤 위에 선다 (SField 로 그대로 넘어간다). |
11399
11422
  | `subLabel?` | `string` | — | 보조 레이블 — 레이블 오른쪽 같은 줄에 붙는다 (SField 로 그대로 넘어간다). |
11400
11423
  | `labelWidth?` | `number \| string` | — | |
@@ -12041,6 +12064,8 @@ export interface SSystemActionButtonMenuOption {
12041
12064
  | `rows?` | `TRow[]` | `[]` | |
12042
12065
  | `rowKey?` | `string` | `'id'` | 행 식별 필드 |
12043
12066
  | `selectable?` | `boolean` | `false` | 행 선택 체크박스 |
12067
+ | `rowSelectLabel?` | `(row: TRow) => string` | — | 행 선택 체크박스의 접근성 이름 — 화면 낭독기가 어느 행의 체크박스인지 읽게 한다. 그 행을 가리키는 글자(주문번호·상품명 등)로 만든다: `row => \`${row.orderNo} 선택\``. 주지 않으면 모든 행이 `행 선택` 으로 읽혀 행끼리 구분되지 않는다. |
12068
+ | `selectAllLabel?` | `string` | `'전체 선택'` | 헤더 전체 선택 체크박스의 접근성 이름 |
12044
12069
  | `dragSelectable?` | `boolean` | `false` | 행을 드래그해서 고른다. 기본 `false`. 고르는 수단만 다를 뿐 `selectable` 과 같은 선택이다 — `selected` · `onSelectedChange` · `isRowSelectable` 을 그대로 쓴다. 체크박스 열은 생기지 않고, 대신 고른 구간이 배경색과 바깥 테두리로 표시된다. **`selectable` 과 함께 켜면 `selectable` 이 이긴다** — 체크박스가 있는 표에서 드래그까지 걸리면 글자를 긁으려던 손이 선택을 갈아치운다. 둘 다 필요해 보이면 체크박스 쪽만 남긴다. 누른 자리가 구간의 기준점이고, 끌어간 자리까지가 구간이다. **새로 끌면 이전 선택은 풀린다** — 기존 선택에 더하려면 `Ctrl`(macOS 는 `Cmd`)을 짚고 끈다. 잠긴 행(`isRowSelectable`)은 구간 안에 있어도 그냥 지나간다. **켜 두면 셀 안의 글자를 긁어 복사할 수 없다** — 끌기가 곧 선택이라 글자 선택과 같은 손짓을 두고 다툰다. 값을 복사해 가는 표에는 켜지 않는다. 셀 안의 버튼·입력은 그대로 눌린다. |
12045
12070
  | `contextMenuItems?` | `STableContextMenuItem[]` | — | 드래그 선택에서 **행을 오른쪽 클릭했을 때** 커서 자리에 뜨는 메뉴의 항목. 주지 않거나 비면 메뉴가 뜨지 않고 브라우저 기본 메뉴가 나온다. **`dragSelectable` 에서만 동작한다** — 체크박스 모드에서는 무시된다. 고르지 않은 행에서 누르면 **그 행만 고른 뒤** 열린다. 메뉴가 다룰 대상과 화면에 칠해진 것이 어긋나지 않게 하기 위함이다. 잠긴 행(`isRowSelectable`) 위에서는 열리지 않는다 — 고를 수 없는 행을 대상으로 삼을 수 없기 때문이다. |
12046
12071
  | `isRowSelectable?` | `(row: TRow) => boolean` | — | 이 행을 고를 수 있는가. 주지 않으면 모든 행을 고를 수 있다. 고를 수 없는 행은 체크박스가 잠기고, **전체 선택·Shift 구간 선택의 셈에서도 빠진다** — 잠긴 행이 셈에 남으면 "전부 선택됨"에 닿지 못해 헤더 체크박스가 해제 방향으로 가지 못한다. 목록에서 지워 버리는 것과 다르다. 실패 0건인 차수처럼 **자리는 보여야 하지만 대상이 될 수는 없는 행**에 쓴다. 아예 대상이 아니라면 `rows` 에서 거르는 편이 낫다. 잠금은 그리기 시점의 판정일 뿐이라, 이미 `selected` 에 든 행이 나중에 잠겨도 DS 가 빼지 않는다(제어 상태를 말없이 바꾸지 않는다). 다만 헤더의 전체 해제로는 걷어낼 수 있다. |
@@ -12068,6 +12093,10 @@ export interface SSystemActionButtonMenuOption {
12068
12093
  | `virtualBuffer?` | `number` | `5` | |
12069
12094
  | `virtualEndThreshold?` | `number` | `10` | |
12070
12095
  | `tableId?` | `string` | — | 테이블 식별자 (없으면 자동 생성) |
12096
+ | `renderExpansion?` | `(row: TRow) => ReactNode` | — | 행마다 펼치는 **상세 영역**. 주면 펼침 컬럼(`expansionColumn`)에 펼침 버튼이 붙고, 펼친 행 바로 아래에 돌려준 내용이 표 전체 폭으로 그려진다. 여러 행을 함께 펼칠 수 있다. "한 행 = 한 건, 펼치면 그 건의 상세" 인 표에 쓴다. 행이 부모-자식 계층을 이루면 `STreeTable` 이다. - 상세 영역은 **행이 아니다** — 선택·체크박스·잠금 표시·행 클릭·hover 와 무관하고, 행 수에 세지 않는다. - 체크박스 열까지 덮는 한 칸이고, 가로 스크롤이 있어도 보이는 폭에 머문다. 줄바꿈을 허용한다. - 행 높이를 늘리므로 `renderCell` 의 `rowSpan` 세로 병합과 함께 쓰지 않는다. - 가상 스크롤에서는 상세 영역까지 그 행의 높이로 잰다. - 펼침·접힘 모션은 컴포넌트가 넣는다 — 상세가 행 아래에서 서랍처럼 열리고 아래 행들이 함께 움직인다 (`STreeTable` 과 같다). 모션을 줄인 환경에서는 꺼진다. `onExpandedChange` 는 모션을 기다리지 않는다. |
12097
+ | `expansionColumn?` | `string` | — | 펼침 버튼이 붙는 컬럼의 `name`. 주지 않으면 보이는 첫 컬럼이다. **이 셀 전체가 펼침 영역**이다 — 어디를 눌러도 접고 펴고, 행 클릭(`onRowClick`)으로는 가지 않는다. 셀 안에 넣은 링크·버튼·입력은 자기 동작만 한다. 이 컬럼에는 `renderCell` 을 쓰지 않는다 — 셀 내용은 `render` · `format` 으로 바꾼다 (`STreeTable` 의 `treeColumn` 과 같다). |
12098
+ | `expandedKeys?` | `STableKey[]` | — | 펼친 행의 키 목록 (controlled) |
12099
+ | `defaultExpandedKeys?` | `STableKey[]` | — | 비제어 펼침 초기값. 주지 않으면 모두 접힌 채로 시작한다 |
12071
12100
  | `className?` | `string` | — | |
12072
12101
  | `style?` | `CSSProperties` | — | |
12073
12102
 
@@ -12084,8 +12113,9 @@ export interface SSystemActionButtonMenuOption {
12084
12113
  | `onScroll` | `(event: ReactUIEvent<HTMLDivElement>) => void` | 본문이 스크롤될 때. **실제 스크롤에만 울린다** — 행 수·컬럼 폭·로딩이 바뀌어 내부에서 위치를 다시 잴 때는 오지 않는다. `onVirtualUpdate` 는 그리는 창이 바뀔 때만 오므로 그 사이 스크롤을 알 수 없다. 스크롤 위치를 읽어 두었다 되돌리는 것처럼 **위치 자체가 필요하면** 이쪽을 쓴다. |
12085
12114
  | `onVirtualUpdate` | `(range: { from: number; to: number }) => void` | |
12086
12115
  | `onVirtualReachEnd` | `(info: { from: number; to: number; rowCount: number; threshold: number; }) =>…` | |
12087
- | `onRowClick` | `(row: TRow) => void` | 행 클릭 |
12116
+ | `onRowClick` | `(row: TRow) => void` | 행 클릭. 선택 체크박스 열과 셀 안의 버튼·링크·입력을 누른 것은 행 클릭이 아니다 — 그 컨트롤의 동작만 일어난다. 셀에 버튼을 두고 `stopPropagation` 을 직접 넣지 않는다. |
12088
12117
  | `onRowDoubleClick` | `(row: TRow) => void` | 행 더블클릭. 목록에서 상세를 여는 것처럼 **한 번 누르는 것과 다른 일**을 걸 때 쓴다. 더블클릭도 클릭 두 번이라 `onRowClick` 과 함께 주면 **`onRowClick` 이 두 번 먼저 울린다** — 둘을 함께 쓰려면 `onRowClick` 은 두 번 울려도 결과가 같은 일(행 강조 등)이어야 한다. 선택 체크박스 열과 셀 안의 버튼·링크·입력 위에서 한 더블클릭은 행 더블클릭이 아니다 — 체크박스를 빠르게 두 번 누르거나 입력의 글자를 더블클릭으로 고르다 상세가 열리지 않게 한다. |
12118
+ | `onExpandedChange` | `(expandedKeys: STableKey[], row: TRow) => void` | 펼침 변경. 바뀐 뒤의 키 목록과 누른 행이 온다 |
12089
12119
 
12090
12120
  #### Methods (ref)
12091
12121
 
@@ -12190,6 +12220,13 @@ export interface STablePagination {
12190
12220
  }
12191
12221
  ```
12192
12222
 
12223
+ ### STableKey
12224
+
12225
+ ```ts
12226
+ /** 행 식별 값 — `rowKey` 필드에 든 값이다 */
12227
+ export type STableKey = string | number;
12228
+ ```
12229
+
12193
12230
  ### STableColumnField
12194
12231
 
12195
12232
  ```ts
package/dist/llms.txt CHANGED
@@ -581,6 +581,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
581
581
  | 나열한 항목을 펼쳐 하위 내용을 보여준다 | `SExpansionList` + `SExpansionItem` | §3-7-7 |
582
582
  | 부모-자식 계층을 들여쓰기로 보여준다 | `STree` | §3-7-7 — 폼 값으로 드롭다운에서 고르는 것이면 `SSelect` `type="tree"` |
583
583
  | 계층을 이루는 행들을 여러 열로 보여준다 (카테고리 > 상품 > 옵션) | `STreeTable` | §3-7-7 — 한 줄에 라벨 하나면 `STree` |
584
+ | 표의 행마다 그 건의 상세를 펼쳐 보여준다 (한 행 = 한 건) | `STable` `renderExpansion` | §3-7-7 — 행끼리 계층이면 `STreeTable` |
584
585
  | 사용자가 순서를 드래그로 바꾸게 한다 | `SDraggableList` + `SDraggableItem` | §3-7-6 |
585
586
  | 날짜별 일정을 한 달치 격자로 펼쳐 보여준다 | `SCalendarBoard` | §3-7-4 |
586
587
  | 항목끼리 크기를 눈으로 견주게 한다 (채널별 매출·상품별 판매량) | `SBarChart` | 정확한 값을 읽어야 하면 `STable` — 그래프는 "어느 쪽이 큰가" 를 답하지 "얼마인가" 를 답하지 않는다 |
@@ -1102,6 +1103,7 @@ const columns: STableColumn<Order>[] = [
1102
1103
  - **컨트롤이 들어가는 컬럼에는 `contentType: 'control'` 을 함께 준다.** 좌우 패딩이 텍스트용(넓게)에서 컨트롤용(좁게)으로 바뀌어, 같은 컬럼 폭에서도 요소가 쓸 폭이 넓어진다. 기본값은 `text` 다.
1103
1104
  - 요소가 둘 이상이면 요소 폭의 합에 **`gap` 까지** 더한다 (행 내부 인라인 액션 간격은 `gap-sd-4` 고정, §3-5-5).
1104
1105
  - `SSelect` · `SInput` 처럼 셀 폭을 채우는 컨트롤은 **컬럼 폭이 곧 컨트롤 폭**이다. 실제 선택값·입력값이 말줄임 없이 읽히는 폭인지 확인한다.
1106
+ - **셀 안 컨트롤을 누른 것은 행 클릭(`onRowClick`)·더블클릭이 아니다.** 버튼·링크·입력과 선택 체크박스 열은 그 컨트롤의 동작만 일으킨다 — 셀이나 컨트롤에 `stopPropagation` 을 직접 넣지 않는다.
1105
1107
  - 폭을 넉넉히 줄 수 없는 자리는 폭을 줄이는 게 아니라 **요소를 바꾼다** — 라벨 버튼 대신 아이콘만 있는 `SGhostButton`, `size="xs"` (§3-5-2, §3-5-5).
1106
1108
 
1107
1109
  ```tsx
@@ -1350,6 +1352,7 @@ tableRef.current.scrollToRow(row); // 복원
1350
1352
  | 본문 속 이동 링크 | `STextLink` |
1351
1353
  | 메인 액션 + 부가 메뉴 | `SDropdownButton` |
1352
1354
  | **GNB system 패널의 한 줄 액션** | **`SSystemActionButton`** — 행 전체가 버튼인 표면형 (아래 3-5-7) |
1355
+ | **본문·목록 사이에 끼어 서는 작은 부가 버튼** (추천 질문·출처 바로가기) | **`SButton pill color="neutral" outline`** (아래 3-5-6) |
1353
1356
 
1354
1357
  #### 3-5-1. 채움 버튼은 페이지당 개수 제한이 있다
1355
1358
 
@@ -1491,6 +1494,7 @@ tableRef.current.scrollToRow(row); // 복원
1491
1494
  - 라벨은 **동사(+목적어)** — `저장` · `적치 지시` · `송장 재출력`
1492
1495
  - 완료된 작업 버튼은 제거하지 않고 **`disabled` 로 남긴다.** 자리가 사라지면 행 높이가 흔들린다.
1493
1496
  - **모달 보조 액션은 `color="neutral" outline`** 이 표준이다.
1497
+ - **`pill` 은 본문·목록 사이에 끼어 서는 작은 부가 버튼에만 준다** — 추천 질문·출처 바로가기처럼 누르면 동작하는 알약. 보통 `color="neutral" outline size="xs"` 와 함께 쓴다. 화면의 주 액션·폼 제출·하단 버튼은 모서리를 바꾸지 않는다. 값·필터 토큰이면 `SChip`, 누를 수 없는 표시면 `STag` 다. 알약 모양을 `className="rounded-full"` 로 직접 만들지 않는다.
1494
1498
 
1495
1499
  **하단 버튼 배치 (모든 화면 공통)** — 그룹을 모아 정렬하지 않고 **양끝으로 벌린다**(`justify-between`).
1496
1500
 
@@ -1944,7 +1948,8 @@ const [from, setFrom] = useState<string | null>(null);
1944
1948
  직접 쓰는 경우는 하나뿐이다 — **디자인 시스템에 없는 컨트롤**에 다른 필드와 똑같은 라벨·필수·에러 모양을 붙일 때.
1945
1949
 
1946
1950
  - **라벨은 `controlId` 로 컨트롤에 잇는다.** 컨트롤에 `id` 를 주고 같은 값을 `controlId` 로 넘기면 라벨이 그 컨트롤의 이름이 되고, 라벨을 누르면 컨트롤에 포커스가 간다. 컨트롤이 `input`·`textarea`·`select`·`button` 이 아니라 `role` 로 만든 요소면 `aria-labelledby={fieldLabelId(id)}` 를 직접 건다.
1947
- - **라벨이 보이는 입력에 `aria-label` 을 다시 적지 않는다.** DS 입력 컴포넌트는 `label` 을 알아서 컨트롤에 잇는다. 화면의 라벨과 읽히는 이름을 두 번 적으면 어긋난다. `aria-label`(`SCheckbox`·`SChipInput` 은 `ariaLabel`)은 **라벨 글자 없이 서는 컨트롤**에만 준다 — 표의 행 선택 체크박스처럼.
1951
+ - **라벨이 보이는 입력에 `aria-label` 을 다시 적지 않는다.** DS 입력 컴포넌트는 `label` 을 알아서 컨트롤에 잇는다. 화면의 라벨과 읽히는 이름을 두 번 적으면 어긋난다. `aria-label`(`SCheckbox`·`SChipInput`·`SSelect` 는 `ariaLabel`)은 **라벨 글자 없이 서는 컨트롤**에만 준다 — 목록 위 정렬 셀렉트처럼. 라벨 없이 서는 `SSelect` 에 `ariaLabel` 을 빠뜨리면 고른 값만 읽혀 무엇을 고르는 칸인지 알 수 없다.
1952
+ - **`selectable` 표에는 `rowSelectLabel` 을 준다.** 행 체크박스는 그 행을 가리키는 글자로 이름이 붙어야 한다(`row => \`${row.orderNo} 선택\``). 주지 않으면 모든 행이 "행 선택" 으로 읽혀 구분되지 않는다. 헤더는 기본 "전체 선택" 이고, 무엇의 전체인지 밝혀야 하면 `selectAllLabel` 로 바꾼다. `STreeTable` 도 같다.
1948
1953
 
1949
1954
  ```tsx
1950
1955
  ✅ <SField label="색상" controlId={id}><input id={id} type="color" /></SField>
@@ -2000,7 +2005,7 @@ const [from, setFrom] = useState<string | null>(null);
2000
2005
  ❌ <div onKeyDown={e => { e.stopPropagation(); … }}> {/* 리스트가 이미 걸러 준다 */}
2001
2006
  ```
2002
2007
 
2003
- #### 3-7-7. 펼치는 넷 — SExpansionItem vs SExpansionList vs STree vs STreeTable
2008
+ #### 3-7-7. 펼치는 다섯 — SExpansionItem vs SExpansionList vs STree vs STreeTable vs STable 펼침
2004
2009
 
2005
2010
  | 판별 | 사용 |
2006
2011
  | --- | --- |
@@ -2008,12 +2013,14 @@ const [from, setFrom] = useState<string | null>(null);
2008
2013
  | **한 번에 하나만** 열려야 한다 (아코디언) | `SExpansionList` + `SExpansionItem` |
2009
2014
  | **부모-자식 계층 자체**를 보여줘야 한다 (2단 이상, 연결선) | `STree` |
2010
2015
  | 계층을 이루는 행마다 **열이 둘 이상**이다 (재고·가격처럼 행끼리 비교하는 값) | `STreeTable` |
2016
+ | 표의 **행마다 그 건의 상세**를 펼친다 (한 행 = 한 건, 상세는 열로 나뉘지 않는 내용) | `STable` + `renderExpansion` |
2011
2017
 
2012
2018
  `STreeTable` 은 `STable` 에 계층을 붙인 것이다. 헤더·셀·밀도·정렬 버튼·sticky·가상 스크롤은 `STable` 과 같고, 컬럼 규칙(§3-4)도 그대로 따른다. 다른 점만 적는다.
2013
2019
 
2014
2020
  - **하위 행은 각 행의 `children` 에 같은 모양으로 넣는다.** 필드 이름이 다르면 `childrenKey` 로 알려 준다. 평평한 목록에 `parentId` 를 달아 넘기지 않는다 — 계층으로 바꿔서 넘긴다.
2015
2021
  - **펼침 버튼·들여쓰기는 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `treeColumn` 으로 옮긴다. 그 컬럼은 왼쪽 정렬로 두고, 셀 내용은 `render`·`format` 으로 바꾼다 — `renderCell` 은 펼침 버튼을 넣을 자리가 없어 에러가 난다.
2016
2022
  - **셀 병합은 가로(`colSpan`)만 한다.** 트리 컬럼이 아닌 컬럼은 `renderCell` 로 `<td>` 를 바꿀 수 있고, 한 행 안에서 끝나는 `colSpan` 은 접고 펴도 그대로다. **`rowSpan` 으로 세로 병합하지 않는다** — 행이 접히면 사라진 행 몫까지 병합이 남아 아래 행의 칸을 덮고, 덮인 행의 칸은 옆 컬럼으로 밀려난다. 부모와 하위 행에 같은 값이 반복되면 부모 행에만 쓰고 하위 행은 비워 둔다.
2023
+ - **"한 행 = 한 건, 펼치면 그 건의 상세" 는 `STreeTable` 이 아니다.** 상세를 가짜 하위 행으로 끼워 넣지 않고 `STable` 의 `renderExpansion` 을 쓴다 (§3-7-7 아래).
2017
2024
  - **펼침 영역은 트리 컬럼 셀 전체다.** 하위 행이 있는 행은 펼침 버튼뿐 아니라 그 셀 어디를 눌러도 접고 편다. 다른 셀을 누르면 행 클릭(`onRowClick`)이다 — 트리 컬럼 셀을 누른 것은 행 클릭으로 가지 않는다.
2018
2025
  - **펼침·접힘 모션은 컴포넌트가 넣는다.** 하위 행이 부모 아래에서 서랍처럼 펼쳐지고 접히며, 아래 행들도 함께 내려가고 올라온다. 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)에 애니메이션·`transform`·`clip-path` 를 직접 주지 않는다 — 컴포넌트가 행의 위치를 움직이는 것과 겹친다. `onExpandedChange` 는 모션을 기다리지 않고 누른 즉시 온다.
2019
2026
  - **트리 컬럼에 링크·버튼을 넣어도 된다.** 셀 안의 링크·버튼·입력을 누르면 그 동작만 일어나고 펼침은 일어나지 않는다 — `stopPropagation` 을 직접 넣지 않는다. 상품명을 상세 링크로 거는 것처럼 쓴다.
@@ -2029,6 +2036,20 @@ const [from, setFrom] = useState<string | null>(null);
2029
2036
  ❌ <STable rows={flatten(categories)} columns={[{ …, render: row => `${' '.repeat(row.depth)}${row.name}` }]} /> {/* 계층을 공백으로 흉내 내지 않는다 */}
2030
2037
  ```
2031
2038
 
2039
+ `STable` 의 행 펼침(`renderExpansion`)은 행마다 **그 건의 상세 영역** 하나를 연다. 계층이 아니므로 `STreeTable` 을 쓰지 않는다.
2040
+
2041
+ - **상세 영역은 행이 아니다.** 선택·체크박스·잠금 표시·행 클릭·hover 와 무관하고 체크박스 열까지 덮는 표 전체 폭 한 칸이다. 상세를 가짜 하위 행으로 만들어 `isRowSelectable` 로 잠그지 않는다.
2042
+ - **펼침 버튼은 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `expansionColumn` 으로 옮긴다. 그 셀 전체가 펼침 영역이며 행 클릭(`onRowClick`)으로 가지 않는다. 펼침 버튼만 담는 좁은 컬럼을 따로 세우지 않고, 그 컬럼에는 `renderCell` 을 쓰지 않는다.
2043
+ - **여러 행을 함께 펼칠 수 있다.** 행 클릭으로도 펴야 하면 `expandedKeys`·`onExpandedChange` 로 들고 `onRowClick` 에서 키를 넣고 뺀다.
2044
+ - **펼침·접힘 모션은 컴포넌트가 넣는다.** `STreeTable` 하위 행과 같은 서랍 움직임이고 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)이나 상세 내용에 펼침 애니메이션·`transform` 을 직접 주지 않는다.
2045
+ - **상세의 여백은 컴포넌트가 넣는다.** 셀과 같은 좌우·위아래 여백이 이미 들어가 있으므로 내용에 바깥 패딩을 직접 주지 않는다.
2046
+ - `renderCell` 의 `rowSpan` 세로 병합과 함께 쓰지 않는다 — 펼친 상세가 병합 사이에 끼어든다.
2047
+
2048
+ ```tsx
2049
+ ✅ <STable<Report> columns={columns} rows={reports} renderExpansion={report => <ReportDetail report={report} />} />
2050
+ ❌ <STreeTable rows={reports.map(r => ({ ...r, children: [{ id: `${r.id}:detail`, detail: r }] }))} … /> {/* 상세를 가짜 하위 행으로 만들지 않는다 */}
2051
+ ```
2052
+
2032
2053
  `SExpansionList` 는 depth 별 **단일 확장**을 관리하는 wrapper 다. 직접 `useState` 로 "열린 항목 하나"를 들고 있지 않는다. 항목 사이 구분선은 여기서도 리스트가 알아서 그린다 — 따로 줄 prop 이 없다 (§3-7-6).
2033
2054
 
2034
2055
  ##### 펼침 ≠ 선택
package/dist/styles.css CHANGED
@@ -1842,6 +1842,9 @@
1842
1842
  .grid-cols-\[240px_auto_1fr\] {
1843
1843
  grid-template-columns: 240px auto 1fr;
1844
1844
  }
1845
+ .grid-cols-\[auto_1fr\] {
1846
+ grid-template-columns: auto 1fr;
1847
+ }
1845
1848
  .grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)\] {
1846
1849
  grid-template-columns: minmax(0,1fr) minmax(0,1fr);
1847
1850
  }
@@ -2345,6 +2348,9 @@
2345
2348
  .gap-x-sd-12 {
2346
2349
  column-gap: var(--spacing-sd-12);
2347
2350
  }
2351
+ .gap-x-sd-16 {
2352
+ column-gap: var(--spacing-sd-16);
2353
+ }
2348
2354
  .gap-y-\[2px\] {
2349
2355
  row-gap: 2px;
2350
2356
  }
@@ -2354,6 +2360,9 @@
2354
2360
  .gap-y-\[var\(--cmp-datepicker-calendar-grid-rowGap\)\] {
2355
2361
  row-gap: var(--cmp-datepicker-calendar-grid-rowGap);
2356
2362
  }
2363
+ .gap-y-sd-4 {
2364
+ row-gap: var(--spacing-sd-4);
2365
+ }
2357
2366
  .self-center {
2358
2367
  align-self: center;
2359
2368
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sellmate-design-system-react",
3
- "version": "9.0.0-beta.95",
3
+ "version": "9.0.0-beta.98",
4
4
  "description": "Sellmate Design System — React (TypeScript + Tailwind v4) port",
5
5
  "license": "UNLICENSED",
6
6
  "keywords": [