sellmate-design-system-react 9.0.0-beta.61 → 9.0.0-beta.63
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.
- package/AGENTS.md +510 -136
- package/dist/components/SBarChart/README.md +15 -0
- package/dist/components/SBarChart/SBarChart.d.ts +15 -1
- package/dist/components/SBarChart/index.d.ts +1 -1
- package/dist/components/SCallout/README.md +1 -0
- package/dist/components/SCallout/SCallout.d.ts +6 -1
- package/dist/components/SDonutChart/README.md +2 -4
- package/dist/components/SDonutChart/SDonutChart.d.ts +9 -10
- package/dist/components/SGhostButton/README.md +2 -0
- package/dist/components/SLineChart/README.md +15 -0
- package/dist/components/SLineChart/SLineChart.d.ts +15 -1
- package/dist/components/SLineChart/index.d.ts +1 -1
- package/dist/components/SLineChart/lineChart.config.d.ts +8 -5
- package/dist/components/SMeatballButton/README.md +67 -0
- package/dist/components/SMeatballButton/SMeatballButton.d.ts +36 -0
- package/dist/components/SMeatballButton/index.d.ts +1 -0
- package/dist/components/SPage/README.md +19 -0
- package/dist/components/SPage/SPage.d.ts +11 -2
- package/dist/components/SPage/index.d.ts +1 -1
- package/dist/components/SPage/page.config.d.ts +8 -0
- package/dist/components/SPieChart/README.md +1 -0
- package/dist/components/SPieChart/SPieChart.d.ts +6 -1
- package/dist/docs/RecipeGallery.d.ts +13 -0
- package/dist/index.cjs +338 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +336 -154
- package/dist/index.js.map +1 -1
- package/dist/lib/chart/radial.d.ts +22 -11
- package/dist/lib/chart/tooltip.d.ts +18 -1
- package/dist/lib/dropdown-menu.d.ts +19 -0
- package/dist/llms-full.txt +628 -140
- package/dist/llms.txt +512 -138
- package/dist/recipes/AppShell.d.ts +21 -0
- package/dist/recipes/ExcelFilePopupPage.d.ts +5 -0
- package/dist/recipes/LoginPage.d.ts +6 -0
- package/dist/recipes/ProductCreatePage.d.ts +1 -0
- package/dist/recipes/ProductCreateSectionsPage.d.ts +5 -0
- package/dist/recipes/ProductDetailInlineEditPage.d.ts +9 -0
- package/dist/recipes/ProductDetailPage.d.ts +1 -0
- package/dist/recipes/ProductListChipFilterPage.d.ts +1 -0
- package/dist/recipes/ProductListPage.d.ts +1 -0
- package/dist/recipes/TransferOrderPopupPage.d.ts +2 -0
- package/dist/recipes/api.d.ts +91 -0
- package/dist/recipes/recipe-story.d.ts +33 -0
- package/dist/recipes/router.d.ts +9 -0
- package/dist/styles.css +21 -3
- package/package.json +3 -1
package/dist/llms-full.txt
CHANGED
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
|
|
48
48
|
| 분류 | 컴포넌트 |
|
|
49
49
|
| --- | --- |
|
|
50
|
-
| **버튼·링크** | `SButton` `SGhostButton` `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
|
|
50
|
+
| **버튼·링크** | `SButton` `SGhostButton` `SMeatballButton`(⋯ 로 '더 보기' 메뉴를 여는 아이콘 버튼) `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
|
|
51
51
|
| **입력 (폼)** | `SForm` `SField` `SInput` `SChatInput`(대화 입력창) `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
|
|
52
52
|
| **날짜·시간** | `SCalendar` `SCalendarBoard`(한 달치 일정 판) `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
|
|
53
53
|
| **표·목록** | `SChatMessage`(대화의 메시지 한 건) `SChatAttachedFile`(입력창 위, 아직 보내지 않은 첨부 한 칸) `SChatFile`(대화 흐름에 선, 보낸 파일 한 칸) `SChatSystemMessage`(대화 흐름 가운데 서는 시스템 안내) `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
|
|
54
54
|
| **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SLauncherListBox`(런처 버튼을 눌러 뜨는 서비스 목록) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SLoginCard`(통합 계정 로그인 화면의 카드) `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
|
|
55
55
|
| **내비게이션** | `STabs` `SPagination` `SStepper` |
|
|
56
|
-
| **차트** | `SBarChart`(막대 그래프 — 항목끼리 크기를 견준다. `stacked` 로 항목 안의 구성까지) `SLineChart`(꺾은선 그래프 — 순서가 있는 항목의 추이를 본다. `area` 로 크기까지, `stacked` 로 구성까지) `SPieChart`(원그래프 — 한 시점의 전체를 몇 개의 몫으로 나눈다) `SDonutChart`(도넛 그래프 —
|
|
56
|
+
| **차트** | `SBarChart`(막대 그래프 — 항목끼리 크기를 견준다. `stacked` 로 항목 안의 구성까지) `SLineChart`(꺾은선 그래프 — 순서가 있는 항목의 추이를 본다. `area` 로 크기까지, `stacked` 로 구성까지) `SPieChart`(원그래프 — 한 시점의 전체를 몇 개의 몫으로 나눈다) `SDonutChart`(도넛 그래프 — 가운데가 빈 원그래프. 구멍에 글자를 두지 않는다) |
|
|
57
57
|
| **표시·상태** | `STag` `SBadge` `SIcon` `SLogo`(브랜드 로고를 아이콘처럼 — size 는 높이다) `SImage` `SCallout` `SGuide` |
|
|
58
58
|
| **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
|
|
59
59
|
| **오버레이** | `STooltip` `SPopover` `SPopup` `SDrawer` `SPortal` |
|
|
@@ -80,7 +80,7 @@ AI 에이전트는 코드를 생성하기 전에 이 목록을 반드시 지킨
|
|
|
80
80
|
|
|
81
81
|
| 금지 | 대신 사용 |
|
|
82
82
|
| --- | --- |
|
|
83
|
-
| `<button>` | `SButton`, `SGhostButton`, `SDropdownButton`, `STextLink` |
|
|
83
|
+
| `<button>` | `SButton`, `SGhostButton`, `SMeatballButton`, `SDropdownButton`, `STextLink` |
|
|
84
84
|
| `<input type="text/password/...">` | `SInput` |
|
|
85
85
|
| `<input type="search">` | `SSearchInput` |
|
|
86
86
|
| `<input type="number">` | `SNumberInput` |
|
|
@@ -176,7 +176,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
176
176
|
| --- | --- | --- |
|
|
177
177
|
| **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SGnbSystem`(GNB 맨 아래 판 · 전폭 상단바 오른쪽 끝) `SPage` `SPageHeader`(`SLayout` 안에서 `SPage` 앞에 둔다) |
|
|
178
178
|
| **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SChatMessage` `SChatSystemMessage` `SChatInput` `SLoginCard`(유일하게 `SPage` 밖에 선다 — 로그인 화면 자체가 자기 자리다, §4-6) `SForm` `SSplitter` `SScrollArea` `SCalendarBoard` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `SBarChart` `SLineChart` `SPieChart` `SDonutChart` `STabs` `SStepper` `SPagination` `SDivider` |
|
|
179
|
-
| **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SDropdownButton` `SSystemActionButton` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SChatAttachedFile` `SChatFile` `SImage` `SLinearProgress` `SCircleProgress` |
|
|
179
|
+
| **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `SButton` `SGhostButton` `SMeatballButton` `SDropdownButton` `SSystemActionButton` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SSwitch` `SToggle` `SChipInput` `SBarcodeInput` `SFilePicker` `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` `SCalendar` `SListItem` `SExpansionItem` `SDraggableItem` `SChatAttachedFile` `SChatFile` `SImage` `SLinearProgress` `SCircleProgress` |
|
|
180
180
|
| **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `SLogo` `STextLink` `SChip` |
|
|
181
181
|
| **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SAccountListBox`(계정 행에 붙어 뜬다 — 직접 띄우지 않는다) `SLauncherListBox`(런처 버튼에 붙어 뜬다 — 직접 띄우지 않는다) `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
|
|
182
182
|
|
|
@@ -316,6 +316,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
316
316
|
| 상황 | 값 |
|
|
317
317
|
| --- | --- |
|
|
318
318
|
| **페이지 콘텐츠 패딩** | **`SPage` 가 `--cmp-pageBody-padding-default` 로 이미 넣는다.** 직접 주지 않는다 (덮어쓰면 토큰이 바뀌어도 안 따라간다). `SPage` 밖에서 같은 패딩이 필요하면 `p-sd-16` |
|
|
319
|
+
| **가장자리까지 차는 블록이 맨 위에 서는 페이지** (`SChipFilter` 바 등) | `SPage` 에 `padding="none"` 으로 끄고, **그 아래 블록이 `p-[var(--cmp-pageBody-padding-default)]` 로 같은 토큰의 여백을 잡는다.** `className="p-0"` 이나 `p-sd-16` 으로 적지 않는다 (§4-2 칩 필터) |
|
|
319
320
|
| **섹션 ↔ 섹션**, **블록 ↔ 블록** (헤더·필터·본문 사이) | **`gap-sd-12`** |
|
|
320
321
|
| **툴바 ↔ 그 표** (`STableBar` ↔ `STable`) | **간격 없음 — 붙인다.** 둘은 한 블록이다 (§2-0) |
|
|
321
322
|
| 요소 ↔ 요소 | **기본 `gap-sd-8`** (`--sys-space-stack-gap-element-normal`) · 타이트 `gap-sd-4`(`-tight`) · 여유 `gap-sd-12`(`-relaxed`) / `gap-sd-16`(`-wide`) |
|
|
@@ -333,7 +334,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
333
334
|
| `SCheckbox` | **`gap-sd-24`** | `gap-sd-8` |
|
|
334
335
|
| `SRadio` | **`gap-sd-24`** | `gap-sd-8` |
|
|
335
336
|
| `STextLink` | **`gap-sd-16`**(sm) / **`gap-sd-24`**(md·lg) | `gap-sd-4` |
|
|
336
|
-
| `SGhostButton` | `gap-sd-4` | `gap-sd-4` |
|
|
337
|
+
| `SGhostButton` · `SMeatballButton` | `gap-sd-4` | `gap-sd-4` |
|
|
337
338
|
| `SButton` | `gap-sd-8` (xs·sm·md) / **`gap-sd-12`**(lg) | 〃 |
|
|
338
339
|
| `STag` | `gap-sd-8` | `gap-sd-8` |
|
|
339
340
|
| `SToggle` | `gap-sd-8` | `gap-sd-8` |
|
|
@@ -415,7 +416,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
415
416
|
```
|
|
416
417
|
|
|
417
418
|
- **`bordered={false}`** 로 표의 테두리를 끈다. 카드가 이미 그린다.
|
|
418
|
-
- **`radius="useTop"`** 으로 위쪽 모서리를 죽인다. 아래쪽 라운드는 카드가 처리한다.
|
|
419
|
+
- **`radius="useTop"`** 으로 위쪽 모서리를 죽인다. 아래쪽 라운드는 카드가 처리한다. **표 아래에 카드 푸터(`button`·`footerLeft`)가 오면 `radius="full"`** — 아래 모서리도 카드가 아니라 푸터의 상단 구분선이 이어받으므로 네 모서리를 모두 죽인다.
|
|
419
420
|
- `STable` 은 **페이지네이션 바의 테두리와 `-mt-px` 겹침도 함께 꺼진다** — 본문 테두리가 없으면 겹칠 대상이 없어, 그대로 두면 페이지네이션만 테두리를 갖고 1px 어긋난다.
|
|
420
421
|
|
|
421
422
|
#### 본문 바탕 눌러앉히기 (선택)
|
|
@@ -579,7 +580,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
579
580
|
| 여러 항목의 추이를 **합계와 함께** 보여준다 (채널별 매출이 쌓여 전체가 되는 흐름) | `SLineChart stacked area` | 합계가 뜻을 갖는 값일 때만 쌓는다. 음수가 섞이거나 더해서 뜻이 없는 값(비율·평균)은 쌓지 말고 겹쳐 그린다 |
|
|
580
581
|
| 항목의 크기와 **그 안의 구성**을 함께 보여준다 (주문 상태별 내역, 유입 경로별 몫) | `SBarChart stacked` | 계열끼리 견주는 것이 목적이면 `stacked` 없이 나란히 세운다. 더해서 뜻이 없는 값(비율·평균)은 쌓지 않는다 — 합계 라벨이 거짓말이 된다 |
|
|
581
582
|
| 한 시점의 **전체를 몇 개의 몫으로** 나눠 보여준다 (채널별 매출 비중, 회원 등급 구성) | `SPieChart` | 조각이 대여섯 개를 넘거나 몫끼리 크기를 견주는 것이 목적이면 `SBarChart` — 사람은 각도보다 길이를 정확히 견준다. 순서가 있는 추이(월별)는 몫이 아니라 흐름이라 `SLineChart`. 0·음수는 그려지지 않으므로 증감은 원으로 나타내지 않는다. 조각은 큰 순으로 넘긴다. 폭은 부모를 따르고 그림은 그 폭의 정사각형이다 — 폭이 필요하면 `className` 으로 폭만 주고 높이는 주지 않는다 (`SDonutChart` 도 같다) |
|
|
582
|
-
|
|
|
583
|
+
| 한 시점의 전체를 몫으로 나누되 **가운데가 빈 고리 모양**으로 보여준다 (시안이 도넛일 때) | `SDonutChart` | 쓰임·데이터 규칙은 `SPieChart` 와 같고 모양만 다르다 — 시안이 정한 쪽을 쓴다. **구멍에 합계·라벨을 겹쳐 올리지 않는다.** 합계 같은 값이 필요하면 차트 바깥(카드 제목·요약 값)에 적는다. 여러 기간에 걸친 구성 변화는 원을 여러 개 늘어놓지 말고 `SBarChart stacked` |
|
|
583
584
|
| 표 위에 건수 요약과 액션을 얹는다 | `STableBar` | 표의 이름도 여기 `title` 슬롯에 넣는다 — 제목만으로 블록을 따로 세우지 않는다. §4-2 |
|
|
584
585
|
| 상태·분류를 라벨로 찍는다 | `STag` | §3-1 |
|
|
585
586
|
| 색 점만으로 상태를 찍는다 | `SBadge` | §3-1 |
|
|
@@ -594,6 +595,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
594
595
|
| --- | --- | --- |
|
|
595
596
|
| 라벨이 있는 일반 액션을 준다 | `SButton` | §3-5 |
|
|
596
597
|
| 아이콘 하나로 뜻이 통하는 부가 조작을 준다 | `SGhostButton` | §3-5-5 |
|
|
598
|
+
| 자리가 좁아 부가 조작 여러 개를 ⋯ 뒤 메뉴로 접어 둔다 | `SMeatballButton` | §3-5-5 |
|
|
597
599
|
| 한 버튼에 여러 선택지를 매단다 | `SDropdownButton` | §3-5-4 |
|
|
598
600
|
| GNB system 패널에서 한 줄짜리 행 액션을 준다 | `SSystemActionButton` | §3-5-7 |
|
|
599
601
|
|
|
@@ -693,7 +695,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
693
695
|
| --- | --- |
|
|
694
696
|
| `default` | 흰 면 위 (`SPage background="frame"`) — 옅은 회색 면이라 페이지와 구분된다 |
|
|
695
697
|
| `white` | 회색 면 위 (`background="neutral"`·`"screen"`) — 그 위에서는 `default` 가 배경에 묻힌다 |
|
|
696
|
-
| `danger` | 경고. 좌측에 "주의사항" 타이틀이 붙으므로 단순 안내에는 쓰지
|
|
698
|
+
| `danger` | 경고. 좌측에 "주의사항" 타이틀이 붙으므로 단순 안내에는 쓰지 않는다. 주변에 이미 경고 맥락(제목 등)이 있어 타이틀이 중복되면 `showTitle={false}` 로 타이틀만 뺀다 — 경고 면은 유지된다 |
|
|
697
699
|
|
|
698
700
|
### 3-3. 플로팅/오버레이류
|
|
699
701
|
|
|
@@ -1216,6 +1218,7 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1216
1218
|
| 일반 액션 | `SButton` (`color`: `primary` / `secondary` / `neutral` / `danger`, `outline`, `size`: xs~md) |
|
|
1217
1219
|
| **삭제 등 파괴적 액션** | **`SButton color="danger"`** 또는 **`color="danger" outline`** (아래 3-5-3) |
|
|
1218
1220
|
| **아이콘 하나로 뜻이 통하는 부가 조작** | **`SGhostButton`** — 라벨이 없는 아이콘 전용 버튼 (아래 3-5-5) |
|
|
1221
|
+
| **부가 조작 여러 개를 ⋯ 메뉴로 접기** | **`SMeatballButton`** — ⋯ 아이콘 버튼 + `SDropdownButton` 과 같은 메뉴 (아래 3-5-5) |
|
|
1219
1222
|
| 본문 속 이동 링크 | `STextLink` |
|
|
1220
1223
|
| 메인 액션 + 부가 메뉴 | `SDropdownButton` |
|
|
1221
1224
|
| **GNB system 패널의 한 줄 액션** | **`SSystemActionButton`** — 행 전체가 버튼인 표면형 (아래 3-5-7) |
|
|
@@ -1329,6 +1332,24 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1329
1332
|
❌ <SGhostButton icon="download" ariaLabel="엑셀 다운로드" /> // 목록의 주 액션 → SButton
|
|
1330
1333
|
```
|
|
1331
1334
|
|
|
1335
|
+
##### `SMeatballButton` — ⋯ 뒤에 메뉴를 접어 둘 때
|
|
1336
|
+
|
|
1337
|
+
자리가 좁아 부가 조작을 하나하나 아이콘으로 늘어놓을 수 없을 때, **⋯ 하나로 접고 누르면 메뉴를 띄운다.** 버튼의 크기·색·hover·툴팁은 `SGhostButton` 과, 메뉴 모양은 `SDropdownButton` 과 같다.
|
|
1338
|
+
|
|
1339
|
+
- **메뉴는 컴포넌트가 띄운다.** `items` 를 넘기고 선택은 `onItemClick(value)` 로 받는다. 메뉴를 따로 만들어 붙이지 않고, 열림 상태(`aria-expanded`)도 직접 넣지 않는다.
|
|
1340
|
+
- **`SGhostButton icon="etc"` 로 직접 만들지 않는다.** '더 보기' 메뉴는 항상 `SMeatballButton` 이다.
|
|
1341
|
+
- **`intent` 는 `default` / `inverse` 뿐이다.** ⋯ 는 조작이 아니라 메뉴를 여는 것이므로 `action`·`danger` 색을 받지 않는다. 어두운 배경 위에서만 `inverse`.
|
|
1342
|
+
- 툴팁·`ariaLabel` 은 기본 문구가 들어 있어 **주지 않아도 된다.** 한 화면에 ⋯ 가 여러 개라 무엇의 메뉴인지 구분해야 할 때만 `tooltipText` 를 바꾼다(`ariaLabel` 이 따라간다).
|
|
1343
|
+
- 메인 액션에 변형 동작을 곁들이는 것이면 `SMeatballButton` 이 아니라 `SDropdownButton split`(§3-5-4)이다.
|
|
1344
|
+
|
|
1345
|
+
```tsx
|
|
1346
|
+
✅ <SMeatballButton size="xs" // 행 끝 부가 조작 묶음
|
|
1347
|
+
items={[{ value: 'copy', label: '복제', icon: 'copy' }, { value: 'remove', label: '삭제', icon: 'remove' }]}
|
|
1348
|
+
onItemClick={v => handleRowAction(row, v)} />
|
|
1349
|
+
✅ <SMeatballButton intent="inverse" items={items} onItemClick={onAction} /> // 어두운 배경 위
|
|
1350
|
+
❌ <SGhostButton icon="etc" ariaLabel="더 보기" onClick={openMyMenu} /> // 직접 조립 → SMeatballButton
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1332
1353
|
#### 3-5-6. 그 밖
|
|
1333
1354
|
|
|
1334
1355
|
- 라벨은 **동사(+목적어)** — `저장` · `적치 지시` · `송장 재출력`
|
|
@@ -1340,6 +1361,7 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1340
1361
|
- **왼쪽 끝**: 취소·닫기·목록 등 되돌리는 액션
|
|
1341
1362
|
- **오른쪽 끝**: 저장·등록·수정·삭제 등 실행 액션
|
|
1342
1363
|
- 체크박스·안내 문구 등 **부가 요소는 오른쪽 그룹 안, 실행 버튼 바로 왼쪽**에 둔다
|
|
1364
|
+
- **되돌리는 액션이 없어도 양끝 분리는 그대로다** — 오른쪽에 모으지 않는다. 그때는 **위계가 낮은 실행 액션이 왼쪽 끝, 가장 강한 실행 액션이 오른쪽 끝**이다. 뒤로가기를 헤더의 `onBack` 이 맡은 상세 페이지(§4-4)의 `삭제`(`danger outline`) · `수정`(`primary` 채움)이 이 경우다.
|
|
1343
1365
|
|
|
1344
1366
|
```tsx
|
|
1345
1367
|
<div className="flex items-center justify-between">
|
|
@@ -1940,6 +1962,8 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
1940
1962
|
>
|
|
1941
1963
|
> **아래 레시피는 §2-0 조합 문법으로 유도된 결과다.** 여기 없는 화면(대시보드·설정·마법사 등)을 만들 때는 임의로 짜지 말고 §2-0 의 층 구조·포함 규칙·블록 순서로 직접 유도한다.
|
|
1942
1964
|
>
|
|
1965
|
+
> **레시피 코드의 `./AppShell` 은 §4-1 의 셸, `./api`·`./router` 는 앱의 데이터 계층·라우터 자리다.** 각 레시피는 Storybook `Recipes/…` 에서 실제로 띄워 볼 수 있고, 아래에 적힌 분기(칩 필터·섹션 나눔 등)의 변형 코드도 거기 있다.
|
|
1966
|
+
>
|
|
1943
1967
|
> **핵심 원칙 — 표 형태의 정보는 `SKeyValueTable` 로 만든다.** 필터·등록/수정 폼·상세 정보가 모두 여기 해당한다.
|
|
1944
1968
|
> `SField` 컨트롤을 `div` 로 직접 나열해 폼을 만들지 않는다.
|
|
1945
1969
|
> (필터만 예외가 하나 있다 — 조건 후보가 많아 골라 붙이게 해야 하면 `SChipFilter` 다. §3-7-11)
|
|
@@ -1947,7 +1971,11 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
1947
1971
|
### 4-1. 앱 셸 (모든 페이지 공통)
|
|
1948
1972
|
|
|
1949
1973
|
```tsx
|
|
1950
|
-
import
|
|
1974
|
+
import type { ReactNode } from 'react';
|
|
1975
|
+
import {
|
|
1976
|
+
SLayout, SGnb, SPage, SPageHeader,
|
|
1977
|
+
type SGnbMenuItem, type SPageHeaderProps, type SPageContentHeight, type SPageContentWidth, type SPagePadding,
|
|
1978
|
+
} from 'sellmate-design-system-react';
|
|
1951
1979
|
|
|
1952
1980
|
const MENU: SGnbMenuItem[] = [
|
|
1953
1981
|
{ label: '주문', value: 'orders', icon: 'bill' },
|
|
@@ -1959,12 +1987,15 @@ export default function AppShell({
|
|
|
1959
1987
|
header,
|
|
1960
1988
|
scrollEndSpacing,
|
|
1961
1989
|
contentHeight,
|
|
1990
|
+
contentWidth,
|
|
1991
|
+
padding,
|
|
1962
1992
|
}: {
|
|
1963
|
-
children:
|
|
1993
|
+
children: ReactNode;
|
|
1964
1994
|
header?: SPageHeaderProps;
|
|
1965
1995
|
scrollEndSpacing?: boolean;
|
|
1966
1996
|
contentHeight?: SPageContentHeight;
|
|
1967
1997
|
contentWidth?: SPageContentWidth;
|
|
1998
|
+
padding?: SPagePadding;
|
|
1968
1999
|
}) {
|
|
1969
2000
|
return (
|
|
1970
2001
|
<SLayout type="box" header="fix">
|
|
@@ -1973,7 +2004,8 @@ export default function AppShell({
|
|
|
1973
2004
|
{/* 페이지 제목은 SPage 앞에 형제로 둔다 — SLayout 이 헤더부터 뒤를 한 열로 묶는다.
|
|
1974
2005
|
header 는 페이지마다 달라 AppShell 이 그대로 받아 넘긴다 — 여기서 만들지 않는다 */}
|
|
1975
2006
|
{header && <SPageHeader {...header} />}
|
|
1976
|
-
{/* 콘텐츠 패딩은 SPage 가 토큰으로 넣는다 — p-sd-* 로 덮어쓰지
|
|
2007
|
+
{/* 콘텐츠 패딩은 SPage 가 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다.
|
|
2008
|
+
가장자리까지 차는 블록이 맨 위에 서는 페이지만 padding="none" 으로 끈다 (§4-2 칩 필터) */}
|
|
1977
2009
|
{/* 높이 모드는 페이지가 정한다 — 대부분 contentHeight="fill" 이다 (§2-2) */}
|
|
1978
2010
|
{/* 스크롤 끝 여백도 SPage 가 넣는다. 페이지가 실제로 스크롤되는 화면에서만 켠다 */}
|
|
1979
2011
|
{/* 가로 폭 모드도 페이지가 정한다 — 넓은 창에서 본문을 늘릴지 최소 폭으로 세울지 */}
|
|
@@ -1982,6 +2014,7 @@ export default function AppShell({
|
|
|
1982
2014
|
scrollEndSpacing={scrollEndSpacing}
|
|
1983
2015
|
contentHeight={contentHeight}
|
|
1984
2016
|
contentWidth={contentWidth}
|
|
2017
|
+
padding={padding}
|
|
1985
2018
|
>
|
|
1986
2019
|
{children}
|
|
1987
2020
|
</SPage>
|
|
@@ -2187,7 +2220,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
|
|
|
2187
2220
|
|
|
2188
2221
|
**뒤의 둘은 한 블록이라 붙여 쓴다** — 툴바는 자기 아래 표의 건수와 액션을 말하므로 사이를 벌리지 않는다. 묶는 방법과 맞닿는 변 처리는 §2-0 에 있다.
|
|
2189
2222
|
|
|
2190
|
-
필터 자리는 `SChipFilter` 로 바꿔 놓을 수 있다 — 조건 후보가 많아 쓸 것만 골라 붙이게 하는 화면이면 그쪽이다 (§3-7-11). 나머지 골격은
|
|
2223
|
+
필터 자리는 `SChipFilter` 로 바꿔 놓을 수 있다 — 조건 후보가 많아 쓸 것만 골라 붙이게 하는 화면이면 그쪽이다 (§3-7-11). 나머지 골격은 같지만 여백이 다르다 — **칩 필터 바는 페이지 가장자리까지 찬다.** `SPage` 에 `padding="none"` 을 주고(§4-1 의 셸이 넘긴다), 바에 `className="border-b"` 로 아래를 긋고(흰 바탕이라 흰 페이지 위에서 경계가 없다 — 선 색은 바가 토큰으로 들고 있어 두께만 준다), 그 아래 툴바+표 블록이 `p-[var(--cmp-pageBody-padding-default)]` 로 같은 토큰의 여백을 잡는다. 코드는 Storybook `Recipes/목록 페이지` 의 "칩 필터" 에 있다.
|
|
2191
2224
|
|
|
2192
2225
|
액션 버튼의 위치가 핵심이다:
|
|
2193
2226
|
|
|
@@ -2199,44 +2232,120 @@ import { SModalOutlet } from 'sellmate-design-system-react';
|
|
|
2199
2232
|
- `fill` 에서는 페이지가 스크롤하지 않으므로 **`scrollEndSpacing` 은 무시된다** — 따로 끄지 않는다 (§2-2).
|
|
2200
2233
|
- **정렬 가능한 컬럼은 `sortable` 로 준다.** 정렬 상태(`sort`)는 이 페이지가 들고 `onSortChange` 로 받는다 — 조회 조건이라 URL 에 실려야 한다 (§3-4).
|
|
2201
2234
|
|
|
2235
|
+
<!-- recipe:src/recipes/ProductListPage.tsx — 아래 코드 블록은 이 파일에서 생성된다(scripts/gen-recipes.mjs). 여기를 고치지 말고 파일을 고친다 -->
|
|
2202
2236
|
```tsx
|
|
2237
|
+
import { useState } from 'react';
|
|
2203
2238
|
import {
|
|
2204
|
-
SButton,
|
|
2205
|
-
|
|
2239
|
+
SButton,
|
|
2240
|
+
SKeyValueTable,
|
|
2241
|
+
STable,
|
|
2242
|
+
STableBar,
|
|
2243
|
+
STag,
|
|
2244
|
+
type SDateRangeValue,
|
|
2245
|
+
type SKeyValueField,
|
|
2246
|
+
type SRow,
|
|
2247
|
+
type STableColumn,
|
|
2248
|
+
type STableSort,
|
|
2206
2249
|
} from 'sellmate-design-system-react';
|
|
2250
|
+
import AppShell from './AppShell';
|
|
2251
|
+
import { STATUS_OPTIONS, removeProducts, useProductList, type ProductQuery } from './api';
|
|
2252
|
+
import { useNavigate } from './router';
|
|
2207
2253
|
|
|
2208
2254
|
/** 필터도 표다 — SKeyValueTable 로 만든다 (서비스 전용 전역 필터가 따로 있는 경우 제외) */
|
|
2209
2255
|
const filterFields: SKeyValueField[][] = [
|
|
2210
2256
|
[
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2257
|
+
// 조회 조건으로 쓰는 셀렉트·날짜 피커는 clearable — 걸어 둔 조건을 풀 수 있어야 한다 (§3-7-4)
|
|
2258
|
+
{
|
|
2259
|
+
name: 'status',
|
|
2260
|
+
label: '상태',
|
|
2261
|
+
type: 'select',
|
|
2262
|
+
options: { options: STATUS_OPTIONS, valueAsPrimitive: true, clearable: true },
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
name: 'keyword',
|
|
2266
|
+
label: '검색어',
|
|
2267
|
+
type: 'input',
|
|
2268
|
+
options: { placeholder: '상품명 / 상품코드' },
|
|
2269
|
+
},
|
|
2215
2270
|
],
|
|
2216
2271
|
[
|
|
2217
|
-
{
|
|
2272
|
+
{
|
|
2273
|
+
name: 'period',
|
|
2274
|
+
label: '등록일',
|
|
2275
|
+
type: 'date-range-picker',
|
|
2276
|
+
tdColSpan: 3,
|
|
2277
|
+
options: { clearable: true },
|
|
2278
|
+
},
|
|
2218
2279
|
],
|
|
2219
2280
|
];
|
|
2220
2281
|
|
|
2282
|
+
// 정렬·폭은 값의 성격에서 나온다 (§3-4). 모든 컬럼에 폭을 px 로 명시한다
|
|
2221
2283
|
const columns: STableColumn[] = [
|
|
2222
|
-
//
|
|
2223
|
-
{ name: 'id', label: 'ID', field: 'id', width:
|
|
2224
|
-
|
|
2225
|
-
{ name: '
|
|
2226
|
-
|
|
2227
|
-
{
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2284
|
+
// ID·코드는 크기를 비교하는 숫자가 아니라 식별자다 → center. right 는 양을 나타내는 값에만
|
|
2285
|
+
{ name: 'id', label: 'ID', field: 'id', width: 80, align: 'center' },
|
|
2286
|
+
// 자유 입력 텍스트 → 기준 폭을 주고, 표에 resizable 을 켜서 사용자가 늘릴 수 있게 한다
|
|
2287
|
+
{ name: 'name', label: '상품명', field: 'name', width: 240 },
|
|
2288
|
+
// 값의 크기를 비교하는 컬럼만 sortable — 정렬 상태는 페이지가 들고 있다
|
|
2289
|
+
{
|
|
2290
|
+
name: 'stock',
|
|
2291
|
+
label: '재고',
|
|
2292
|
+
field: 'stock',
|
|
2293
|
+
width: 100,
|
|
2294
|
+
align: 'right',
|
|
2295
|
+
sortable: true,
|
|
2296
|
+
format: (v: unknown) => `${Number(v).toLocaleString()}개`,
|
|
2297
|
+
},
|
|
2298
|
+
{
|
|
2299
|
+
name: 'price',
|
|
2300
|
+
label: '판매가',
|
|
2301
|
+
field: 'price',
|
|
2302
|
+
width: 120,
|
|
2303
|
+
align: 'right',
|
|
2304
|
+
sortable: true,
|
|
2305
|
+
format: (v: unknown) => `${Number(v).toLocaleString()}원`,
|
|
2306
|
+
},
|
|
2307
|
+
// 상태 컬럼은 STag size="sm" — SBadge 색 점을 기본으로 쓰지 않는다.
|
|
2308
|
+
// 컨트롤이 들어가는 컬럼은 contentType: 'control', resizable 표에서는 minWidth 도 함께 (§3-4)
|
|
2309
|
+
{
|
|
2310
|
+
name: 'status',
|
|
2311
|
+
label: '상태',
|
|
2312
|
+
field: 'status',
|
|
2313
|
+
width: 100,
|
|
2314
|
+
minWidth: 100,
|
|
2315
|
+
align: 'center',
|
|
2316
|
+
contentType: 'control',
|
|
2231
2317
|
render: (row: SRow) =>
|
|
2232
|
-
row.status === 'selling'
|
|
2233
|
-
|
|
2234
|
-
|
|
2318
|
+
row.status === 'selling' ? (
|
|
2319
|
+
<STag size="sm" color="green" label="판매중" />
|
|
2320
|
+
) : (
|
|
2321
|
+
<STag size="sm" color="grey" label="판매중지" />
|
|
2322
|
+
),
|
|
2323
|
+
},
|
|
2235
2324
|
];
|
|
2236
2325
|
|
|
2237
2326
|
export default function ProductListPage() {
|
|
2327
|
+
const navigate = useNavigate();
|
|
2328
|
+
// 편집 중인 필터 값과, 검색을 눌러 실제로 걸린 조회 조건은 따로 든다.
|
|
2329
|
+
// 조회 조건(필터·정렬·페이지)은 실제 앱에서는 URL 쿼리에 싣는다 — 새로고침·공유에 살아남아야 한다
|
|
2238
2330
|
const [filters, setFilters] = useState<Record<string, unknown>>({});
|
|
2331
|
+
const [query, setQuery] = useState<ProductQuery>({ page: 1, rowsPerPage: 20 });
|
|
2239
2332
|
const [selected, setSelected] = useState<SRow[]>([]);
|
|
2333
|
+
const { rows, total, lastPage, loading } = useProductList(query);
|
|
2334
|
+
|
|
2335
|
+
const search = () => {
|
|
2336
|
+
setQuery(q => ({
|
|
2337
|
+
...q,
|
|
2338
|
+
status: (filters.status as string | null) ?? undefined,
|
|
2339
|
+
keywords: filters.keyword ? [String(filters.keyword)] : undefined,
|
|
2340
|
+
period: (filters.period as SDateRangeValue) ?? undefined,
|
|
2341
|
+
page: 1,
|
|
2342
|
+
}));
|
|
2343
|
+
};
|
|
2344
|
+
|
|
2345
|
+
const removeSelected = async () => {
|
|
2346
|
+
await removeProducts(selected.map(row => Number(row.id)));
|
|
2347
|
+
setSelected([]);
|
|
2348
|
+
};
|
|
2240
2349
|
|
|
2241
2350
|
return (
|
|
2242
2351
|
<AppShell
|
|
@@ -2244,7 +2353,15 @@ export default function ProductListPage() {
|
|
|
2244
2353
|
variant: 'bar',
|
|
2245
2354
|
title: '상품 목록',
|
|
2246
2355
|
// 이 페이지의 주요 액션이 아니라 부가 액션 — slot 은 sm 버튼으로만 채운다
|
|
2247
|
-
slot:
|
|
2356
|
+
slot: (
|
|
2357
|
+
<SButton
|
|
2358
|
+
size="sm"
|
|
2359
|
+
color="neutral"
|
|
2360
|
+
outline
|
|
2361
|
+
label="이용 가이드"
|
|
2362
|
+
onClick={() => navigate('/guide/products')}
|
|
2363
|
+
/>
|
|
2364
|
+
),
|
|
2248
2365
|
}}
|
|
2249
2366
|
contentHeight="fill" // 표가 남은 높이를 채우고 페이지네이션이 하단에 고정된다
|
|
2250
2367
|
>
|
|
@@ -2258,10 +2375,11 @@ export default function ProductListPage() {
|
|
|
2258
2375
|
fieldWidth="fill"
|
|
2259
2376
|
search
|
|
2260
2377
|
onChange={({ values }) => setFilters(values)}
|
|
2261
|
-
onSearch={
|
|
2378
|
+
onSearch={search}
|
|
2262
2379
|
/>
|
|
2263
2380
|
|
|
2264
|
-
{/* 툴바+표 = 한 블록. 사이를 벌리지 않는다 (§2-0)
|
|
2381
|
+
{/* 툴바+표 = 한 블록. 사이를 벌리지 않는다 (§2-0).
|
|
2382
|
+
이 div 도 높이 사슬의 한 칸이라 min-h-0 flex-1 이 필요하다 */}
|
|
2265
2383
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
2266
2384
|
{/* 툴바 — 좌: 건수 + (구분선 자동) + 선택 액션 / 우: 주요 액션
|
|
2267
2385
|
맞닿는 가로선은 표가 그리므로 아래 테두리는 지운다 */}
|
|
@@ -2271,25 +2389,37 @@ export default function ProductListPage() {
|
|
|
2271
2389
|
selected={selected.length}
|
|
2272
2390
|
actions={
|
|
2273
2391
|
/* 선택 항목 단위 파괴 액션 → danger outline (§3-5-3) */
|
|
2274
|
-
<SButton
|
|
2275
|
-
|
|
2392
|
+
<SButton
|
|
2393
|
+
size="sm"
|
|
2394
|
+
color="danger"
|
|
2395
|
+
outline
|
|
2396
|
+
label="선택 삭제"
|
|
2397
|
+
disabled={!selected.length}
|
|
2398
|
+
onClick={removeSelected}
|
|
2399
|
+
/>
|
|
2276
2400
|
}
|
|
2277
2401
|
rightActions={
|
|
2278
2402
|
/* 이 페이지의 유일한 primary 채움 (§3-5-1) */
|
|
2279
|
-
<SButton size="sm" label="상품 등록" onClick={
|
|
2403
|
+
<SButton size="sm" label="상품 등록" onClick={() => navigate('/products/new')} />
|
|
2280
2404
|
}
|
|
2281
2405
|
/>
|
|
2282
2406
|
|
|
2283
|
-
{/* 남은 높이를 채우고 본문만 스크롤한다 — 페이지네이션 바는 표 안에서 하단
|
|
2407
|
+
{/* 남은 높이를 채우고 본문만 스크롤한다 — 페이지네이션 바는 표 안에서 하단 고정.
|
|
2408
|
+
dense 는 시작 밀도이자 밀도 토글의 스위치 — onDenseChange 값을 되돌려 넣지 않는다 (§4-2) */}
|
|
2284
2409
|
<STable
|
|
2285
2410
|
className="min-h-0 flex-1"
|
|
2286
2411
|
columns={columns}
|
|
2287
2412
|
rows={rows}
|
|
2288
2413
|
rowKey="id"
|
|
2414
|
+
dense
|
|
2415
|
+
resizable
|
|
2289
2416
|
selectable
|
|
2290
2417
|
selected={selected}
|
|
2291
2418
|
onSelectedChange={setSelected}
|
|
2292
|
-
|
|
2419
|
+
sort={query.sort}
|
|
2420
|
+
onSortChange={(sort: STableSort | null) => setQuery(q => ({ ...q, sort, page: 1 }))}
|
|
2421
|
+
pagination={{ page: query.page, rowsPerPage: query.rowsPerPage, lastPage }}
|
|
2422
|
+
onPageChange={page => setQuery(q => ({ ...q, page }))}
|
|
2293
2423
|
loading={loading}
|
|
2294
2424
|
/>
|
|
2295
2425
|
</div>
|
|
@@ -2298,6 +2428,7 @@ export default function ProductListPage() {
|
|
|
2298
2428
|
);
|
|
2299
2429
|
}
|
|
2300
2430
|
```
|
|
2431
|
+
<!-- /recipe -->
|
|
2301
2432
|
|
|
2302
2433
|
#### 한 화면에 더 많은 행을 — `dense` 와 밀도 토글
|
|
2303
2434
|
|
|
@@ -2307,7 +2438,7 @@ export default function ProductListPage() {
|
|
|
2307
2438
|
|
|
2308
2439
|
```tsx
|
|
2309
2440
|
// 좁게 시작하고, 사용자가 바꾸는 밀도는 표가 알아서 들고 간다
|
|
2310
|
-
<STable dense showRowsPerPageSelect pagination={{
|
|
2441
|
+
<STable dense showRowsPerPageSelect pagination={{ page, rowsPerPage, lastPage }} />;
|
|
2311
2442
|
|
|
2312
2443
|
// 사용자가 고른 밀도를 다음 방문에도 남기려는 화면만 받아서 저장한다.
|
|
2313
2444
|
// 저장한 값은 시작 밀도로만 돌려준다 — 되돌려 넣지 않는다
|
|
@@ -2317,7 +2448,7 @@ const [initialDense] = useState(() => loadPref('list.dense', true));
|
|
|
2317
2448
|
dense={initialDense}
|
|
2318
2449
|
onDenseChange={next => savePref('list.dense', next)}
|
|
2319
2450
|
showRowsPerPageSelect
|
|
2320
|
-
pagination={{
|
|
2451
|
+
pagination={{ page, rowsPerPage, lastPage }}
|
|
2321
2452
|
/>;
|
|
2322
2453
|
```
|
|
2323
2454
|
|
|
@@ -2382,61 +2513,113 @@ const [initialDense] = useState(() => loadPref('list.dense', true));
|
|
|
2382
2513
|
|
|
2383
2514
|
**병합으로 찬 자리는 배열 순서가 아니라 실제 열 위치로 판정한다.** 그러니 아래 행의 필드를 병합된 열에 맞추려고 자리 채우기용 빈 필드를 끼우지 않는다 — 앞 필드에 `tdColSpan` 이나 `hideTh` 가 있어도 남은 필드가 알아서 빈 열부터 놓인다.
|
|
2384
2515
|
|
|
2516
|
+
<!-- recipe:src/recipes/ProductCreatePage.tsx — 아래 코드 블록은 이 파일에서 생성된다(scripts/gen-recipes.mjs). 여기를 고치지 말고 파일을 고친다 -->
|
|
2385
2517
|
```tsx
|
|
2518
|
+
import { useState } from 'react';
|
|
2386
2519
|
import {
|
|
2387
|
-
|
|
2388
|
-
|
|
2520
|
+
SButton,
|
|
2521
|
+
SCheckbox,
|
|
2522
|
+
SForm,
|
|
2523
|
+
SKeyValueTable,
|
|
2524
|
+
type Rule,
|
|
2525
|
+
type SKeyValueField,
|
|
2389
2526
|
} from 'sellmate-design-system-react';
|
|
2527
|
+
import AppShell from './AppShell';
|
|
2528
|
+
import { CATEGORY_OPTIONS, saveProduct } from './api';
|
|
2529
|
+
import { useNavigate } from './router';
|
|
2390
2530
|
|
|
2391
|
-
const required =
|
|
2392
|
-
|
|
2531
|
+
const required =
|
|
2532
|
+
(msg: string): Rule =>
|
|
2533
|
+
v =>
|
|
2534
|
+
v != null && String(v).trim() !== '' ? true : msg;
|
|
2393
2535
|
|
|
2536
|
+
/** 필드를 div 로 나열하지 않는다 — SKeyValueTable 의 행으로 구성한다 */
|
|
2394
2537
|
const fields: SKeyValueField[][] = [
|
|
2395
2538
|
[
|
|
2396
|
-
{
|
|
2397
|
-
|
|
2539
|
+
{
|
|
2540
|
+
name: 'name',
|
|
2541
|
+
label: '상품명',
|
|
2542
|
+
required: true,
|
|
2543
|
+
type: 'input',
|
|
2544
|
+
options: { placeholder: '상품명 입력', rules: [required('상품명을 입력해 주세요.')] },
|
|
2545
|
+
},
|
|
2398
2546
|
{ name: 'code', label: '상품코드', type: 'input', options: { placeholder: '자동 생성' } },
|
|
2399
2547
|
],
|
|
2400
2548
|
[
|
|
2401
|
-
{
|
|
2402
|
-
|
|
2403
|
-
|
|
2549
|
+
{
|
|
2550
|
+
name: 'category',
|
|
2551
|
+
label: '카테고리',
|
|
2552
|
+
required: true,
|
|
2553
|
+
type: 'select',
|
|
2554
|
+
options: {
|
|
2555
|
+
options: CATEGORY_OPTIONS,
|
|
2556
|
+
valueAsPrimitive: true,
|
|
2557
|
+
rules: [required('카테고리를 선택해 주세요.')],
|
|
2558
|
+
},
|
|
2559
|
+
},
|
|
2404
2560
|
{ name: 'price', label: '판매가', type: 'number-input' },
|
|
2405
2561
|
],
|
|
2406
2562
|
[
|
|
2407
|
-
|
|
2408
|
-
|
|
2563
|
+
// 가장 긴 행(필드 2개 = 4칸)에 맞춰 마지막 필드가 남는 칸을 채운다
|
|
2564
|
+
{
|
|
2565
|
+
name: 'memo',
|
|
2566
|
+
label: '메모',
|
|
2567
|
+
type: 'textarea',
|
|
2568
|
+
tdColSpan: 3,
|
|
2569
|
+
helpText: ['내부 관리용 메모입니다.'],
|
|
2570
|
+
},
|
|
2409
2571
|
],
|
|
2410
2572
|
];
|
|
2411
2573
|
|
|
2412
2574
|
export default function ProductCreatePage() {
|
|
2413
|
-
const
|
|
2575
|
+
const navigate = useNavigate();
|
|
2414
2576
|
const [values, setValues] = useState<Record<string, unknown>>({});
|
|
2577
|
+
const [keepOpen, setKeepOpen] = useState(false);
|
|
2578
|
+
|
|
2579
|
+
// SForm 이 하위 컨트롤의 rules 를 모아 submit 때 일괄 검증한다 — 통과했을 때만 불린다
|
|
2580
|
+
const save = async () => {
|
|
2581
|
+
await saveProduct(values);
|
|
2582
|
+
if (keepOpen) setValues({});
|
|
2583
|
+
else navigate('/products');
|
|
2584
|
+
};
|
|
2415
2585
|
|
|
2416
2586
|
return (
|
|
2417
|
-
<AppShell
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2587
|
+
<AppShell
|
|
2588
|
+
header={{ variant: 'bar', title: '상품 등록', onBack: () => navigate(-1) }}
|
|
2589
|
+
contentHeight="fill" // 폼 페이지도 fill 이 기본 — 폼이 길어 실제로 스크롤될 때만 auto + scrollEndSpacing
|
|
2590
|
+
>
|
|
2591
|
+
<SForm formClassName="flex flex-col gap-sd-12" onSubmit={save}>
|
|
2592
|
+
<SKeyValueTable
|
|
2593
|
+
fields={fields}
|
|
2594
|
+
values={values}
|
|
2595
|
+
onChange={({ values }) => setValues(values)}
|
|
2596
|
+
/>
|
|
2425
2597
|
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2598
|
+
{/* 버튼 순서는 모든 화면에서 같다 — 취소는 왼쪽 끝, 저장은 오른쪽 끝.
|
|
2599
|
+
부가 요소(체크박스 등)는 저장 바로 왼쪽 */}
|
|
2600
|
+
<div className="flex items-center justify-between">
|
|
2601
|
+
<SButton
|
|
2602
|
+
type="button"
|
|
2603
|
+
color="neutral"
|
|
2604
|
+
outline
|
|
2605
|
+
label="취소"
|
|
2606
|
+
onClick={() => navigate(-1)}
|
|
2607
|
+
/>
|
|
2608
|
+
<div className="flex items-center gap-sd-8">
|
|
2609
|
+
<SCheckbox
|
|
2610
|
+
label="계속 등록하기"
|
|
2611
|
+
value={keepOpen}
|
|
2612
|
+
onValueChange={v => setKeepOpen(v as boolean)}
|
|
2613
|
+
/>
|
|
2614
|
+
<SButton type="submit" label="저장" />
|
|
2433
2615
|
</div>
|
|
2434
|
-
</
|
|
2435
|
-
</
|
|
2616
|
+
</div>
|
|
2617
|
+
</SForm>
|
|
2436
2618
|
</AppShell>
|
|
2437
2619
|
);
|
|
2438
2620
|
}
|
|
2439
2621
|
```
|
|
2622
|
+
<!-- /recipe -->
|
|
2440
2623
|
|
|
2441
2624
|
### 4-4. 상세(조회) 페이지
|
|
2442
2625
|
|
|
@@ -2444,7 +2627,7 @@ export default function ProductCreatePage() {
|
|
|
2444
2627
|
|
|
2445
2628
|
- 조회 값은 `type: 'text'` 행으로 표시한다. **상태·분류 태그도 별도 영역이 아니라 표의 한 행**으로 넣는다 (`render` 에 `STag`).
|
|
2446
2629
|
- 행이 많아지면 **유형별로 섹션을 나누고, 각 섹션을 `SSectionHeaderCard` 로 감싼다.**
|
|
2447
|
-
섹션 제목은 `title` prop 으로, 바디 여백은 `padding` prop 으로 준다.
|
|
2630
|
+
섹션 제목은 `title` prop 으로, 바디 여백은 `padding` prop 으로 준다. **카드 안이 `SKeyValueTable` 하나뿐이면 `padding="none"` 으로 가장자리까지 채우고 표는 `bordered={false} radius="useTop"`** 으로 자기 테두리를 끈다 (§2-2 "카드 가장자리까지 채우는 표").
|
|
2448
2631
|
- **한 화면에 행이 많아 훑어보기가 목적이면 `SKeyValueTable` 에 `dense` 를 켠다** — 셀 상하 여백과
|
|
2449
2632
|
최소 높이가 함께 내려가 행이 좁아진다. 다만 **읽기 전용(`text`) 행에서만 실제로 좁아진다**:
|
|
2450
2633
|
폼 필드가 든 행은 필드 자체의 높이가 행 높이를 정하므로 거의 변하지 않는다. 그래서 필터(§4-2)나
|
|
@@ -2453,23 +2636,19 @@ export default function ProductCreatePage() {
|
|
|
2453
2636
|
- **상세 페이지도 `contentHeight="fill"` 이 기본이다**(§2-2). 섹션이 많아 페이지가 실제로 스크롤되는 화면에서만 `auto` + `scrollEndSpacing` 을 켠다.
|
|
2454
2637
|
- **섹션마다 독립 인라인 폼이 있는 형태**도 상세 페이지의 변형이다. 섹션 안에서 바로 수정·저장하게 하는 화면인데, 이때 버튼 강조는 **섹션 단위가 아니라 페이지 단위로 판단한다** — §3-5-1 의 "`secondary` 연속 배치 금지"는 섹션이 다르면 적용되지 않는다. 그 섹션 안에서 닫히는 저장·취소는 `SSectionHeaderCard` 의 `button`·`footerLeft` 로 넘긴다 (§3-7-8). 아래 예처럼 **페이지 전체를 확정하는 버튼은 카드 밖 하단 줄**에 둔다 — 둘을 섞지 않는다.
|
|
2455
2638
|
|
|
2639
|
+
<!-- recipe:src/recipes/ProductDetailPage.tsx — 아래 코드 블록은 이 파일에서 생성된다(scripts/gen-recipes.mjs). 여기를 고치지 말고 파일을 고친다 -->
|
|
2456
2640
|
```tsx
|
|
2457
2641
|
import {
|
|
2458
|
-
|
|
2642
|
+
SButton,
|
|
2643
|
+
SKeyValueTable,
|
|
2644
|
+
SModal,
|
|
2645
|
+
SSectionHeaderCard,
|
|
2646
|
+
STag,
|
|
2647
|
+
type SKeyValueField,
|
|
2459
2648
|
} from 'sellmate-design-system-react';
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
{ name: 'code', label: '상품코드', type: 'text' },
|
|
2464
|
-
{ name: 'createdAt', label: '등록일', type: 'text' },
|
|
2465
|
-
],
|
|
2466
|
-
[
|
|
2467
|
-
{ name: 'category', label: '카테고리', type: 'text' },
|
|
2468
|
-
// 상태 태그도 표의 한 행으로 표현한다
|
|
2469
|
-
{ name: 'status', label: '상태',
|
|
2470
|
-
render: <STag size="sm" color="green" label="판매중" /> },
|
|
2471
|
-
],
|
|
2472
|
-
];
|
|
2649
|
+
import AppShell from './AppShell';
|
|
2650
|
+
import { removeProducts, useProduct } from './api';
|
|
2651
|
+
import { useNavigate, useParams } from './router';
|
|
2473
2652
|
|
|
2474
2653
|
const priceFields: SKeyValueField[][] = [
|
|
2475
2654
|
[
|
|
@@ -2479,41 +2658,96 @@ const priceFields: SKeyValueField[][] = [
|
|
|
2479
2658
|
];
|
|
2480
2659
|
|
|
2481
2660
|
export default function ProductDetailPage() {
|
|
2661
|
+
const navigate = useNavigate();
|
|
2662
|
+
const { id } = useParams();
|
|
2663
|
+
const product = useProduct(Number(id));
|
|
2664
|
+
|
|
2665
|
+
// 되돌릴 수 없는 액션은 실행 여부만 확정받는다 — 직접 만든 confirm 대신 SModal.confirm (§3-3-1)
|
|
2666
|
+
const confirmDelete = () =>
|
|
2667
|
+
SModal.confirm({
|
|
2668
|
+
type: 'negative',
|
|
2669
|
+
modalTitle: '상품을 삭제할까요?',
|
|
2670
|
+
topMessage: ['삭제된 상품은 복구할 수 없습니다.'],
|
|
2671
|
+
mainButtonLabel: '삭제',
|
|
2672
|
+
}).onOk(async () => {
|
|
2673
|
+
await removeProducts([product.id]);
|
|
2674
|
+
navigate('/products');
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
// 조회 값은 type: 'text' 행으로 보인다.
|
|
2678
|
+
// 상태 태그도 별도 영역이 아니라 표의 한 행이다 — 값에 따라 달라지므로 컴포넌트 안에서 만든다
|
|
2679
|
+
const basicFields: SKeyValueField[][] = [
|
|
2680
|
+
[
|
|
2681
|
+
{ name: 'code', label: '상품코드', type: 'text' },
|
|
2682
|
+
{ name: 'createdAt', label: '등록일', type: 'text' },
|
|
2683
|
+
],
|
|
2684
|
+
[
|
|
2685
|
+
{ name: 'category', label: '카테고리', type: 'text' },
|
|
2686
|
+
{
|
|
2687
|
+
name: 'status',
|
|
2688
|
+
label: '상태',
|
|
2689
|
+
render:
|
|
2690
|
+
product.status === 'selling' ? (
|
|
2691
|
+
<STag size="sm" color="green" label="판매중" />
|
|
2692
|
+
) : (
|
|
2693
|
+
<STag size="sm" color="grey" label="판매중지" />
|
|
2694
|
+
),
|
|
2695
|
+
},
|
|
2696
|
+
],
|
|
2697
|
+
];
|
|
2698
|
+
|
|
2482
2699
|
return (
|
|
2483
2700
|
// 목록에서 들어온 상세 페이지 — onBack 으로 뒤로가기를 준다
|
|
2484
|
-
<AppShell
|
|
2701
|
+
<AppShell
|
|
2702
|
+
header={{ variant: 'bar', title: product.name, onBack: () => navigate('/products') }}
|
|
2703
|
+
contentHeight="fill" // 섹션이 많아 실제로 스크롤될 때만 auto + scrollEndSpacing
|
|
2704
|
+
>
|
|
2485
2705
|
<div className="flex flex-col gap-sd-12">
|
|
2486
|
-
|
|
2487
|
-
|
|
2706
|
+
{/* 행이 많아지면 유형별로 섹션을 나눈다. 카드가 둘 이상이라 marker 를 켠다 —
|
|
2707
|
+
한 페이지 안의 카드 강조(marker·thickness)는 전부 같게 간다 (§4-5).
|
|
2708
|
+
카드 안이 표 하나뿐이면 padding="none" 으로 가장자리까지 채우고, 표는 bordered={false} 로
|
|
2709
|
+
자기 테두리를 끄고 radius="useTop" 으로 위 모서리를 죽인다 — 카드가 테두리·아래 라운드를 그린다 (§2-2) */}
|
|
2710
|
+
<SSectionHeaderCard title="기본 정보" marker padding="none">
|
|
2711
|
+
<SKeyValueTable fields={basicFields} values={product} bordered={false} radius="useTop" />
|
|
2488
2712
|
</SSectionHeaderCard>
|
|
2489
2713
|
|
|
2490
|
-
{/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 */}
|
|
2714
|
+
{/* 헤더 우측에 액션이 필요하면 slot 을 쓴다 — 이동 액션이라 outline (§3-5-1) */}
|
|
2491
2715
|
<SSectionHeaderCard
|
|
2492
2716
|
title="가격 정보"
|
|
2493
2717
|
marker
|
|
2494
2718
|
helpText={['부가세 포함 금액입니다.']}
|
|
2495
|
-
|
|
2719
|
+
padding="none"
|
|
2720
|
+
slot={
|
|
2721
|
+
<SButton
|
|
2722
|
+
size="sm"
|
|
2723
|
+
color="neutral"
|
|
2724
|
+
outline
|
|
2725
|
+
label="이력"
|
|
2726
|
+
onClick={() => navigate(`/products/${id}/price-history`)}
|
|
2727
|
+
/>
|
|
2728
|
+
}
|
|
2496
2729
|
>
|
|
2497
|
-
<SKeyValueTable fields={priceFields} values={product} />
|
|
2730
|
+
<SKeyValueTable fields={priceFields} values={product} bordered={false} radius="useTop" />
|
|
2498
2731
|
</SSectionHeaderCard>
|
|
2499
2732
|
|
|
2500
|
-
{/* 액션은 하단 —
|
|
2733
|
+
{/* 액션은 하단 — 그룹을 모으지 않고 양끝으로 벌린다 (§3-5).
|
|
2734
|
+
목록으로 돌아가기는 헤더의 onBack 이 이미 하므로 되돌리는 버튼을 따로 두지 않는다.
|
|
2735
|
+
되돌리는 액션이 없어도 양끝 분리는 그대로다 — 위계가 낮은 삭제(danger outline)가 왼쪽 끝,
|
|
2736
|
+
가장 강한 실행 액션인 수정이 오른쪽 끝 */}
|
|
2501
2737
|
<div className="flex items-center justify-between">
|
|
2502
|
-
<SButton color="
|
|
2503
|
-
<
|
|
2504
|
-
<SButton color="danger" outline label="삭제" onClick={confirmDelete} />
|
|
2505
|
-
<SButton label="수정" onClick={goEdit} />
|
|
2506
|
-
</div>
|
|
2738
|
+
<SButton color="danger" outline label="삭제" onClick={confirmDelete} />
|
|
2739
|
+
<SButton label="수정" onClick={() => navigate(`/products/${id}/edit`)} />
|
|
2507
2740
|
</div>
|
|
2508
2741
|
</div>
|
|
2509
2742
|
</AppShell>
|
|
2510
2743
|
);
|
|
2511
2744
|
}
|
|
2512
2745
|
```
|
|
2746
|
+
<!-- /recipe -->
|
|
2513
2747
|
|
|
2514
2748
|
### 4-5. 섹션 카드 — SSectionHeaderCard
|
|
2515
2749
|
|
|
2516
|
-
폼 페이지에서도 입력 항목이 많으면 유형별로 `SSectionHeaderCard` 로 나눈다. 주요 옵션:
|
|
2750
|
+
폼 페이지에서도 입력 항목이 많으면 유형별로 `SSectionHeaderCard` 로 나눈다. **카드 안이 `SKeyValueTable` 하나뿐이면 `padding="none"` 으로 가장자리까지 채우고, 표는 `bordered={false} radius="useTop"` 으로 자기 테두리를 끈다** (§2-2 "카드 가장자리까지 채우는 표"). 코드는 Storybook `Recipes/폼 페이지` 의 "섹션 나눔" 에 있다. 주요 옵션:
|
|
2517
2751
|
|
|
2518
2752
|
| Prop (Header) | 용도 |
|
|
2519
2753
|
| --- | --- |
|
|
@@ -2556,24 +2790,48 @@ export default function ProductDetailPage() {
|
|
|
2556
2790
|
|
|
2557
2791
|
로그인 전이라 앱 셸이 아직 없다. `SLayout`·`SGnb`·`SPage` 가 없고, 회색 바탕 위에 카드 하나만 선다. (앱 셸 없이 서는 화면은 이것과 팝업 라우트(§4-7) 둘뿐이다.)
|
|
2558
2792
|
|
|
2793
|
+
<!-- recipe:src/recipes/LoginPage.tsx — 아래 코드 블록은 이 파일에서 생성된다(scripts/gen-recipes.mjs). 여기를 고치지 말고 파일을 고친다 -->
|
|
2559
2794
|
```tsx
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2795
|
+
import { SLoginCard, STextLink } from 'sellmate-design-system-react';
|
|
2796
|
+
import { useNavigate } from './router';
|
|
2797
|
+
|
|
2798
|
+
/**
|
|
2799
|
+
* 로그인 전이라 앱 셸이 없다 — AppShell 을 쓰지 않고 회색 바탕 위에 카드 하나만 선다.
|
|
2800
|
+
* 카드는 화면을 잡지 않으므로 바깥 한 겹은 앱이 만든다. 그 한 겹이 하는 일은 셋뿐이다:
|
|
2801
|
+
* 바탕·여백 / items-stretch 로 카드가 높이를 채우게 / justify-center 로 가로 가운데
|
|
2802
|
+
*/
|
|
2803
|
+
export default function LoginPage() {
|
|
2804
|
+
const navigate = useNavigate();
|
|
2805
|
+
|
|
2806
|
+
return (
|
|
2807
|
+
// 여백은 카드가 최소 폭을 지킬 수 있는 값으로 둔다 — 카드 폭은 className 으로 박지 않는다
|
|
2808
|
+
<div className="flex h-screen items-stretch justify-center bg-bg-neutralLight p-sd-24">
|
|
2809
|
+
<SLoginCard
|
|
2810
|
+
product="AI assistant"
|
|
2811
|
+
title="셀메이트 AI 어시스턴트와 함께 자료 찾는 시간은 줄이고, 업무는 빠르게!"
|
|
2812
|
+
description="(사내 답변은 사내 문서를 기반으로 답변합니다.)"
|
|
2813
|
+
// 로그인 버튼은 카드가 그린다 — 동작만 넘긴다
|
|
2814
|
+
onLogin={() => navigate('/auth/sso')}
|
|
2815
|
+
footerNote="계정이 없다면 담당자에게 문의를 요청해 주세요."
|
|
2816
|
+
// 로그인과 성격이 다른 보조 진입점만 — STextLink 하나. 버튼을 세우지 않는다
|
|
2817
|
+
bottomAction={
|
|
2818
|
+
<STextLink
|
|
2819
|
+
label="관리자 로그인"
|
|
2820
|
+
icon="settingOutline"
|
|
2821
|
+
rightArrow="chevron"
|
|
2822
|
+
onClick={() => navigate('/admin/login')}
|
|
2823
|
+
/>
|
|
2824
|
+
}
|
|
2825
|
+
/>
|
|
2826
|
+
</div>
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2572
2829
|
```
|
|
2830
|
+
<!-- /recipe -->
|
|
2573
2831
|
|
|
2574
2832
|
**바깥 한 겹은 앱이 만든다.** 카드는 화면을 잡지 않는다 — 덕분에 로그인 화면 밖(세션 만료 재인증 등)에서도 같은 카드를 그대로 쓴다. 그 한 겹은 위 세 가지만 하면 되고, 그 이상은 하지 않는다.
|
|
2575
2833
|
|
|
2576
|
-
- 바탕은 `bg-
|
|
2834
|
+
- 바탕은 `bg-bg-neutralLight`, 여백은 `p-sd-24`. **여백은 카드가 최소 폭(368px)을 지킬 수 있는 값으로 둔다** — 좌우 합이 창 폭에서 368px 을 남기지 못하면 카드가 넘친다
|
|
2577
2835
|
- `items-stretch` — 카드가 남은 높이를 채운다. `items-center` 로 두면 카드가 내용 높이로 줄어든다
|
|
2578
2836
|
- `justify-center` — 카드를 가로 가운데에
|
|
2579
2837
|
|
|
@@ -2600,9 +2858,23 @@ export default function ProductDetailPage() {
|
|
|
2600
2858
|
|
|
2601
2859
|
앱 셸 없이 서는 또 하나의 화면이다. 별도 브라우저 창으로 열리는 전용 라우트이므로 `SLayout`·`SGnb`·`SPage` 가 없고, 창을 통째로 `SPopup` 하나가 채운다. 무엇을 팝업으로 열지는 §3-3-1·§3-3-3 에서 고르고, 여기서는 그 라우트의 골격만 다룬다.
|
|
2602
2860
|
|
|
2861
|
+
<!-- recipe:src/recipes/TransferOrderPopupPage.tsx — 아래 코드 블록은 이 파일에서 생성된다(scripts/gen-recipes.mjs). 여기를 고치지 말고 파일을 고친다 -->
|
|
2603
2862
|
```tsx
|
|
2863
|
+
import { useState } from 'react';
|
|
2864
|
+
import {
|
|
2865
|
+
SKeyValueTable,
|
|
2866
|
+
SPopup,
|
|
2867
|
+
SSectionHeaderCard,
|
|
2868
|
+
STable,
|
|
2869
|
+
STabs,
|
|
2870
|
+
type SKeyValueField,
|
|
2871
|
+
type STableColumn,
|
|
2872
|
+
} from 'sellmate-design-system-react';
|
|
2873
|
+
import { ORDER_TABS, useTransferOrder } from './api';
|
|
2874
|
+
import { useParams } from './router';
|
|
2875
|
+
|
|
2604
2876
|
// 1) 목록에서 별도 창을 연다 — 창 크기 = 콘텐츠 크기
|
|
2605
|
-
function
|
|
2877
|
+
export function openTransferOrderPopup(orderId: string) {
|
|
2606
2878
|
window.open(
|
|
2607
2879
|
`${window.location.origin}/popup/transfer-orders/${orderId}`,
|
|
2608
2880
|
`transfer-order-${orderId}`,
|
|
@@ -2610,21 +2882,55 @@ function openDetailPopup(orderId: string) {
|
|
|
2610
2882
|
);
|
|
2611
2883
|
}
|
|
2612
2884
|
|
|
2613
|
-
|
|
2885
|
+
const summaryFields: SKeyValueField[][] = [
|
|
2886
|
+
[
|
|
2887
|
+
{ name: 'from', label: '출발 창고', type: 'text' },
|
|
2888
|
+
{ name: 'to', label: '도착 창고', type: 'text' },
|
|
2889
|
+
{ name: 'requestedAt', label: '요청일', type: 'text' },
|
|
2890
|
+
],
|
|
2891
|
+
];
|
|
2892
|
+
|
|
2893
|
+
// 정렬·폭은 값의 성격에서 나온다 (§3-4) — 코드·일시·닫힌 집합은 center, 양은 right, 자유 텍스트는 기준 폭
|
|
2894
|
+
const itemColumns: STableColumn[] = [
|
|
2895
|
+
{ name: 'sku', label: '품목 코드', field: 'sku', width: 140, align: 'center' },
|
|
2896
|
+
{ name: 'name', label: '품목명', field: 'name', width: 240 },
|
|
2897
|
+
{ name: 'qty', label: '수량', field: 'qty', width: 100, align: 'right' },
|
|
2898
|
+
];
|
|
2899
|
+
|
|
2900
|
+
const historyColumns: STableColumn[] = [
|
|
2901
|
+
{ name: 'at', label: '일시', field: 'at', width: 160, align: 'center' },
|
|
2902
|
+
// 처리 종류는 닫힌 값 집합(요청·승인 …) → center
|
|
2903
|
+
{ name: 'action', label: '처리', field: 'action', width: 100, align: 'center' },
|
|
2904
|
+
{ name: 'by', label: '처리자', field: 'by', width: 120 },
|
|
2905
|
+
];
|
|
2906
|
+
|
|
2907
|
+
// 2) 그 라우트의 루트는 SPopup 하나다 — 앱 셸(SLayout·SGnb·SPage)이 없다. 조회만 하므로 푸터도 없다
|
|
2614
2908
|
export default function TransferOrderPopupPage() {
|
|
2909
|
+
const { orderId } = useParams();
|
|
2910
|
+
const order = useTransferOrder(orderId);
|
|
2911
|
+
const [tab, setTab] = useState('items');
|
|
2912
|
+
|
|
2615
2913
|
return (
|
|
2616
2914
|
// h-screen 으로 창 높이를 잡는다 — 이게 없으면 본문이 창 밖으로 자란다
|
|
2617
2915
|
<SPopup className="h-screen" popupTitle="이동 오더 상세">
|
|
2618
2916
|
{/* 본문 패딩은 SPopup 이 토큰으로 넣는다 — p-sd-* 로 덮어쓰지 않는다 */}
|
|
2619
2917
|
<div className="flex flex-col gap-sd-12">
|
|
2620
|
-
|
|
2621
|
-
<
|
|
2622
|
-
|
|
2918
|
+
{/* 카드 안이 표 하나뿐이라 padding="none" 으로 가장자리까지 채운다 — 표는 자기 테두리를 끄고 위 모서리를 죽인다 (§2-2) */}
|
|
2919
|
+
<SSectionHeaderCard title={order.orderId} padding="none">
|
|
2920
|
+
<SKeyValueTable fields={summaryFields} values={order} bordered={false} radius="useTop" />
|
|
2921
|
+
</SSectionHeaderCard>
|
|
2922
|
+
<STabs value={tab} tabs={ORDER_TABS} onValueChange={setTab} />
|
|
2923
|
+
{tab === 'items' ? (
|
|
2924
|
+
<STable columns={itemColumns} rows={order.items} rowKey="id" resizable />
|
|
2925
|
+
) : (
|
|
2926
|
+
<STable columns={historyColumns} rows={order.history} rowKey="id" resizable />
|
|
2927
|
+
)}
|
|
2623
2928
|
</div>
|
|
2624
2929
|
</SPopup>
|
|
2625
2930
|
);
|
|
2626
2931
|
}
|
|
2627
2932
|
```
|
|
2933
|
+
<!-- /recipe -->
|
|
2628
2934
|
|
|
2629
2935
|
**높이는 라우트가 준다 — `SPopup` 에 `h-screen` 을 준다.** `SPopup` 자체는 부모 높이를 채우도록만 되어 있어서, 부모(라우트 루트·`body`)가 높이를 정해 주지 않으면 100% 가 풀려 본문이 창 밖으로 자란다. 그러면 헤더·푸터가 위아래로 밀려 창 안에 보이지 않는다. `h-screen` 이면 `html`·`body`·마운트 루트에 전역 CSS 를 걸지 않고도 창 높이가 바로 들어온다. (`body` 여백은 0 이어야 한다 — Tailwind preflight 가 이미 0 으로 만든다.)
|
|
2630
2936
|
|
|
@@ -2632,28 +2938,96 @@ export default function TransferOrderPopupPage() {
|
|
|
2632
2938
|
|
|
2633
2939
|
**표를 담으면 본문이 아니라 표가 스크롤한다.** 본문이 통째로 스크롤되면 표 헤더와 페이지네이션 바가 위아래로 밀려 사라진다. `SPopup` 의 본문은 이미 남은 높이를 잡고 있으므로, 본문 루트에 `h-full min-h-0` 으로 세로 축을 잇고 표에 `min-h-0 flex-1` 을 준다. **툴바+표를 묶은 `div` 도 사슬의 한 칸이라 거기에도 `min-h-0 flex-1` 이 필요하다** (§4-2 목록 페이지와 같은 사슬이다 — 한 칸만 끊겨도 표가 높이를 못 잡는데 실패가 조용하다).
|
|
2634
2940
|
|
|
2941
|
+
<!-- recipe:src/recipes/ExcelFilePopupPage.tsx — 아래 코드 블록은 이 파일에서 생성된다(scripts/gen-recipes.mjs). 여기를 고치지 말고 파일을 고친다 -->
|
|
2635
2942
|
```tsx
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2943
|
+
import { useState } from 'react';
|
|
2944
|
+
import {
|
|
2945
|
+
SPopup,
|
|
2946
|
+
SSearchInput,
|
|
2947
|
+
STable,
|
|
2948
|
+
STableBar,
|
|
2949
|
+
type SRow,
|
|
2950
|
+
type STableColumn,
|
|
2951
|
+
} from 'sellmate-design-system-react';
|
|
2952
|
+
import { applyExcelFiles, useExcelFiles } from './api';
|
|
2646
2953
|
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2954
|
+
// 정렬·폭은 값의 성격에서 나온다 (§3-4) — 자유 텍스트는 기준 폭 + resizable, 일자는 center, 양은 right
|
|
2955
|
+
const columns: STableColumn[] = [
|
|
2956
|
+
{ name: 'fileName', label: '파일명', field: 'fileName', width: 320 },
|
|
2957
|
+
{ name: 'uploadedAt', label: '업로드일', field: 'uploadedAt', width: 120, align: 'center' },
|
|
2958
|
+
{
|
|
2959
|
+
name: 'rows',
|
|
2960
|
+
label: '행 수',
|
|
2961
|
+
field: 'rows',
|
|
2962
|
+
width: 100,
|
|
2963
|
+
align: 'right',
|
|
2964
|
+
format: (v: unknown) => Number(v).toLocaleString(),
|
|
2965
|
+
},
|
|
2966
|
+
];
|
|
2967
|
+
|
|
2968
|
+
/**
|
|
2969
|
+
* 표를 담은 팝업 — 본문이 아니라 표가 스크롤한다. 툴바·푸터는 늘 보인다.
|
|
2970
|
+
* SPopup 의 본문은 이미 남은 높이를 잡고 있으므로, 본문 루트부터 표까지 세로 축을 잇는다 (§4-2 와 같은 사슬).
|
|
2971
|
+
*/
|
|
2972
|
+
export default function ExcelFilePopupPage() {
|
|
2973
|
+
// 입력 중인 검색어와 Enter 로 확정된 조회 조건은 따로 든다
|
|
2974
|
+
const [keyword, setKeyword] = useState('');
|
|
2975
|
+
const [query, setQuery] = useState({ keyword: '', page: 1, rowsPerPage: 20 });
|
|
2976
|
+
const [selected, setSelected] = useState<SRow[]>([]);
|
|
2977
|
+
const { rows, total, lastPage } = useExcelFiles(query);
|
|
2978
|
+
|
|
2979
|
+
const save = () => applyExcelFiles(selected.map(row => Number(row.id)));
|
|
2980
|
+
|
|
2981
|
+
return (
|
|
2982
|
+
<SPopup
|
|
2983
|
+
className="h-screen"
|
|
2984
|
+
popupTitle="엑셀 파일 관리"
|
|
2985
|
+
showFooter // 확정할 작업이 있을 때만 (§3-3-3)
|
|
2986
|
+
submitButton={{ label: '저장', disabled: !selected.length }}
|
|
2987
|
+
onSubmit={save}
|
|
2988
|
+
>
|
|
2989
|
+
{/* 툴바+표 = 한 블록이자 본문 루트. h-full min-h-0 → 표의 min-h-0 flex-1 로 세로 축이 이어진다.
|
|
2990
|
+
필터 표가 위에 더 서면 이 블록을 한 겹 감싸고 여기에 min-h-0 flex-1 을 준다 (§4-2 와 같은 사슬) */}
|
|
2991
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
2992
|
+
{/* 조건이 검색어 하나뿐이면 필터 표를 세우지 않는다 — 목록을 좁히기만 하는 검색어는
|
|
2993
|
+
폼 필드가 아니라 SSearchInput 이다 (§3-7-1). 툴바 우측에 둔다 */}
|
|
2994
|
+
<STableBar
|
|
2995
|
+
className="border-b-0"
|
|
2996
|
+
total={total}
|
|
2997
|
+
selected={selected.length}
|
|
2998
|
+
rightActions={
|
|
2999
|
+
<SSearchInput
|
|
3000
|
+
value={keyword}
|
|
3001
|
+
onValueChange={value => {
|
|
3002
|
+
setKeyword(value);
|
|
3003
|
+
// 지우기(×)는 값만 비우고 onSearch 를 부르지 않는다 — 걸려 있던 조건도 여기서 함께 푼다
|
|
3004
|
+
if (!value) setQuery(q => ({ ...q, keyword: '', page: 1 }));
|
|
3005
|
+
}}
|
|
3006
|
+
onSearch={value => setQuery(q => ({ ...q, keyword: value, page: 1 }))}
|
|
3007
|
+
placeholder="파일명 검색"
|
|
3008
|
+
width="md"
|
|
3009
|
+
clearable
|
|
3010
|
+
/>
|
|
3011
|
+
}
|
|
3012
|
+
/>
|
|
3013
|
+
<STable
|
|
3014
|
+
className="min-h-0 flex-1"
|
|
3015
|
+
columns={columns}
|
|
3016
|
+
rows={rows}
|
|
3017
|
+
rowKey="id"
|
|
3018
|
+
resizable
|
|
3019
|
+
selectable
|
|
3020
|
+
selected={selected}
|
|
3021
|
+
onSelectedChange={setSelected}
|
|
3022
|
+
pagination={{ page: query.page, rowsPerPage: query.rowsPerPage, lastPage }}
|
|
3023
|
+
onPageChange={page => setQuery(q => ({ ...q, page }))}
|
|
3024
|
+
/>
|
|
3025
|
+
</div>
|
|
3026
|
+
</SPopup>
|
|
3027
|
+
);
|
|
3028
|
+
}
|
|
2656
3029
|
```
|
|
3030
|
+
<!-- /recipe -->
|
|
2657
3031
|
|
|
2658
3032
|
조회만 하고 표가 없는 팝업은 이 사슬이 필요 없다 — 본문이 통째로 스크롤되어도 헤더는 고정이고 밀려날 푸터가 없다.
|
|
2659
3033
|
|
|
@@ -2707,7 +3081,7 @@ export default function TransferOrderPopupPage() {
|
|
|
2707
3081
|
- [ ] 서버에서 페이지 단위로 받는 `SSelect` 에 `onReachEnd` 와 `hasMore`·`loading`·`serverSearch` 를 함께 줬는가, 늦게 온 응답을 버리는 cleanup 이 있는가 (§3-7-2 — 렌더 최적화는 DS 가 알아서 한다)
|
|
2708
3082
|
- [ ] `SKeyValueTable` 의 짧은 행에 `tdColSpan` 을 주어 전체 열 수를 채웠는가 (§4-3 — 안 채우면 그 구간의 행 구분선이 끊긴다)
|
|
2709
3083
|
- [ ] 날짜·시간 필드가 섞인 `SKeyValueTable` 에서 오른쪽 끝을 맞출지 정했는가 (§4-3 `fieldWidth` — 필터는 `"fill"`, 등록·수정 폼은 기본값. 필드마다 `maxWidth` 를 주지 않는다)
|
|
2710
|
-
- [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
|
|
3084
|
+
- [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가 (되돌리는 액션이 없어도 양끝 분리는 그대로 — 오른쪽에 모으지 않고 위계가 낮은 쪽을 왼쪽 끝에 둔다)
|
|
2711
3085
|
- [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
|
|
2712
3086
|
- [ ] 버튼 `size` 가 위치 규칙과 맞는가 (행 내부 `xs` / 화면 액션 `sm` / 모달 푸터 `md`)
|
|
2713
3087
|
- [ ] 파괴 액션의 채움/outline 이 **적용 범위 + 위험도** 기준과 맞는가
|
|
@@ -3149,6 +3523,7 @@ export const BADGE_COLORS = [
|
|
|
3149
3523
|
| `showTooltip?` | `boolean` | `true` | 막대에 마우스를 올리면 값 상자를 띄운다 |
|
|
3150
3524
|
| `formatValue?` | `(value: number) => string` | `defaultFormat` | 값 표시 형식 |
|
|
3151
3525
|
| `height?` | `number \| string` | — | 차트 높이. **아래 축 텍스트까지 포함한 값**이다 — 그림은 그만큼 줄어든다 |
|
|
3526
|
+
| `tooltipFooter?` | `(category: SBarChartTooltipCategory) => ReactNode` | — | 툴팁의 값 아래에 덧붙일 내용. 짚은 항목을 받아 그린다 — 계열 값만으로 모자란 맥락 (합계, 전월 대비 등)을 적는다. 툴팁은 누를 수 없으므로 버튼·링크는 넣지 않는다 |
|
|
3152
3527
|
| `className?` | `string` | — | |
|
|
3153
3528
|
| `style?` | `CSSProperties` | — | |
|
|
3154
3529
|
|
|
@@ -3229,6 +3604,20 @@ export interface SBarChartPoint {
|
|
|
3229
3604
|
}
|
|
3230
3605
|
```
|
|
3231
3606
|
|
|
3607
|
+
### SBarChartTooltipCategory
|
|
3608
|
+
|
|
3609
|
+
```ts
|
|
3610
|
+
/** 툴팁 하단(`tooltipFooter`)이 받는 짚은 항목. */
|
|
3611
|
+
export interface SBarChartTooltipCategory {
|
|
3612
|
+
/** 카테고리 인덱스 */
|
|
3613
|
+
categoryIndex: number;
|
|
3614
|
+
/** 카테고리 이름 */
|
|
3615
|
+
name: string;
|
|
3616
|
+
/** 툴팁에 값이 뜬 지점들 — 값이 없는 계열은 빠진다 */
|
|
3617
|
+
points: SBarChartPoint[];
|
|
3618
|
+
}
|
|
3619
|
+
```
|
|
3620
|
+
|
|
3232
3621
|
---
|
|
3233
3622
|
|
|
3234
3623
|
# SBarcodeInput
|
|
@@ -3567,6 +3956,7 @@ export const CALENDAR_BOARD_EVENT_COLORS = [
|
|
|
3567
3956
|
| Prop | Type | Default | Description |
|
|
3568
3957
|
|------|------|---------|-------------|
|
|
3569
3958
|
| `type?` | `SCalloutType` | `'default'` | 콜아웃 타입 (색상·아이콘·타이틀 결정). - `default` — 옅은 회색 면. 흰 페이지(`background="frame"`) 위의 기본 안내. - `white` — 같은 안내인데 배경만 흰 면. 회색 면(`neutral`·`screen`) 위에서 default 가 묻힐 때. - `danger` — 좌측에 주의사항 타이틀이 붙는 경고. |
|
|
3959
|
+
| `showTitle?` | `boolean` | `true` | `danger` 좌측의 주의사항 타이틀(아이콘+라벨) 표시 여부. 기본 `true`. `false` 면 타이틀만 빠지고 danger 의 면·테두리·본문색은 그대로다. 다른 타입에는 영향이 없다. |
|
|
3570
3960
|
| `message?` | `SCalloutMessage[]` | `[]` | 표시할 메시지 목록 (배열 중첩으로 depth 표현). 각 문자열은 인라인 HTML(`<b>`, `<a>` 등)을 허용하며 sanitizeInlineHtml 로 살균 후 렌더된다. |
|
|
3571
3961
|
| `children?` | `ReactNode` | — | message 대신 직접 본문을 넣을 때 |
|
|
3572
3962
|
| `className?` | `string` | — | |
|
|
@@ -4792,14 +5182,12 @@ export type SDateRangePickerSize = SFieldSize;
|
|
|
4792
5182
|
| `data` | `SDonutChartSlice[]` | — | 조각 목록. 주는 순서대로 12시에서 시계 방향으로 그린다 — 보통 큰 순으로 준다 |
|
|
4793
5183
|
| `palette?` | `SDonutChartPalette` | `'multi'` | 색 팔레트. `custom` 일 때만 `colors` 를 본다 |
|
|
4794
5184
|
| `colors?` | `SDonutChartColor[]` | — | `palette='custom'` 에서 쓸 색 이름을 조각 순서대로. 조각보다 짧으면 앞에서부터 순환하고, 비어 있으면 `multi` 로 그린다 |
|
|
4795
|
-
| `centerLabel?` | `ReactNode` | — | 가운데 윗줄에 적을 이름 (예: "총 주문") |
|
|
4796
|
-
| `centerValue?` | `ReactNode` | — | 가운데 큰 값. 주지 않으면 그려진 조각의 합계를 `formatValue` 로 적는다 |
|
|
4797
|
-
| `hideCenter?` | `boolean` | `false` | 가운데를 비운다 — 합계 대신 범례·툴팁만으로 읽게 할 때 |
|
|
4798
5185
|
| `showValueLabel?` | `boolean` | `false` | 고리 위에 몫(%)을 적는다. 좁은 조각은 글자가 넘치므로 적지 않는다 |
|
|
4799
5186
|
| `showLegend?` | `boolean` | `true` | 범례를 보인다 |
|
|
4800
5187
|
| `legendPosition?` | `SDonutChartLegendPosition` | `'top'` | 범례 위치 |
|
|
4801
5188
|
| `showTooltip?` | `boolean` | `true` | 조각에 마우스를 올리면 값 상자를 띄운다 |
|
|
4802
|
-
| `formatValue?` | `(value: number) => string` |
|
|
5189
|
+
| `formatValue?` | `(value: number) => string` | — | 값 표시 형식 |
|
|
5190
|
+
| `tooltipFooter?` | `(slice: SDonutChartSlicePoint) => ReactNode` | — | 툴팁의 값 아래에 덧붙일 내용. 짚은 조각을 받아 그린다 — 몫·값만으로 모자란 맥락 (전월 대비, 건수 등)을 적는다. 툴팁은 누를 수 없으므로 버튼·링크는 넣지 않는다 |
|
|
4803
5191
|
| `className?` | `string` | — | 폭은 기본 100% 다. 폭을 정하려면 여기에 준다 — 그림은 그 폭의 정사각형이 된다 |
|
|
4804
5192
|
| `style?` | `CSSProperties` | — | |
|
|
4805
5193
|
|
|
@@ -5790,6 +6178,7 @@ export type SGhostButtonIntent = 'default' | 'danger' | 'action' | 'subAction' |
|
|
|
5790
6178
|
- [SGnb](../SGnb)
|
|
5791
6179
|
- [SGuide](../SGuide)
|
|
5792
6180
|
- [SInput](../SInput)
|
|
6181
|
+
- [SMeatballButton](../SMeatballButton)
|
|
5793
6182
|
- [SModalContainer](../SModalContainer)
|
|
5794
6183
|
- [SOverlayHeader](../SOverlayHeader)
|
|
5795
6184
|
- [SPage](../SPage)
|
|
@@ -6650,6 +7039,7 @@ export type SLayoutHeader = SGnbHeader;
|
|
|
6650
7039
|
| `showTooltip?` | `boolean` | `true` | 항목에 마우스를 올리면 값 상자를 띄운다 |
|
|
6651
7040
|
| `formatValue?` | `(value: number) => string` | `defaultFormat` | 값 표시 형식 |
|
|
6652
7041
|
| `height?` | `number \| string` | — | 차트 높이. **아래 축 텍스트까지 포함한 값**이다 — 그림은 그만큼 줄어든다 |
|
|
7042
|
+
| `tooltipFooter?` | `(category: SLineChartTooltipCategory) => ReactNode` | — | 툴팁의 값 아래에 덧붙일 내용. 짚은 항목을 받아 그린다 — 계열 값만으로 모자란 맥락 (합계, 전월 대비 등)을 적는다. 툴팁은 누를 수 없으므로 버튼·링크는 넣지 않는다 |
|
|
6653
7043
|
| `className?` | `string` | — | |
|
|
6654
7044
|
| `style?` | `CSSProperties` | — | |
|
|
6655
7045
|
|
|
@@ -6730,6 +7120,20 @@ export interface SLineChartPoint {
|
|
|
6730
7120
|
}
|
|
6731
7121
|
```
|
|
6732
7122
|
|
|
7123
|
+
### SLineChartTooltipCategory
|
|
7124
|
+
|
|
7125
|
+
```ts
|
|
7126
|
+
/** 툴팁 하단(`tooltipFooter`)이 받는 짚은 항목. */
|
|
7127
|
+
export interface SLineChartTooltipCategory {
|
|
7128
|
+
/** 카테고리 인덱스 */
|
|
7129
|
+
categoryIndex: number;
|
|
7130
|
+
/** 카테고리 이름 */
|
|
7131
|
+
name: string;
|
|
7132
|
+
/** 툴팁에 값이 뜬 지점들 — 값이 없는 계열은 빠진다. 누적이어도 각 계열이 제 몫으로 가진 값이다 */
|
|
7133
|
+
points: SLineChartPoint[];
|
|
7134
|
+
}
|
|
7135
|
+
```
|
|
7136
|
+
|
|
6733
7137
|
---
|
|
6734
7138
|
|
|
6735
7139
|
# SLinearProgress
|
|
@@ -7045,6 +7449,70 @@ export const LOGO_NAMES = ['sellmate'] as const;
|
|
|
7045
7449
|
|
|
7046
7450
|
---
|
|
7047
7451
|
|
|
7452
|
+
# SMeatballButton
|
|
7453
|
+
|
|
7454
|
+
> 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
|
|
7455
|
+
|
|
7456
|
+
### SMeatballButton
|
|
7457
|
+
|
|
7458
|
+
#### Props
|
|
7459
|
+
|
|
7460
|
+
| Prop | Type | Default | Description |
|
|
7461
|
+
|------|------|---------|-------------|
|
|
7462
|
+
| `size?` | `SMeatballButtonSize` | `'sm'` | 버튼 크기 |
|
|
7463
|
+
| `intent?` | `SMeatballButtonIntent` | `'default'` | 색상 의도. 어두운 배경 위에서는 inverse |
|
|
7464
|
+
| `items?` | `SMeatballButtonItem[]` | `[]` | 메뉴 항목 목록 |
|
|
7465
|
+
| `tooltipText?` | `string` | `'기능 더 보기'` | 호버 시 표시할 툴팁 텍스트. 빈 문자열이면 툴팁을 띄우지 않는다. 메뉴가 열려 있는 동안은 숨는다. |
|
|
7466
|
+
| `ariaLabel?` | `string` | — | 스크린리더용 접근성 레이블. 미지정 시 tooltipText, 그것도 비면 '기능 더 보기' |
|
|
7467
|
+
| `disabled?` | `boolean` | `false` | 비활성 |
|
|
7468
|
+
| `className?` | `string` | — | 버튼을 감싸는 요소의 클래스. 위치 조정(absolute 배치 등)용 |
|
|
7469
|
+
| `style?` | `CSSProperties` | — | 버튼을 감싸는 요소의 스타일. 위치 조정용 |
|
|
7470
|
+
|
|
7471
|
+
#### Events
|
|
7472
|
+
|
|
7473
|
+
| Event | Type | Description |
|
|
7474
|
+
|-------|------|-------------|
|
|
7475
|
+
| `onItemClick` | `(value: string \| number) => void` | 메뉴 항목 선택 |
|
|
7476
|
+
| `onOpenChange` | `(open: boolean) => void` | 열림/닫힘 변경 |
|
|
7477
|
+
|
|
7478
|
+
#### Methods (ref)
|
|
7479
|
+
|
|
7480
|
+
| Method | Type | Description |
|
|
7481
|
+
|--------|------|-------------|
|
|
7482
|
+
| `open` | `() => void` | 메뉴 열기 |
|
|
7483
|
+
| `close` | `() => void` | 메뉴 닫기 |
|
|
7484
|
+
|
|
7485
|
+
## Types
|
|
7486
|
+
|
|
7487
|
+
### SMeatballButtonSize
|
|
7488
|
+
|
|
7489
|
+
```ts
|
|
7490
|
+
export type SMeatballButtonSize = SGhostButtonSize;
|
|
7491
|
+
```
|
|
7492
|
+
|
|
7493
|
+
### SMeatballButtonIntent
|
|
7494
|
+
|
|
7495
|
+
```ts
|
|
7496
|
+
export type SMeatballButtonIntent = 'default' | 'inverse';
|
|
7497
|
+
```
|
|
7498
|
+
|
|
7499
|
+
### SMeatballButtonItem
|
|
7500
|
+
|
|
7501
|
+
```ts
|
|
7502
|
+
/** 메뉴 항목 — SDropdownButton 과 같은 모양이다 */
|
|
7503
|
+
export type SMeatballButtonItem = SDropdownButtonItem;
|
|
7504
|
+
```
|
|
7505
|
+
|
|
7506
|
+
## Dependencies
|
|
7507
|
+
|
|
7508
|
+
### Depends on
|
|
7509
|
+
|
|
7510
|
+
- [SGhostButton](../SGhostButton)
|
|
7511
|
+
|
|
7512
|
+
### Graph
|
|
7513
|
+
|
|
7514
|
+
---
|
|
7515
|
+
|
|
7048
7516
|
# SModal
|
|
7049
7517
|
|
|
7050
7518
|
> 수작성 문서 — SModal 은 컴포넌트가 아니라 **명령형 모달 서비스**라 `docs:gen`(Props/Events 표) 대상이 아니다.
|
|
@@ -7476,6 +7944,7 @@ export type SNumberInputSize = SFieldSize;
|
|
|
7476
7944
|
|------|------|---------|-------------|
|
|
7477
7945
|
| `background?` | `SPageBackground` | `'frame'` | 페이지 배경 표면. frame=흰 콘텐츠 면, neutral=옅은 회색 면, screen=앱 바탕. 스크롤바 처리도 여기 묶여 있다 — 셋 다 구분선+트랙 배경이고, 트랙 색만 neutral 에서 흰색이 된다. |
|
|
7478
7946
|
| `scrollEndSpacing?` | `boolean` | `false` | 스크롤 끝 여백. 마지막 항목이 창 하단에 붙어 "여기서 끝"이 안 읽히는 것을 막는다. **기본은 꺼져 있다.** 페이지 스크롤 자체가 예외이기 때문이다 — 대부분의 화면은 본문이 창을 꽉 채우고 스크롤은 각 영역 안에서 일어난다. 여백을 조건 없이 붙이면 내용이 화면에 거의 딱 맞는 페이지까지 그 여백 때문에 스크롤되게 만든다. 페이지가 실제로 스크롤되는 화면(`contentHeight="auto"` + 내용이 창보다 김)에서만 켠다. 페이지네이션처럼 끝을 알려주는 것이 이미 있으면 켜지 않는다. |
|
|
7947
|
+
| `padding?` | `SPagePadding` | `'default'` | 본문 안쪽 여백. 기본은 `default` — `--cmp-pageBody-padding-default` 를 사방에 넣는다. `none` 은 필터 바처럼 **가장자리까지 차는 블록이 맨 위에 서는** 화면용이다. 패딩을 끄면 그 아래 블록이 `p-[var(--cmp-pageBody-padding-default)]` 로 같은 토큰의 여백을 직접 잡는다 — `p-sd-*` 로 적으면 토큰이 바뀌어도 따라가지 않는다. 사방 패딩만 끄고 `scrollEndSpacing` 은 그대로다. (`SPopup`·`SSectionHeaderCard` 의 `padding="none"` 과 같은 관용구) |
|
|
7479
7948
|
| `overlayScrollbar?` | `boolean` | `false` | true면 네이티브 스크롤바를 숨기고 SPage 위에 오버레이 스크롤바를 얹는다. 스크롤바가 레이아웃 폭을 차지하지 않아 내부 콘텐츠 폭이 줄어들지 않는다. |
|
|
7480
7949
|
| `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` 이 무시된다. |
|
|
7481
7950
|
| `contentWidth?` | `SPageContentWidth` | `'full'` | 본문 가로 폭 모드. 기본은 `full` — 창이 넓어지면 본문도 같이 넓어진다(반응형). `fixed`·`fixedCenter` 는 본문을 **레이아웃 최소 폭일 때의 폭**으로 고정한다. 최소 폭 기준으로 그린 시안이 넓은 모니터에서 늘어나 성기어지는 것을 막는 모드다. 남는 폭은 `fixed` 면 오른쪽에, `fixedCenter` 면 좌우로 나뉘어 페이지 배경으로 남는다. 고정 폭은 GNB 폭·접힘을 반영해 저절로 따라오므로 앱이 숫자를 쓸 일은 없다. 창이 최소 폭보다 좁아지면 세 모드가 모두 같다 — 남는 폭이 없어 규칙이 꺼지고, 모자란 몫을 지금까지처럼 이 영역이 가로 스크롤로 삼킨다. 같은 열에 `SPageHeader` 가 있으면 헤더도 같은 폭·정렬을 따른다(제목과 본문의 좌우 끝이 맞는다). `SLayout` 밖에서는 기준이 될 최소 폭이 없으므로 무시된다. |
|
|
@@ -7494,6 +7963,12 @@ export type SPageHeaderVariant = (typeof PAGE_HEADER_VARIANTS)[number];
|
|
|
7494
7963
|
export type SPageBackground = (typeof PAGE_BACKGROUNDS)[number];
|
|
7495
7964
|
```
|
|
7496
7965
|
|
|
7966
|
+
### SPagePadding
|
|
7967
|
+
|
|
7968
|
+
```ts
|
|
7969
|
+
export type SPagePadding = (typeof PAGE_PADDINGS)[number];
|
|
7970
|
+
```
|
|
7971
|
+
|
|
7497
7972
|
### SPageContentHeight
|
|
7498
7973
|
|
|
7499
7974
|
```ts
|
|
@@ -7530,6 +8005,18 @@ export const PAGE_HEADER_VARIANTS = ['plain', 'bar'] as const;
|
|
|
7530
8005
|
export const PAGE_BACKGROUNDS = ['frame', 'neutral', 'screen'] as const;
|
|
7531
8006
|
```
|
|
7532
8007
|
|
|
8008
|
+
### PAGE_PADDINGS
|
|
8009
|
+
|
|
8010
|
+
```ts
|
|
8011
|
+
/**
|
|
8012
|
+
* 본문 안쪽 여백 모드.
|
|
8013
|
+
* - default: SPage 가 --cmp-pageBody-padding-default 로 사방 패딩을 넣는다
|
|
8014
|
+
* - none: 패딩을 끈다. 가장자리까지 차는 블록(필터 바 등)이 맨 위에 서고, 그 아래 블록이
|
|
8015
|
+
* 같은 토큰으로 여백을 직접 잡는 화면용. SPopup·SSectionHeaderCard 의 padding="none" 과 같은 관용구.
|
|
8016
|
+
*/
|
|
8017
|
+
export const PAGE_PADDINGS = ['default', 'none'] as const;
|
|
8018
|
+
```
|
|
8019
|
+
|
|
7533
8020
|
### PAGE_CONTENT_HEIGHTS
|
|
7534
8021
|
|
|
7535
8022
|
```ts
|
|
@@ -7625,6 +8112,7 @@ export const PAGE_CONTENT_WIDTHS = ['full', 'fixed', 'fixedCenter'] as const;
|
|
|
7625
8112
|
| `legendPosition?` | `SPieChartLegendPosition` | `'top'` | 범례 위치 |
|
|
7626
8113
|
| `showTooltip?` | `boolean` | `true` | 조각에 마우스를 올리면 값 상자를 띄운다 |
|
|
7627
8114
|
| `formatValue?` | `(value: number) => string` | — | 값 표시 형식 |
|
|
8115
|
+
| `tooltipFooter?` | `(slice: SPieChartSlicePoint) => ReactNode` | — | 툴팁의 값 아래에 덧붙일 내용. 짚은 조각을 받아 그린다 — 몫·값만으로 모자란 맥락 (전월 대비, 건수 등)을 적는다. 툴팁은 누를 수 없으므로 버튼·링크는 넣지 않는다 |
|
|
7628
8116
|
| `className?` | `string` | — | 폭은 기본 100% 다. 폭을 정하려면 여기에 준다 — 그림은 그 폭의 정사각형이 된다 |
|
|
7629
8117
|
| `style?` | `CSSProperties` | — | |
|
|
7630
8118
|
|