sellmate-design-system-react 9.0.0-beta.54 → 9.0.0-beta.55

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 CHANGED
@@ -833,6 +833,32 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
833
833
 
834
834
  **`button` 은 `SButton` 의 prop 을 그대로 받는다** — `icon`·`rightIcon`·`outline`·`disabled` 등을 함께 넘길 수 있다. 단 **`size` 는 타입에 없다**: 하단 액션 영역은 언제나 `md` 이고(§3-5-2) 예외가 없어 푸터가 고정한다. **`color` 는 주지 않는 것이 기본**이다 — 주지 않으면 주 액션 규칙대로 `primary` 가 되고, 그 버튼이 파괴적 액션일 때만 §3-5-3 에 따라 `danger` 를 준다. 카드(§3-7-8)의 `button` 도 같다.
835
835
 
836
+ **확인 모달은 확인 버튼 하나로 세우는 것이 기본이다 — `subButtonLabel` 을 주지 않는다.** 비워 두면 서브 버튼이 아예 서지 않는다. 답하지 않고 나가는 길은 **X 버튼이 이미 맡고**(늘 서 있다), `persistent` 를 켜지 않았으면 백드롭·ESC 로도 닫힌다(§3-3-5).
837
+
838
+ **서브 버튼은 `onCancel` 이 닫는 것 말고 따로 할 일이 있을 때만 둔다.** 눌러도 창만 닫힌다면 X 와 똑같은 일을 하는 버튼이 하나 더 서는 것이라, 어느 것을 눌러야 하는지가 오히려 흐려진다. **되묻는 모달이라고 예외가 아니다** — "삭제할까요?" 에 답하지 않고 나가는 것은 X 로 충분하다.
839
+
840
+ 그래서 서브 버튼이 설 자리는 **확인과 대등한 두 번째 선택지**뿐이고, 그때 글자는 `취소`·`닫기` 가 아니라 그 버튼이 하는 일의 이름이 된다.
841
+
842
+ ```tsx
843
+ ✅ SModal.confirm({ type: 'negative', modalTitle: '삭제할까요?', mainButtonLabel: '삭제' })
844
+ .onOk(() => remove())
845
+ ✅ SModal.confirm({ type: 'negative', modalTitle: '재고가 모자란 주문이 있습니다',
846
+ mainButtonLabel: '자동 취소', subButtonLabel: '수동 처리' })
847
+ .onOk(() => cancelShortOrders()).onCancel(() => openManualFlow())
848
+ // 서브가 창을 닫는 것이 아니라 다른 흐름을 시작한다
849
+
850
+ ❌ SModal.confirm({ …, mainButtonLabel: '삭제', subButtonLabel: '취소' }).onCancel(() => {})
851
+ // 눌러도 닫히기만 한다 — X 가 이미 하는 일
852
+ ❌ SModal.confirm({ …, mainButtonLabel: '확인', subButtonLabel: '닫기' })
853
+ // 닫으려고 붙인 버튼 — X 와 겹친다
854
+ ❌ SModal.confirm({ type: 'positive', modalTitle: '저장되었습니다', mainButtonLabel: '확인', subButtonLabel: '취소' })
855
+ // 이미 끝난 일 옆의 취소 — 무엇이 취소되는지 알 수 없다
856
+ ```
857
+
858
+ 습관적으로 `subButtonLabel: '취소'` 를 옵션에 넣지 않는다. **`onCancel` 에 적을 것이 없으면 서브 버튼도 없다.**
859
+
860
+ **확인 모달의 확인 버튼은 `mainButtonDisabled` 로 잠근다.** 기본값이 `false` 이므로 **아직 확정할 수 없을 때만** 준다 — 동의를 안 받았거나, `contentSlot` 의 입력이 덜 찼거나, 확인 뒤에 나갈 요청이 이미 나가는 중일 때. **왜 못 누르는지를 화면에 함께 둔다**(`bottomMessage` 나 `contentSlot`): 흐려진 버튼만 남으면 사용자가 고장으로 읽는다. **취소는 따라 잠그지 않는다** — 확정하지 못하는 상태일수록 나가는 길은 열려 있어야 해서 서브 버튼을 막는 prop 자체가 없다. 조건이 채워지면 `SModal.confirm` 이 돌려준 핸들로 `update({ mainButtonDisabled: false })` 해서 푼다.
861
+
836
862
  **모달 안에서도 앱의 훅을 그냥 쓴다 — 단, 앱 루트에 `SModalOutlet` 이 있어야 한다 (§4-1).** outlet 이 있으면 명령형 모달이 앱 렌더 트리의 자식으로 그려지므로 `useQuery`·`useNavigate`·`useTheme` 같은 Context 기반 훅이 페이지에서와 똑같이 동작한다. **모달 컴포넌트를 Provider 로 다시 감싸지 않는다.** outlet 없이 띄우면 모달이 별도 React 루트로 떠서 Provider 가 하나도 닿지 않고, `No QueryClient set` 처럼 모달을 여는 순간에만 터진다.
837
863
 
838
864
  #### 3-3-5. 닫기 경로 — `persistent` 기본값은 컴포넌트마다 다르다
@@ -843,7 +869,7 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
843
869
  | --- | --- | --- | --- |
844
870
  | `SActionModal` · `SLoadingModal` | `true` | X 버튼, 모달 안의 버튼 | 막힘 (흔들림) |
845
871
  | `SDrawer` | `true` | X 버튼, footer 버튼 | 막힘 (흔들림) |
846
- | `SConfirmModal` | `false` | X 버튼, 확인/취소 버튼 | **닫힌다** |
872
+ | `SConfirmModal` | `false` | X 버튼, 확인 버튼 | **닫힌다** |
847
873
  | `SPopover` · `STooltip` · `SSelect` 등 floating | — | 바깥 클릭·ESC | **막지 않는다** — 이 규칙의 대상이 아니다 |
848
874
 
849
875
  따라서 다음을 지킨다.
@@ -852,6 +878,7 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
852
878
  - **기본값과 같은 `persistent` 를 직접 주지 않는다.** 중복이다.
853
879
  - **`SActionModal`·`SDrawer` 에 `persistent={false}` 는 잃을 입력이 없을 때만.** 단순 알림처럼 임의로 닫혀도 아무것도 사라지지 않는 경우로 한정한다.
854
880
  - **`SConfirmModal` 에 `persistent` 를 켜는 것은 반드시 답을 받아야 할 때만.** 되돌릴 수 없는 파괴적 작업의 확인처럼, 임의로 닫히면 안 되는 경우로 한정한다.
881
+ - **`SConfirmModal` 에 `persistent` 와 `mainButtonDisabled` 를 함께 켜면 나가는 길이 X 하나만 남는다.** 갇히지는 않지만(X 는 늘 서 있다) 확인이 왜 잠겼는지가 화면에 없으면 막다른 길로 읽힌다 — 잠근 이유를 `bottomMessage` 나 `contentSlot` 에 함께 둔다(§3-3-4).
855
882
 
856
883
  작성 중인 내용이 있을 때 닫기를 시도하면 이탈 안내를 띄우는 것은 **소비 앱 몫**이다. 디자인 시스템은 dirty 상태를 알지 못하므로 백드롭·ESC 를 일괄 차단할 뿐이다. 안내가 필요하면 앱이 자체 dirty 판정 후 `SModal.confirm` 으로 띄운다.
857
884
 
@@ -1364,7 +1391,9 @@ tableRef.current.scrollToRow(row); // 복원
1364
1391
 
1365
1392
  **"이 계정으로 갈 수 있는 다른 서비스가 무엇인가"를 한 자리에 모은 목록**이다. 한 줄이 서비스 하나이고, 위에 브랜드 로고(+ 상태 태그) 아래에 서비스 이름이 선다.
1366
1393
 
1367
- **직접 띄우지 않는다.** 런처 버튼에 붙여 여는 것은 `SGnb` 의 `launcher` 가 맡는다 — 목록만 넘기면 버튼 아래로 · 버튼 왼쪽 끝에 맞춰 뜨고, 서비스를 고르거나 바깥을 누르면 닫힌다.
1394
+ **직접 띄우지 않는다.** 런처 버튼에 붙여 여는 것은 `SGnb` 의 `launcher` 가 맡는다 — 목록만 넘기면 버튼 아래로 · 버튼 왼쪽 끝에 맞춰 뜨고, 서비스를 고르거나 바깥을 누르면 닫힌다. 고른 서비스는 **새 탭에서 열린다** — 이동도 앱이 만들지 않는다.
1395
+
1396
+ **앱이 넘기는 것은 서비스 키와 갈 주소 둘뿐이다.** 로고·서비스 표기·이름·상태 태그는 디자인 시스템의 카탈로그가 서비스마다 정해 두므로 앱이 만들지 않는다.
1368
1397
 
1369
1398
  ```tsx
1370
1399
  <SGnb
@@ -1372,35 +1401,36 @@ tableRef.current.scrollToRow(row); // 복원
1372
1401
  logo={<Logo />}
1373
1402
  launcher={{
1374
1403
  items: [
1375
- { value: 'sellmate', name: '셀메이트' },
1376
- { value: 'wms', service: 'WMS', name: '셀메이트 WMS' },
1377
- { value: 'account', service: 'Account', name: '셀메이트 어카운트',
1378
- tag: 'NEW', tagColor: 'blue', tagIcon: 'star' },
1379
- { value: 'crm', service: 'CRM', name: '셀메이트 CRM', tag: '출시예정', disabled: true },
1404
+ { service: 'sellmate' }, // 없는 줄 — 주소를 주지 않는다
1405
+ { service: 'wms', href: env.WMS_URL }, // 주소는 앱의 환경(QA·실서버)에서 온다
1406
+ { service: 'account', href: env.ACCOUNT_URL },
1407
+ { service: 'crm', href: env.CRM_URL },
1380
1408
  ],
1381
- onSelect: value => go(SERVICE_URL[value]),
1382
1409
  }}
1383
1410
  />
1384
1411
  ```
1385
1412
 
1386
1413
  - **`launcher` 를 주지 않으면 런처 버튼 자체가 렌더되지 않는다.** 옮겨 갈 서비스가 없는 앱은 이 슬롯을 비워 둔다 — 눌러도 아무것도 없는 버튼을 상단바에 남기지 않는다.
1387
- - **어디로 갈지는 `launcher.onSelect` 하나로 받는다.** 줄마다 콜백을 두지 않는다 목록에서 일어나는 일은 "어느 서비스로 옮겨 가는가" 하나뿐이라, 고른 줄의 `value` 하나면 곳이 정해진다. 고르면 목록은 스스로 닫히고, `disabled` 줄은 눌리지 않아 오지 않는다. (런처 **버튼**을 누른 것은 `launcher.onClick` 으로 따로 온다 여는 말고 일이 있을 때만 준다.)
1388
- - **셀메이트 워드마크는 리스트박스가 늘 그린다.** 앱이 넘기는 것은 그 오른쪽에 잇는 **서비스 표기(`service`)** 뿐이다 — `WMS` · `Account` · `CRM` 같은 것. 워드마크를 앱이 넘기게 하면 같은 로고가 화면마다 다른 크기·색으로 선다. 셀메이트 본체처럼 표기가 없는 줄은 `service` 를 주지 않으면 워드마크만 선다.
1389
- - **표기를 글자로 넘기면 크기·굵기·색을 주지 않는다.** 워드마크 옆에 서는 글자는 로고의 일부처럼 읽혀야 해서 리스트박스가 한 모습으로 정한다(14px bold · `fg.accentLight`). 서비스마다 색을 달리 주면 로고가 서비스마다 다른 물건처럼 보인다. 서비스가 자기 그래픽 마크를 가지면 svg·img 로 넘기고, 그때도 정해진 높이에 비율대로 들어가므로 **크기를 직접 주지 않는다.**
1414
+ - **서비스는 키(`service`)로 고른다 보이는 것은 아무것도 넘기지 않는다.** 있는 키는 `'sellmate' | 'wms' | 'account' | 'crm'`(`SLauncherService`)이고, 로고 오른쪽 표기(`WMS`·`Account`·`CRM`)·아래 한글 이름·상태 태그(`NEW`·`출시예정`)·출시 여부를 카탈로그(`LAUNCHER_SERVICE_CATALOG`)가 키로 정한다. 앱이 넘기게 하면 같은 서비스가 앱마다 다른 이름·색·문구로 선다. **서비스가 늘거나 이름·상태가 바뀌면 디자인 시스템 릴리스로 따라온다** 앱은 고칠 것이 없다.
1390
1415
 
1391
1416
  ```tsx
1392
- ✅ { value: 'sellmate', name: '셀메이트' } // 표기 없음 — 워드마크만
1393
- { value: 'wms', service: 'WMS', name: '셀메이트 WMS' }
1394
- { value: 'x', service: <XMark />, name: '자기 마크를 가진 서비스' }
1417
+ ✅ { service: 'wms', href: env.WMS_URL }
1418
+ { service: 'WMS', name: '셀메이트 WMS', href: … } // 표기·이름을 앱이 정하지 않는다
1419
+ { service: 'wms', tag: 'NEW', href: } // 태그도 앱이 정하지 않는다
1420
+ ❌ { service: <><SLogo size={12} />WMS</>, … } // 로고를 다시 넘기지 않는다
1421
+ ```
1422
+ - **주소는 앱이 준다 — 디자인 시스템은 주소를 들지 않는다.** 앱마다 QA·실서버 주소가 달라 한곳에서 관리할 수 없기 때문이다. `href` 를 주면 그 줄은 링크가 되어 **새 탭에서 열린다** — 가운데 클릭·⌘클릭·주소 복사가 그대로 듣는다.
1423
+ - **이동을 앱이 다시 하지 않는다.** `launcher.onSelect` 는 **고른 것을 알려 줄 뿐 화면을 옮기지 않는다** — 여기서 `window.open` 이나 라우터를 부르면 탭이 두 번 열린다. 기록을 남기는 것처럼 이동 말고 할 일이 있을 때만 준다. 고르면 목록은 스스로 닫히고, 갈 수 없는 줄은 눌리지 않아 오지 않는다. (런처 **버튼**을 누른 것은 `launcher.onClick` 으로 따로 온다.)
1424
+ - **`href` 를 주는 것이 곧 "갈 수 있다"는 뜻이다 — 줄을 막는 prop 은 없다.** 주소를 주지 않으면 그 줄은 목록에 남되 눌리지 않고 물러난다. 지금 보고 있는 서비스든 이 앱이 계약하지 않은 서비스든 갈 곳이 없기는 같아서 한 가지로 다룬다.
1395
1425
 
1396
- ❌ { value: 'wms', service: <><SLogo size={12} />WMS</>, … } // 워드마크를 다시 넘기지 않는다
1397
- { value: 'wms', service: <span className="typo-body-sm-bold text-fg-success">WMS</span>, }
1398
- // 글자의 색·크기를 앱이 정하지 않는다
1426
+ ```tsx
1427
+ { service: 'sellmate' } // 없다 주소를 주지 않는다
1428
+ ❌ { service: 'sellmate', disabled: true } // 줄을 막는 prop 없다
1399
1429
  ```
1400
- - **아직 수 없는 서비스는 목록에서 빼지 않고 `disabled` 로 둔다.** 그 서비스가 있다는 사실과 아직쓴다는 사실을 함께 알리는 것이 런처의 일이다. **왜 쓰는지를 `tag` 함께 적는다**(`출시예정`·`미계약` 등) 이유 없이 흐려진 줄은 고장으로 읽힌다.
1401
- - **태그는 알릴 것이 있는 줄에만 붙인다.** 모든 줄에 붙으면 무엇이 새것인지가 읽히지 않는다.
1402
- - **줄에 얹으면 "여기서 나간다"는 것이 드러난다** 배경이 옅은 파랑으로 물들고 이름이 단계 진해지며, 오른쪽 끝에 페이지 이동 아이콘이 떠오른다. **앱이 켜고 끄는 prop 이 없다** — 다른 서비스로 넘어가는 자리라는 신호는 화면마다 달라지면 된다. `disabled` 줄은 얹어도 반응하지 않는다.
1403
- - **지금 보고 있는 서비스를 표시하는 prop 없다.** 런처는 "여기 말고 어디로 있는가"를 보여주는 자리라 현재 위치는 GNB 이미 말하고 있다.
1430
+ - **갈 수 없는 서비스도 목록에서 빼지 않는다.** 그 서비스가 있다는 사실과 지금은간다는 사실을 함께 알리는 것이 런처의 일이다. 지금 보고 있는 서비스를 빼면 사용자가 지금 어디에 있는지가 런처에서 사라진다.
1431
+ - **출시 전인 서비스는 앱이 일이 없다.** 카탈로그가 `출시예정` 태그와 함께 막아 두므로 주소를 줘도 열리지 않고, 열리는 날 디자인 시스템 릴리스 하나로 모든 앱에서 함께 열린다. **앱이 앞질러 열 수단은 없다.**
1432
+ - **목록 순서를 앱이 정하지 않는다.** 넘긴 순서가 아니라 카탈로그 순서(`LAUNCHER_SERVICES`)로 선다 같은 런처가 앱마다 다른 순서로 뜨면 사용자가 매번 다시 찾는다. 정렬해서 넘기려 애쓰지 않는다.
1433
+ - **줄에 얹으면 "여기서 나간다"는 것이 드러난다** — 배경이 옅은 파랑으로 물들고 이름이 한 단계 진해지며, 오른쪽 끝에 페이지 이동 아이콘이 떠오른다. **앱이 켜고 끄는 prop 없다** 다른 서비스로 넘어가는 자리라는 신호는 화면마다 달라지면 된다. 없는 줄은 얹어도 반응하지 않는다.
1404
1434
  - **폭을 늘리지 않는다.** 로고 길이에 따라 패널이 출렁이지 않도록 고정 폭이다.
1405
1435
  - **뜨는 방향은 prop 이 아니다.** 런처 버튼이 상단바 왼쪽 끝에 있으므로 **아래로 · 버튼 왼쪽 끝에 맞춰** 펼친다. 방향을 바꾸는 prop 은 없으니 찾지 않는다.
1406
1436
  - **`header="fix"` 에서 GNB 를 접으면 열려 있던 목록은 닫힌다.** 접힘 레일에는 폴드 버튼만 남아 런처 버튼이 화면에서 사라지기 때문이다. **그 닫힘을 알아야 하면 `launcher.onOpenChange` 를 준다** — 열림을 따라 그리는 화면이 앱에 있을 때만 필요하다.
@@ -1986,7 +2016,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
1986
2016
  - 슬롯이 남는 폭을 통째로 받으므로 **정렬은 안에서 직접 잡는다** (좌측 정렬 + 우측은 `ml-auto`).
1987
2017
  - `header="full"` 에서 로고 자리는 140px 로 고정된다 — 로고 내용이 바뀌어도 `topContent` 시작점이 흔들리지 않게 하기 위함이다. 로고가 그보다 넓으면 잘리므로 이 폭에 맞춰 준비한다.
1988
2018
  - `launcher` 를 주지 않으면 런처가 렌더되지 않고, 그 자리(버튼 + 간격)를 로고 슬롯이 이어받아 172px 가 된다. `topContent` 시작점은 런처 유무와 관계없이 같은 자리다.
1989
- - **런처 버튼은 서비스 목록의 트리거다.** `launcher.items` 에 옮겨 갈 서비스를 넘기면 버튼 아래로 목록이 뜬다팝오버를 손으로 조립하지 않는다 (§3-5-9).
2019
+ - **런처 버튼은 서비스 목록의 트리거다.** `launcher.items` 에 옮겨 갈 서비스를 **키와 주소로** 넘기면 버튼 아래로 목록이 뜨고, 고른 서비스가 새 탭에서 열린다 팝오버도 이동도 손으로 만들지 않는다 (§3-5-9).
1990
2020
 
1991
2021
  ```tsx
1992
2022
  <SLayout type="box" header="full">
@@ -25,7 +25,8 @@
25
25
  | `contentSlot?` | `ReactNode` | — | 콘텐츠 박스 바로 아래에 통으로 들어가는 커스텀 콘텐츠 (전체 폭) |
26
26
  | `mainButtonLabel?` | `string` | `'확인'` | 메인(확인) 버튼에 찍히는 글자 |
27
27
  | `mainButtonOutline?` | `boolean` | — | 메인 버튼을 아웃라인(테두리)으로 세울지. 지정하지 않으면 `type` 이 정한다 — positive·negative 는 채운 버튼, default 는 아웃라인. **색은 여기서 고르지 않는다.** `type` 이 정한다(positive=primary / negative=danger / default=neutral) — 아이콘·제목이 말하는 것과 버튼 색이 어긋나면 읽는 사람이 헷갈리므로 그 조합은 타입에서 막는다. 글자는 `mainButtonLabel` 이다. `type="default"` 에 `false` 를 주면 테두리 없는 흰 버튼이 된다(neutral 은 solid·outline 이 같은 흰 면이고 테두리 유무만 다르다). 확인 버튼이 눌러야 할 것으로 안 읽히므로 권하지 않는다. |
28
- | `subButtonLabel?` | `string` | | 서브(취소) 버튼 |
28
+ | `mainButtonDisabled?` | `boolean` | `false` | 메인 버튼을 눌리지 않게 한다. **기본값 `false`.** 모달 안에서 **아직 확정할 수 없을 때** 쓴다 — 동의 체크를 아직 안 했거나, `contentSlot` 의 입력이 덜 찼거나, 확인 뒤에 나갈 요청이 이미 나가는 중일 때. **왜 못 누르는지는 화면이 말해야 한다.** 흐려진 버튼만 남으면 사용자는 고장으로 읽는다 — 못 채운 것을 `bottomMessage` 나 `contentSlot` 안에서 함께 보인다. 서브(취소) 버튼은 따라 잠기지 않는다. 확정하지 못하는 상태일수록 **나가는 길은 열려 있어야** 하기 때문이다. 그래서 서브 버튼을 막는 prop 은 두지 않는다. |
29
+ | `subButtonLabel?` | `string` | — | 서브 버튼에 찍히는 글자. **주지 않는 것이 기본이다** — 비어 있으면 버튼이 아예 서지 않고 확인 하나만 남는다. **`onCancel` 이 닫는 것 말고 따로 할 일이 있을 때만 준다.** 눌러도 창만 닫힌다면 X 버튼이 이미 그 일을 하므로(X 는 늘 서 있고, `persistent` 를 켜지 않았으면 백드롭·ESC 로도 닫힌다) 같은 일을 하는 버튼을 하나 더 세우지 않는다. 되묻는 모달이라고 해서 예외가 아니다 — "삭제할까요?" 에 답하지 않고 나가는 것은 X 로 충분하다. 그래서 서브 버튼이 설 자리는 **확인과 대등한 두 번째 선택지**다. 그때 글자는 `취소`·`닫기` 가 아니라 그 버튼이 하는 일의 이름이 된다. ```tsx ✅ { mainButtonLabel: '자동 취소', subButtonLabel: '수동 처리' } // 두 갈래를 고르게 한다 ❌ { mainButtonLabel: '삭제', subButtonLabel: '취소' } // X 가 이미 하는 일 ``` |
29
30
 
30
31
  #### Events
31
32
 
@@ -43,7 +43,36 @@ export interface SConfirmModalProps {
43
43
  * 같은 흰 면이고 테두리 유무만 다르다). 확인 버튼이 눌러야 할 것으로 안 읽히므로 권하지 않는다.
44
44
  */
45
45
  mainButtonOutline?: boolean;
46
- /** 서브(취소) 버튼 */
46
+ /**
47
+ * 메인 버튼을 눌리지 않게 한다. **기본값 `false`.**
48
+ *
49
+ * 모달 안에서 **아직 확정할 수 없을 때** 쓴다 — 동의 체크를 아직 안 했거나, `contentSlot` 의
50
+ * 입력이 덜 찼거나, 확인 뒤에 나갈 요청이 이미 나가는 중일 때.
51
+ *
52
+ * **왜 못 누르는지는 화면이 말해야 한다.** 흐려진 버튼만 남으면 사용자는 고장으로 읽는다 —
53
+ * 못 채운 것을 `bottomMessage` 나 `contentSlot` 안에서 함께 보인다.
54
+ *
55
+ * 서브(취소) 버튼은 따라 잠기지 않는다. 확정하지 못하는 상태일수록 **나가는 길은 열려 있어야**
56
+ * 하기 때문이다. 그래서 서브 버튼을 막는 prop 은 두지 않는다.
57
+ */
58
+ mainButtonDisabled?: boolean;
59
+ /**
60
+ * 서브 버튼에 찍히는 글자. **주지 않는 것이 기본이다** — 비어 있으면 버튼이 아예 서지 않고
61
+ * 확인 하나만 남는다.
62
+ *
63
+ * **`onCancel` 이 닫는 것 말고 따로 할 일이 있을 때만 준다.** 눌러도 창만 닫힌다면 X 버튼이
64
+ * 이미 그 일을 하므로(X 는 늘 서 있고, `persistent` 를 켜지 않았으면 백드롭·ESC 로도 닫힌다)
65
+ * 같은 일을 하는 버튼을 하나 더 세우지 않는다. 되묻는 모달이라고 해서 예외가 아니다 —
66
+ * "삭제할까요?" 에 답하지 않고 나가는 것은 X 로 충분하다.
67
+ *
68
+ * 그래서 서브 버튼이 설 자리는 **확인과 대등한 두 번째 선택지**다. 그때 글자는 `취소`·`닫기`
69
+ * 가 아니라 그 버튼이 하는 일의 이름이 된다.
70
+ *
71
+ * ```tsx
72
+ * ✅ { mainButtonLabel: '자동 취소', subButtonLabel: '수동 처리' } // 두 갈래를 고르게 한다
73
+ * ❌ { mainButtonLabel: '삭제', subButtonLabel: '취소' } // X 가 이미 하는 일
74
+ * ```
75
+ */
47
76
  subButtonLabel?: string;
48
77
  /** 확인 버튼 클릭 (sdOk) */
49
78
  onOk?: () => void;
@@ -52,4 +81,4 @@ export interface SConfirmModalProps {
52
81
  onClose?: () => void;
53
82
  }
54
83
  /** SConfirmModal — sd-confirm-modal 포팅. 아이콘+제목+메시지+태그+확인/취소 버튼. */
55
- export declare function SConfirmModal({ open, onOpenChange, persistent, type, modalTitle, titleClassName, topMessage, bottomMessage, tagLabel, tagShape, tagSize, tagColor, slotLabel, tagSlot, optionSlot, contentSlot, mainButtonLabel, mainButtonOutline, subButtonLabel, onOk, onCancel, onClose, }: SConfirmModalProps): import("react").JSX.Element;
84
+ export declare function SConfirmModal({ open, onOpenChange, persistent, type, modalTitle, titleClassName, topMessage, bottomMessage, tagLabel, tagShape, tagSize, tagColor, slotLabel, tagSlot, optionSlot, contentSlot, mainButtonLabel, mainButtonOutline, mainButtonDisabled, subButtonLabel, onOk, onCancel, onClose, }: SConfirmModalProps): import("react").JSX.Element;
@@ -101,10 +101,13 @@ export interface SGnbLauncher {
101
101
  /** 런처 **버튼**을 누름. 목록은 어차피 열리므로, 여는 것 말고 따로 할 일이 있을 때만 준다 */
102
102
  onClick?: () => void;
103
103
  /**
104
- * 목록에서 **서비스를 고름**. 고른 줄의 `value` 가 온다 — 그 값 하나로 앱이 갈 곳을 정한다.
105
- * 고르면 목록은 할 일을 끝낸 것이라 스스로 닫힌다(`onOpenChange(false)` 도 함께 온다).
104
+ * 목록에서 **서비스를 고름**. 고른 서비스 키가 온다.
105
+ *
106
+ * **여기서 화면을 옮기지 않는다** — 줄은 링크라 브라우저가 `href` 를 새 탭으로 이미 연다.
107
+ * 고른 것을 앱이 알아야 할 때만(기록 등) 준다. 고르면 목록은 할 일을 끝낸 것이라 스스로
108
+ * 닫힌다(`onOpenChange(false)` 도 함께 온다).
106
109
  */
107
- onSelect?: (value: string) => void;
110
+ onSelect?: (service: SLauncherService) => void;
108
111
  /**
109
112
  * 목록이 열리고 닫힐 때. 여닫는 것은 런처 버튼이 스스로 하므로 **그 사실을 알아야 할 때만** 준다
110
113
  * (연 김에 목록을 다시 불러오거나, 열려 있는 동안 다른 층을 접어 두는 화면).
@@ -1,5 +1,5 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
- import { type SLauncherListBoxItem } from '../SLauncherListBox';
2
+ import { type SLauncherListBoxItem, type SLauncherService } from '../SLauncherListBox';
3
3
  import type { SGnbSystemPlacement } from '../SGnbSystem/gnbSystem.config';
4
4
  import { type SGnbType, type SGnbHeader, type SGnbColor, type SGnbMenuItem } from './gnb.config';
5
5
  /**
@@ -55,10 +55,13 @@ export interface SGnbLauncher {
55
55
  /** 런처 **버튼**을 누름. 목록은 어차피 열리므로, 여는 것 말고 따로 할 일이 있을 때만 준다 */
56
56
  onClick?: () => void;
57
57
  /**
58
- * 목록에서 **서비스를 고름**. 고른 줄의 `value` 가 온다 — 그 값 하나로 앱이 갈 곳을 정한다.
59
- * 고르면 목록은 할 일을 끝낸 것이라 스스로 닫힌다(`onOpenChange(false)` 도 함께 온다).
58
+ * 목록에서 **서비스를 고름**. 고른 서비스 키가 온다.
59
+ *
60
+ * **여기서 화면을 옮기지 않는다** — 줄은 링크라 브라우저가 `href` 를 새 탭으로 이미 연다.
61
+ * 고른 것을 앱이 알아야 할 때만(기록 등) 준다. 고르면 목록은 할 일을 끝낸 것이라 스스로
62
+ * 닫힌다(`onOpenChange(false)` 도 함께 온다).
60
63
  */
61
- onSelect?: (value: string) => void;
64
+ onSelect?: (service: SLauncherService) => void;
62
65
  /**
63
66
  * 목록이 열리고 닫힐 때. 여닫는 것은 런처 버튼이 스스로 하므로 **그 사실을 알아야 할 때만** 준다
64
67
  * (연 김에 목록을 다시 불러오거나, 열려 있는 동안 다른 층을 접어 두는 화면).
@@ -8,14 +8,14 @@
8
8
 
9
9
  | Prop | Type | Default | Description |
10
10
  |------|------|---------|-------------|
11
- | `items?` | `SLauncherListBoxItem[]` | — | 옮겨 있는 서비스 목록. 비어 있으면 아무것도 렌더하지 않는다 |
11
+ | `items?` | `SLauncherListBoxItem[]` | — | 목록에 세울 서비스. 비어 있으면 아무것도 렌더하지 않는다. **순서는 넘긴 대로가 아니라 카탈로그 순서다**(`LAUNCHER_SERVICES`) — 같은 런처가 앱마다 다른 순서로 뜨면 사용자가 매번 다시 찾는다. 같은 서비스를 두 번 넘기면 처음 것만 선다. |
12
12
  | `ariaLabel?` | `string` | — | 목록(ul)의 접근성 레이블 |
13
13
 
14
14
  #### Events
15
15
 
16
16
  | Event | Type | Description |
17
17
  |-------|------|-------------|
18
- | `onSelect` | `(value: string) => void` | 서비스를 고름. **줄마다 콜백을 두지 않는다** — 목록에서 일어나는 일은 "어느 서비스로 옮겨 가는가" 하나뿐이라, 고른 줄의 `value` 하나면 앱이 곳을 정할있다. `disabled` 줄은 눌리지 않으므로 여기로 오지 않는다. |
18
+ | `onSelect` | `(service: SLauncherService) => void` | 서비스를 고름. **가는 것은 콜백이 하지 않는다** — 줄이 링크라 브라우저가 `href` 탭으로 연다. 고른 것을 앱이 알아야 때만(기록·목록 닫기) 준다. 없는 줄(주소가 없거나 카탈로그가 아직 출시 전으로 든 서비스)은 눌리지 않으므로 여기로 오지 않는다. |
19
19
 
20
20
  ## Types
21
21
 
@@ -24,36 +24,39 @@
24
24
  ```ts
25
25
  /** 리스트박스의 한 줄 — 옮겨 갈 수 있는 서비스 하나 */
26
26
  export interface SLauncherListBoxItem {
27
- /** 목록 key */
28
- value: string;
29
27
  /**
30
- * 워드마크 오른쪽에 잇는 **서비스 표기** (slot) `WMS` · `Account` · `CRM` 같은 것.
31
- * 셀메이트 워드마크는 컴포넌트가 늘 그리므로 넘기지 않는다. 없으면 워드마크만 선다.
32
- *
33
- * **글자로 넘기면 크기·굵기·색을 주지 않는다** — 워드마크 옆에 서는 글자는 로고의 일부처럼
34
- * 읽혀야 해서 이 컴포넌트가 한 모습으로 정한다(14px bold · `fg.accentLight`).
35
- *
36
- * ```tsx
37
- * { value: 'wms', service: 'WMS', name: '셀메이트 WMS' }
38
- * ```
28
+ * 어느 서비스인가. **로고·표기·이름·상태 태그는 하나로 정해진다** 카탈로그
29
+ * (`LAUNCHER_SERVICE_CATALOG`)가 들고 있으므로 앱이 넘기지 않는다.
30
+ */
31
+ service: SLauncherService;
32
+ /**
33
+ * 새 탭에서 열 주소. **디자인 시스템은 주소를 들지 않는다** — 앱마다 QA·실서버가 달라
34
+ * 여기서 관리할 수 없다.
39
35
  *
40
- * 서비스가 자기 그래픽 마크를 가지면 svg·img 넘긴다 로고 높이에 맞춰 비율대로 들어가므로
41
- * 크기를 따로 주지 않는다.
36
+ * **주소를 주는 것이 "갈 있다"는 뜻이다.** 비우면 줄은 목록에 남되 눌리지 않고
37
+ * 물러난다 지금 보고 있는 서비스든, 이 앱이 계약하지 않은 서비스든 갈 곳이 없기는 같다.
38
+ * 못 가는 줄을 막는 별도의 prop 은 없다.
42
39
  */
43
- service?: ReactNode;
44
- /** 로고 아래 줄에 적히는 서비스 이름 */
45
- name?: string;
46
- /** 로고 오른쪽에 붙는 태그 텍스트. 없으면 태그가 붙지 않는다 */
47
- tag?: string;
48
- /** 태그 색. 기본 `'grey'` */
49
- tagColor?: STagColor;
50
- /** 태그 라벨 왼쪽 아이콘. 없으면 글자만 남는다 */
51
- tagIcon?: SIconName;
52
- /** 아직 갈 수 없는 서비스(출시 전 · 미계약). 눌리지 않고 로고·이름이 물러난다 */
53
- disabled?: boolean;
40
+ href?: string;
54
41
  }
55
42
  ```
56
43
 
44
+ ### SLauncherService
45
+
46
+ ```ts
47
+ export type SLauncherService = (typeof LAUNCHER_SERVICES)[number];
48
+ ```
49
+
50
+ ### LAUNCHER_SERVICES
51
+
52
+ ```ts
53
+ /**
54
+ * 런처에 설 수 있는 서비스 — **목록의 순서이기도 하다.**
55
+ * 앱이 넘긴 순서가 아니라 이 순서로 선다: 같은 런처가 앱마다 다른 순서로 뜨면 사용자가 매번 다시 찾는다.
56
+ */
57
+ export const LAUNCHER_SERVICES = ['sellmate', 'wms', 'account', 'crm'] as const;
58
+ ```
59
+
57
60
  ## Dependencies
58
61
 
59
62
  ### Used by
@@ -1,45 +1,38 @@
1
- import { type HTMLAttributes, type ReactNode } from 'react';
2
- import { type SIconName } from '../SIcon';
3
- import { type STagColor } from '../STag';
1
+ import { type HTMLAttributes } from 'react';
2
+ import { type SLauncherService } from './launcherListBox.config';
4
3
  /** 리스트박스의 한 줄 — 옮겨 갈 수 있는 서비스 하나 */
5
4
  export interface SLauncherListBoxItem {
6
- /** 목록 key */
7
- value: string;
8
5
  /**
9
- * 워드마크 오른쪽에 잇는 **서비스 표기** (slot) `WMS` · `Account` · `CRM` 같은 것.
10
- * 셀메이트 워드마크는 컴포넌트가 늘 그리므로 넘기지 않는다. 없으면 워드마크만 선다.
11
- *
12
- * **글자로 넘기면 크기·굵기·색을 주지 않는다** — 워드마크 옆에 서는 글자는 로고의 일부처럼
13
- * 읽혀야 해서 이 컴포넌트가 한 모습으로 정한다(14px bold · `fg.accentLight`).
14
- *
15
- * ```tsx
16
- * { value: 'wms', service: 'WMS', name: '셀메이트 WMS' }
17
- * ```
6
+ * 어느 서비스인가. **로고·표기·이름·상태 태그는 하나로 정해진다** 카탈로그
7
+ * (`LAUNCHER_SERVICE_CATALOG`)가 들고 있으므로 앱이 넘기지 않는다.
8
+ */
9
+ service: SLauncherService;
10
+ /**
11
+ * 새 탭에서 열 주소. **디자인 시스템은 주소를 들지 않는다** — 앱마다 QA·실서버가 달라
12
+ * 여기서 관리할 수 없다.
18
13
  *
19
- * 서비스가 자기 그래픽 마크를 가지면 svg·img 넘긴다 로고 높이에 맞춰 비율대로 들어가므로
20
- * 크기를 따로 주지 않는다.
14
+ * **주소를 주는 것이 "갈 있다"는 뜻이다.** 비우면 줄은 목록에 남되 눌리지 않고
15
+ * 물러난다 지금 보고 있는 서비스든, 이 앱이 계약하지 않은 서비스든 갈 곳이 없기는 같다.
16
+ * 못 가는 줄을 막는 별도의 prop 은 없다.
21
17
  */
22
- service?: ReactNode;
23
- /** 로고 아래 줄에 적히는 서비스 이름 */
24
- name?: string;
25
- /** 로고 오른쪽에 붙는 태그 텍스트. 없으면 태그가 붙지 않는다 */
26
- tag?: string;
27
- /** 태그 색. 기본 `'grey'` */
28
- tagColor?: STagColor;
29
- /** 태그 라벨 왼쪽 아이콘. 없으면 글자만 남는다 */
30
- tagIcon?: SIconName;
31
- /** 아직 갈 수 없는 서비스(출시 전 · 미계약). 눌리지 않고 로고·이름이 물러난다 */
32
- disabled?: boolean;
18
+ href?: string;
33
19
  }
34
20
  export interface SLauncherListBoxProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'> {
35
- /** 옮겨 갈 수 있는 서비스 목록. 비어 있으면 아무것도 렌더하지 않는다 */
21
+ /**
22
+ * 목록에 세울 서비스. 비어 있으면 아무것도 렌더하지 않는다.
23
+ *
24
+ * **순서는 넘긴 대로가 아니라 카탈로그 순서다**(`LAUNCHER_SERVICES`) — 같은 런처가 앱마다
25
+ * 다른 순서로 뜨면 사용자가 매번 다시 찾는다. 같은 서비스를 두 번 넘기면 처음 것만 선다.
26
+ */
36
27
  items?: SLauncherListBoxItem[];
37
28
  /**
38
- * 서비스를 고름. **줄마다 콜백을 두지 않는다** — 목록에서 일어나는 일은 "어느 서비스로
39
- * 옮겨 가는가" 하나뿐이라, 고른 줄의 `value` 하나면 앱이 곳을 정할 있다.
40
- * `disabled` 줄은 눌리지 않으므로 여기로 오지 않는다.
29
+ * 서비스를 고름. **가는 것은 콜백이 하지 않는다** — 줄이 링크라 브라우저가 `href`
30
+ * 탭으로 연다. 고른 것을 앱이 알아야 때만(기록·목록 닫기) 준다.
31
+ *
32
+ * 갈 수 없는 줄(주소가 없거나 카탈로그가 아직 출시 전으로 든 서비스)은 눌리지 않으므로
33
+ * 여기로 오지 않는다.
41
34
  */
42
- onSelect?: (value: string) => void;
35
+ onSelect?: (service: SLauncherService) => void;
43
36
  /** 목록(ul)의 접근성 레이블 */
44
37
  ariaLabel?: string;
45
38
  }
@@ -48,18 +41,22 @@ export interface SLauncherListBoxProps extends Omit<HTMLAttributes<HTMLDivElemen
48
41
  *
49
42
  * 한 줄이 서비스 하나다: 위에 브랜드 로고(+ 상태 태그), 아래에 한글 이름.
50
43
  *
51
- * **셀메이트 워드마크는 컴포넌트가 그린다.** 앱은 오른쪽에 잇는 서비스 표기(`service` —
52
- * WMS · Account · CRM …)만 넘긴다. 표기를 **글자로 넘기면 모습도 이 컴포넌트가 정한다**
53
- * (14px bold · `fg.accentLight`) 워드마크 옆에 서는 글자는 로고의 일부처럼 읽혀야 해서
54
- * 서비스마다 달라지면 안 된다.
44
+ * **앱이 넘기는 것은 서비스 키와 주소 둘뿐이다.** 로고·서비스 표기·이름·상태 태그는 카탈로그
45
+ * (`LAUNCHER_SERVICE_CATALOG`)가 서비스마다 모습으로 정해 두므로 앱이 넘기지 않는다
46
+ * 넘기게 하면 같은 서비스가 앱마다 다른 이름·색·문구로 선다.
47
+ *
48
+ * **주소는 카탈로그에 두지 않는다.** 앱마다 QA·실서버 주소가 달라 디자인 시스템이 관리할 수 없다.
49
+ * 앱이 `href` 로 넘기면 줄이 링크가 되어 **새 탭에서 열린다** — 가운데 클릭·⌘클릭도 그대로 듣는다.
50
+ * **주소를 주는 것이 곧 "갈 수 있다"는 뜻이다**: 주지 않은 줄은 눌리지 않는다(지금 보고 있는
51
+ * 서비스이거나 이 앱이 계약하지 않은 서비스). 줄을 막는 별도의 prop 은 없다.
55
52
  *
56
53
  * **줄에 얹으면 "여기서 나간다"는 것이 드러난다** — 배경이 옅은 파랑으로 물들고 이름이 한 단계
57
54
  * 진해지며, 오른쪽 끝에 페이지 이동 아이콘이 떠오른다. 아이콘 자리는 늘 비워 두므로 얹었다 뗄 때
58
55
  * 글자가 밀리지 않는다.
59
56
  *
60
- * 아직 갈 수 없는 서비스(출시 · 미계약)는 `disabled` 둔다 눌리지 않고 얹어도 반응하지 않으며
61
- * 로고는 색이 빠지고 이름은 비활성 글자색이 된다. **목록에서 빼지 않는 것이 기본이다**: 서비스가
62
- * 있다는 사실과 아직 쓴다는 사실을 함께 알리는 것이 런처의 일이라, 이유를 적은 태그(`tag`)를 함께 붙인다.
57
+ * 갈 수 없는 줄은 눌리지 않고 로고는 색이 빠지며 이름은 비활성 글자색이 된다. **목록에서 빼지
58
+ * 않는 것이 기본이다**: 서비스가 있다는 사실과 지금은 간다는 사실을 함께 알리는 것이 런처의
59
+ * 일이다. 출시 전인 서비스는 카탈로그가 `출시예정` 태그와 함께 막아 두므로 앱이 일이 없다.
63
60
  *
64
61
  * **이 컴포넌트는 패널만 그린다.** 런처 버튼에 붙여 띄우는 것은 `SGnb` 의 `launcher` 가 맡으므로
65
62
  * (`launcher.items` 에 이 목록을 넘긴다), 앱이 직접 팝오버를 조립할 일은 없다.
@@ -1,2 +1,2 @@
1
1
  export { SLauncherListBox, type SLauncherListBoxProps, type SLauncherListBoxItem, } from './SLauncherListBox';
2
- export { LAUNCHER_LIST_BOX_LAYOUT, LAUNCHER_LIST_BOX_LOGO_HEIGHT, LAUNCHER_LIST_BOX_LOGO_SERVICE_GAP, LAUNCHER_LIST_BOX_TRAILING_ICON, LAUNCHER_LIST_BOX_WIDTH, } from './launcherListBox.config';
2
+ export { LAUNCHER_LIST_BOX_LAYOUT, LAUNCHER_LIST_BOX_LOGO_HEIGHT, LAUNCHER_LIST_BOX_LOGO_SERVICE_GAP, LAUNCHER_LIST_BOX_TRAILING_ICON, LAUNCHER_LIST_BOX_WIDTH, LAUNCHER_SERVICES, LAUNCHER_SERVICE_CATALOG, type SLauncherService, type SLauncherServiceEntry, } from './launcherListBox.config';
@@ -1,4 +1,5 @@
1
1
  import type { SIconName } from '../SIcon';
2
+ import type { STagColor } from '../STag';
2
3
  /**
3
4
  * 패널 고정 폭(px, 시안 — 전용 토큰 없음).
4
5
  * 담기는 것은 서비스 로고 한 줄이라 내용 폭을 따르면 로고 길이에 따라 패널이 출렁인다.
@@ -21,6 +22,44 @@ export declare const LAUNCHER_LIST_BOX_LOGO_SERVICE_GAP = 8;
21
22
  * TODO(디자인): 시안의 글리프를 확인해 확정한다.
22
23
  */
23
24
  export declare const LAUNCHER_LIST_BOX_TRAILING_ICON: SIconName;
25
+ /**
26
+ * 런처에 설 수 있는 서비스 — **목록의 순서이기도 하다.**
27
+ * 앱이 넘긴 순서가 아니라 이 순서로 선다: 같은 런처가 앱마다 다른 순서로 뜨면 사용자가 매번 다시 찾는다.
28
+ */
29
+ export declare const LAUNCHER_SERVICES: readonly ["sellmate", "wms", "account", "crm"];
30
+ export type SLauncherService = (typeof LAUNCHER_SERVICES)[number];
31
+ /** 카탈로그 한 줄 — 그 서비스가 런처에서 갖는 고정된 모습 */
32
+ export interface SLauncherServiceEntry {
33
+ /**
34
+ * 워드마크 오른쪽에 잇는 서비스 표기(`WMS` · `Account` · `CRM`). 셀메이트 본체처럼 표기가
35
+ * 없는 서비스는 비운다 — 워드마크만 선다.
36
+ */
37
+ mark?: string;
38
+ /** 로고 아래 줄에 적히는 서비스 이름 */
39
+ name: string;
40
+ /**
41
+ * 상태 태그(`출시예정`·`NEW` 등). **앱이 붙이거나 지울 수 없다** — 같은 서비스가 앱마다
42
+ * 다른 문구를 달면 그 문구를 사용자가 믿을 수 없게 된다.
43
+ */
44
+ tag?: string;
45
+ /** 태그 색. 비우면 `'grey'` */
46
+ tagColor?: STagColor;
47
+ /** 태그 라벨 왼쪽 아이콘 */
48
+ tagIcon?: SIconName;
49
+ /** 아직 아무도 못 쓰는 서비스(출시 전). 계약 여부처럼 앱마다 갈리는 것은 여기 적지 않는다 */
50
+ disabled?: boolean;
51
+ }
52
+ /**
53
+ * 서비스 카탈로그 — **런처에 서는 각 서비스의 모습은 여기가 정한다.**
54
+ *
55
+ * 앱은 서비스 키와 갈 주소만 넘긴다. 표기·이름·태그를 앱이 넘기게 하면 같은 서비스가 앱마다 다른
56
+ * 이름으로 서고(`셀메이트 어카운트` / `Account` / `어카운트`), 한쪽을 고쳐도 나머지는 그대로 남는다.
57
+ *
58
+ * **주소는 여기 없다.** 앱마다 QA·실서버 주소가 다르므로 갈 곳은 앱이 `href` 로 넘긴다.
59
+ *
60
+ * 서비스가 늘거나 이름·상태가 바뀌면 이 표를 고치고 릴리스한다 — 소비 앱은 고칠 것이 없다.
61
+ */
62
+ export declare const LAUNCHER_SERVICE_CATALOG: Record<SLauncherService, SLauncherServiceEntry>;
24
63
  /** 치수·색 — 전용 토큰이 없는 자리만 위 상수를 쓴다 */
25
64
  export declare const LAUNCHER_LIST_BOX_LAYOUT: {
26
65
  /** 패널 배경 */
@@ -47,7 +47,7 @@ import { SModalOutlet } from 'sellmate-design-system-react';
47
47
 
48
48
  ## SModal.confirm
49
49
 
50
- 아이콘 + 제목 + 메시지 + 확인/취소 버튼. `type` 에 따라 아이콘·메인 버튼 색이 결정된다.
50
+ 아이콘 + 제목 + 메시지 + 확인 버튼. `type` 에 따라 아이콘·메인 버튼 색이 결정된다.
51
51
 
52
52
  ```tsx
53
53
  SModal.confirm({
@@ -55,13 +55,23 @@ SModal.confirm({
55
55
  modalTitle: '삭제하시겠습니까?',
56
56
  topMessage: ['이 작업은 되돌릴 수 없습니다.'],
57
57
  mainButtonLabel: '삭제',
58
- subButtonLabel: '취소',
58
+ }).onOk(() => deleteItem());
59
+ ```
60
+
61
+ **`subButtonLabel` 은 기본이 비어 있고, 그대로 두는 것이 기본이다.** `onCancel` 이 닫는 것 말고 따로 할 일이 있을 때만 준다 — 눌러도 창만 닫힌다면 X 버튼이 이미 하는 일이라(백드롭·ESC 도 열려 있다) 같은 버튼을 하나 더 세우지 않는다. 되묻는 모달도 예외가 아니다. 서브 버튼이 설 자리는 확인과 대등한 두 번째 선택지일 때이고, 그때 글자는 `취소`·`닫기` 가 아니라 그 버튼이 하는 일의 이름이 된다.
62
+
63
+ ```tsx
64
+ SModal.confirm({
65
+ type: 'negative',
66
+ modalTitle: '재고가 모자란 주문이 있습니다',
67
+ mainButtonLabel: '자동 취소',
68
+ subButtonLabel: '수동 처리',
59
69
  })
60
- .onOk(() => deleteItem())
61
- .onCancel(() => {});
70
+ .onOk(() => cancelShortOrders())
71
+ .onCancel(() => openManualFlow());
62
72
  ```
63
73
 
64
- **옵션 (`SConfirmOptions`)** — 선언형 `SConfirmModalProps` 에서 제어 흐름 props(`open`/`onOpenChange`/`onOk`/`onCancel`/`onClose`)를 제외한 전부. 주요 키: `type`, `modalTitle`, `topMessage`/`bottomMessage`, `mainButtonLabel`/`mainButtonOutline`, `subButtonLabel`, `tagSlot`/`optionSlot`/`contentSlot`, `persistent`.
74
+ **옵션 (`SConfirmOptions`)** — 선언형 `SConfirmModalProps` 에서 제어 흐름 props(`open`/`onOpenChange`/`onOk`/`onCancel`/`onClose`)를 제외한 전부. 주요 키: `type`, `modalTitle`, `topMessage`/`bottomMessage`, `mainButtonLabel`/`mainButtonOutline`/`mainButtonDisabled`, `subButtonLabel`, `tagSlot`/`optionSlot`/`contentSlot`, `persistent`.
65
75
 
66
76
  ---
67
77
 
@@ -82,7 +82,7 @@ export interface SModalRef {
82
82
  *
83
83
  * @example
84
84
  * // 확인/취소
85
- * SModal.confirm({ type: 'negative', modalTitle: '삭제하시겠습니까?', mainButtonLabel: '삭제', subButtonLabel: '취소' })
85
+ * SModal.confirm({ type: 'negative', modalTitle: '삭제하시겠습니까?', mainButtonLabel: '삭제' })
86
86
  * .onOk(() => deleteItem())
87
87
  * .onCancel(() => {});
88
88
  *