sellmate-design-system-react 9.0.0-beta.44 → 9.0.0-beta.46
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 +107 -4
- package/dist/components/SButton/README.md +2 -0
- package/dist/components/SChatInput/README.md +44 -0
- package/dist/components/SChatInput/SChatInput.d.ts +60 -0
- package/dist/components/SChatInput/index.d.ts +1 -0
- package/dist/components/SChatMessage/README.md +81 -0
- package/dist/components/SChatMessage/SChatMessage.d.ts +102 -0
- package/dist/components/SChatMessage/index.d.ts +1 -0
- package/dist/components/SFooter/README.md +13 -8
- package/dist/components/SFooter/SFooter.d.ts +14 -9
- package/dist/components/SGhostButton/README.md +2 -0
- package/dist/components/SIcon/README.md +2 -0
- package/dist/components/SIcon/icons.gen.d.ts +1 -0
- package/dist/components/SImage/README.md +5 -0
- package/dist/components/SLoginCard/README.md +34 -0
- package/dist/components/SLoginCard/SLoginCard.d.ts +28 -0
- package/dist/components/SLoginCard/index.d.ts +1 -0
- package/dist/components/SLoginCard/sellmate-wordmark.d.ts +13 -0
- package/dist/components/SPopup/README.md +5 -6
- package/dist/components/SPopup/SPopup.d.ts +6 -7
- package/dist/components/SSelect/README.md +8 -0
- package/dist/components/SSelect/SSelect.d.ts +44 -0
- package/dist/components/SSelect/row-metrics.d.ts +60 -0
- package/dist/components/SSelect/select.config.d.ts +15 -0
- package/dist/components/STextarea/README.md +6 -1
- package/dist/components/STextarea/STextarea.d.ts +25 -2
- package/dist/index.cjs +9036 -8350
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9059 -8379
- package/dist/index.js.map +1 -1
- package/dist/llms-full.txt +293 -19
- package/dist/llms.txt +109 -6
- package/dist/styles.css +89 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
| 분류 | 컴포넌트 |
|
|
28
28
|
| --- | --- |
|
|
29
29
|
| **버튼·링크** | `SButton` `SGhostButton` `SDropdownButton` `STextLink` `SSwitch` `SToggle` |
|
|
30
|
-
| **입력 (폼)** | `SForm` `SField` `SInput` `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
|
|
30
|
+
| **입력 (폼)** | `SForm` `SField` `SInput` `SChatInput`(대화 입력창) `SSearchInput` `SNumberInput` `STextarea` `SEditor` `SSelect` `SCheckbox` `SRadio` `SRadioGroup` `SRadioButton` `SRadioCard` `SRadioCardGroup` `SChip` `SChipInput` `SBarcodeInput` `SFilePicker` |
|
|
31
31
|
| **날짜·시간** | `SCalendar` `SCalendarBoard`(한 달치 일정 판) `SDatePicker` `SDatePickerYearListbox` `SDatePickerMonthListbox` `SDateRangePicker` `STimePicker` `STimeRangePicker` |
|
|
32
|
-
| **표·목록** | `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
|
|
33
|
-
| **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
|
|
32
|
+
| **표·목록** | `SChatMessage`(대화의 메시지 한 건) `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SListItem` `SExpansionList` `SDraggableList` `SDraggableItem` `STree` |
|
|
33
|
+
| **레이아웃** | `SLayout` `SGnb` `SGnbSystem` `SSystemActionButton`(GNB system 패널에 한 줄씩 쌓는 액션 행 — 버튼 고르기는 §3-5) `SAccountListBox`(계정 행을 눌러 뜨는 계정 패널) `SPage` `SPageHeader`(페이지 제목 영역 — `SLayout` 안에서 `SPage` 앞에 둔다) `SSectionHeaderCard` `SCard` `SLoginCard`(통합 계정 로그인 화면의 카드) `SDivider` `SSplitter` `SScrollArea` `SExpansionItem` |
|
|
34
34
|
| **내비게이션** | `STabs` `SPagination` `SStepper` |
|
|
35
35
|
| **표시·상태** | `STag` `SBadge` `SIcon` `SImage` `SCallout` `SGuide` |
|
|
36
36
|
| **진행·로딩** | `SLinearProgress` `SCircleProgress` `SLoadingContainer` `SLoadingModal` |
|
|
@@ -138,6 +138,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
138
138
|
|
|
139
139
|
**`toLocaleDateString()` 은 쓰지 않는다** — 로케일에 따라 결과가 바뀌어 표기를 지킬 수 없다.
|
|
140
140
|
|
|
141
|
+
**대화 화면의 시각은 예외다.** `SChatMessage` 의 `time` 처럼 말풍선에 붙는 시각은 `오후 5:52` 로 쓴다. 여기서 시각은 정렬하거나 크기를 비교하는 값이 아니라 대화의 흐름을 짚는 표시라, 자릿수를 맞춘 `17:52` 보다 말하듯 읽히는 표기가 맞다. **이 예외는 대화 말풍선에만 열린다** — 같은 화면이라도 표·상세·목록에 들어가는 일시는 그대로 `YYYY-MM-DD HH:mm` 이다.
|
|
142
|
+
|
|
141
143
|
---
|
|
142
144
|
|
|
143
145
|
## 2. 조합 규칙 — 화면을 어떻게 쌓는가
|
|
@@ -151,7 +153,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
151
153
|
| 층 | 무엇인가 | 컴포넌트 |
|
|
152
154
|
| --- | --- | --- |
|
|
153
155
|
| **셸** | 앱 전체 뼈대. 페이지가 바뀌어도 남는다 | `SLayout` `SGnb` `SGnbSystem`(GNB 맨 아래 판 · 전폭 상단바 오른쪽 끝) `SPage` `SPageHeader`(`SLayout` 안에서 `SPage` 앞에 둔다) |
|
|
154
|
-
| **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SForm` `SSplitter` `SScrollArea` `SCalendarBoard` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
|
|
156
|
+
| **블록** | `SPage` 의 직계 자식. 페이지를 세로로 쌓는 단위 | `SSectionHeaderCard` `SCard` `SChatMessage` `SChatInput` `SLoginCard`(유일하게 `SPage` 밖에 선다 — 로그인 화면 자체가 자기 자리다, §4-6) `SForm` `SSplitter` `SScrollArea` `SCalendarBoard` `STable` `STableBar` `SChipFilter` `SKeyValueTable` `SList` `SExpansionList` `SDraggableList` `STree` `SCallout` `STabs` `SStepper` `SPagination` `SDivider` |
|
|
155
157
|
| **요소** | 블록 **안에** 놓이는 컨트롤. 혼자 페이지에 서지 않는다 | `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` `SImage` `SLinearProgress` `SCircleProgress` |
|
|
156
158
|
| **인라인** | 텍스트 흐름·셀·라벨 안에 섞인다. 혼자 블록이 되지 않는다 | `STag` `SBadge` `SIcon` `STextLink` `SChip` |
|
|
157
159
|
| **레이어** | 문서 흐름 **밖**에 떠서 그려진다. 어느 층에서 띄우든 레이아웃에 영향이 없다 | `SModal` `SActionModal` `SConfirmModal` `SPopup` `SDrawer` `SPopover` `STooltip` `SPortal` `SAccountListBox`(계정 행에 붙어 뜬다 — 직접 띄우지 않는다) `SToast` `SLoadingModal` `SLoadingContainer` `SGuide` |
|
|
@@ -474,6 +476,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
474
476
|
| --- | --- | --- |
|
|
475
477
|
| 한 줄 텍스트를 받는다 | `SInput` | §3-7-1 |
|
|
476
478
|
| 여러 줄 텍스트를 받는다 | `STextarea` | §3-7-1 |
|
|
479
|
+
| 대화 화면 아래에서 메시지를 입력받는다 | `SChatInput` | |
|
|
477
480
|
| 제목·굵게·목록·색 같은 **서식이 남아야 하는** 글을 받는다 | `SEditor` | §3-7-1 |
|
|
478
481
|
| 목록·결과를 검색어로 좁힌다 | `SSearchInput` | §3-7-1 |
|
|
479
482
|
| 숫자(수량·금액)를 받는다 | `SNumberInput` | §3-7-13 |
|
|
@@ -509,6 +512,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
509
512
|
| 여러 건을 여러 열로 보여주고 열끼리 비교하게 한다 | `STable` | §3-7-6 |
|
|
510
513
|
| 항목 하나의 속성들을 `라벨: 값` 으로 보여준다 | `SKeyValueTable` | §4-4 |
|
|
511
514
|
| 한 줄로 읽히는 항목을 세로로 나열한다 | `SList` + `SListItem` | §3-7-6 |
|
|
515
|
+
| 대화의 메시지 한 건을 보여준다 | `SChatMessage` | |
|
|
512
516
|
| 나열한 항목을 펼쳐 하위 내용을 보여준다 | `SExpansionList` + `SExpansionItem` | §3-7-7 |
|
|
513
517
|
| 부모-자식 계층을 들여쓰기로 보여준다 | `STree` | §3-7-7 |
|
|
514
518
|
| 사용자가 순서를 드래그로 바꾸게 한다 | `SDraggableList` + `SDraggableItem` | §3-7-6 |
|
|
@@ -542,6 +546,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
|
|
|
542
546
|
| 페이지 제목(+ 서브 텍스트·뒤로가기·우측 슬롯)을 만든다 | `SPageHeader` (`SLayout` 안에서 `SPage` 앞에 둔다) | §4-1 |
|
|
543
547
|
| 제목 있는 섹션으로 묶는다 | `SSectionHeaderCard` | §3-7-8 |
|
|
544
548
|
| 제목 없이 흰 면으로만 묶는다 | `SCard` | §3-7-8 |
|
|
549
|
+
| 통합 계정 로그인 화면을 만든다 | `SLoginCard` | §4-6 |
|
|
545
550
|
| 가로선으로 끊는다 | `SDivider` | §3-6 |
|
|
546
551
|
| 사용자가 영역 크기를 조절하게 한다 | `SSplitter` | §3-6 |
|
|
547
552
|
| 특정 영역 안에서만 스크롤시킨다 | `SScrollArea` | |
|
|
@@ -774,6 +779,8 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
|
|
|
774
779
|
|
|
775
780
|
**하단 버튼을 본문(children)에 직접 두지 않는다.** 주 액션은 `button`, 보조 버튼은 `footerLeft` 로 넘긴다 — 푸터 배경·여백·양끝 분리가 컴포넌트 규칙대로 잡히는 자리다. `button` 은 클릭해도 **모달을 닫지 않으므로**(`onClick` 만 발화) 저장 API 응답을 보고 `modalRef.ok()` 로 닫으면 되고, 그 때문에 본문에 버튼을 따로 둘 이유가 없다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며 `size="md"` 를 명시한다(§3-5-2).
|
|
776
781
|
|
|
782
|
+
**`button` 은 `SButton` 의 prop 을 그대로 받는다** — `icon`·`rightIcon`·`outline`·`disabled` 등을 함께 넘길 수 있다. 단 **`size` 는 타입에 없다**: 하단 액션 영역은 언제나 `md` 이고(§3-5-2) 예외가 없어 푸터가 고정한다. **`color` 는 주지 않는 것이 기본**이다 — 주지 않으면 주 액션 규칙대로 `primary` 가 되고, 그 버튼이 파괴적 액션일 때만 §3-5-3 에 따라 `danger` 를 준다. 카드(§3-7-8)의 `button` 도 같다.
|
|
783
|
+
|
|
777
784
|
**모달 안에서도 앱의 훅을 그냥 쓴다 — 단, 앱 루트에 `SModalOutlet` 이 있어야 한다 (§4-1).** outlet 이 있으면 명령형 모달이 앱 렌더 트리의 자식으로 그려지므로 `useQuery`·`useNavigate`·`useTheme` 같은 Context 기반 훅이 페이지에서와 똑같이 동작한다. **모달 컴포넌트를 Provider 로 다시 감싸지 않는다.** outlet 없이 띄우면 모달이 별도 React 루트로 떠서 Provider 가 하나도 닿지 않고, `No QueryClient set` 처럼 모달을 여는 순간에만 터진다.
|
|
778
785
|
|
|
779
786
|
#### 3-3-5. 닫기 경로 — `persistent` 기본값은 컴포넌트마다 다르다
|
|
@@ -1324,6 +1331,10 @@ const columns: STableColumn[] = [
|
|
|
1324
1331
|
|
|
1325
1332
|
폼 필드 둘은 **줄 수가 아니라 값의 성격으로** 갈린다. 값이 길어질 수 있는데 `SInput` 을 쓰면 사용자가 자기가 쓴 것을 다시 읽지 못한다 — 한 줄 안에서 좌우로 스크롤해야 하기 때문이다. 반대로 짧은 값에 `STextarea` 를 쓰면 빈 공간이 남아 입력량을 잘못 기대하게 한다.
|
|
1326
1333
|
|
|
1334
|
+
**`STextarea` 의 기본 높이는 두 줄이다.** 크기는 `size`(`'sm'` 기본 · `'md'`)로 정하고, 한 줄 필드와 나란히 놓이는 자리라면 `SInput` 과 같은 등급으로 맞춘다 — 등급이 글자·행간·안쪽 여백·모서리를 함께 정하므로 섞으면 같은 줄에서 어긋나 보인다. **더 높게 쓰려면 `rows` 를 준다**(줄 수). 높이를 `style` 이나 `textareaStyle` 로 직접 주지 않는다 — 등급이 정하는 값이고, 사용자가 모서리를 끌어 늘릴 수 있다. `rows` 를 두 줄 아래로 줄여도 등급이 정한 높이 밑으로는 내려가지 않는다. 한 줄만 받을 자리면 `SInput` 이다.
|
|
1335
|
+
|
|
1336
|
+
**입력한 만큼 늘어나게 하려면 `autogrow` 다.** 스크롤 대신 필드가 자라므로 쓴 글을 한눈에 다시 읽을 수 있다. 켜면 모서리를 끌어 크기를 바꾸는 손잡이는 사라진다 — 끌어 둔 높이를 다음 타이핑이 도로 계산하기 때문이다. **모달·드로어·카드처럼 아래에 버튼이 있는 자리에서는 `maxRows` 를 반드시 함께 준다** — 상한이 없으면 긴 글에서 필드가 계속 자라 그 버튼을 화면 밖으로 밀어낸다. 상한에 닿으면 그 안에서 스크롤한다. 페이지 본문처럼 아래로 밀려도 괜찮은 자리라면 상한 없이 써도 된다.
|
|
1337
|
+
|
|
1327
1338
|
`SEditor` 는 **서식이 값의 일부일 때만** 쓴다. 값을 HTML 문자열로 주고받으므로 저장·검색·비교가 평문보다 비싸고, 화면에 다시 보여줄 때도 HTML 로 렌더해야 한다. 서식이 필요 없는 메모·사유는 `STextarea` 다 — "입력창이 커 보여서" 고르는 컴포넌트가 아니다. 반대로 공지·안내문·상품 상세처럼 **작성자가 정한 강조와 목록이 그대로 보여야 하는 글**이면 `STextarea` 로는 표현할 수 없다.
|
|
1328
1339
|
|
|
1329
1340
|
`SEditor` 도 `SInput`·`STextarea` 와 같은 폼 필드다 — `label`·`hint`·`rules`·`errorMessage` 를 자기 prop 으로 받고 `SForm` 제출 검증에도 들어간다. 빈 문서는 빈 문자열로 나오므로 `required` 규칙이 그대로 걸린다. **툴바를 끄는 길은 없다** — 서식 입력이 필요 없는 자리라면 서식 없는 `SEditor` 가 아니라 `STextarea` 를 고른다.
|
|
@@ -1369,6 +1380,57 @@ const columns: STableColumn[] = [
|
|
|
1369
1380
|
- 카드 안에 상태 배지를 넣으려면 `tag` 슬롯에 `STag` 를 준다. 라벨 문자열에 "(추천)" 처럼 섞어 쓰지 않는다.
|
|
1370
1381
|
- **카드처럼 생겼다고 `SCard`/`SSectionHeaderCard` 로 감싸지 않는다.** `SRadioCard` 자체가 완결된 요소이고, 나열 간격은 `SRadioCardGroup` 의 `direction` 이 맞춘다 (§2-2).
|
|
1371
1382
|
|
|
1383
|
+
##### 옵션이 수백~수천 개면 — `onReachEnd`
|
|
1384
|
+
|
|
1385
|
+
**렌더는 걱정하지 않아도 된다.** `SSelect` 는 언제나 보이는 범위의 행만 그린다 — 켜고 끄는 prop 이 없고, 옵션이 5개든 5,000개든 여는 비용이 같다. 행 높이가 균일하다고 가정하지도 않으므로 계층 목록이나 큰 글씨가 섞인 라벨도 그대로 넘기면 된다.
|
|
1386
|
+
|
|
1387
|
+
**남는 부담은 전달이다.** 수천 건을 한 번에 받아 오는 것 자체가 무거우면 페이지 단위로 받는다.
|
|
1388
|
+
|
|
1389
|
+
- **`onReachEnd`** — 목록 끝이 가까워지면 불린다. 다음 페이지를 받아 `options` **뒤에 이어붙인다**(갈아끼우지 않는다). `hasMore` · `loading` 을 함께 준다. 없으면 마지막 페이지 뒤로도 계속 청하거나, 받는 중에 같은 페이지를 두 번 청한다.
|
|
1390
|
+
- **`onReachEnd` 를 쓰면 검색도 서버로 넘긴다** — `serverSearch` 를 켜고 `onSearchChange` 로 온 검색어에 맞는 목록을 내려준다. 켜지 않으면 검색이 "지금까지 받은 페이지" 안에서만 걸러져, 아직 받지 않은 항목은 검색해도 나오지 않는다. 검색어가 바뀌면 첫 페이지부터 다시 받는다.
|
|
1391
|
+
- **`reachEndThreshold` 는 한 페이지 크기보다 충분히 작게 잡는다.** 한 페이지가 드롭다운을 채우고도 이 문턱만큼 남기지 못하면 페이지가 도착하는 족족 다음 페이지를 다시 청해, 사용자가 스크롤하지 않아도 목록 전체를 받아 온다 — 페이징을 한 의미가 사라진다. 기본값이면 대개 그대로 두면 된다.
|
|
1392
|
+
- **늦게 온 응답이 최신 목록을 덮지 않게 한다.** 검색어나 페이지가 바뀌면 앞선 요청은 버려야 한다 — `useEffect` 의 cleanup 에서 취소 플래그를 세우는 것이 정석이다. 빠뜨리면 빠르게 지나간 검색어의 결과가 화면에 남는다. **무한 로딩에서 가장 흔히 새는 곳이다.**
|
|
1393
|
+
- **`showSelectAll` 은 함께 쓰지 않는다.** 아직 받지 않은 옵션은 고를 수 없어 "전체"가 거짓이 된다. 함께 주면 무시하고 개발 모드에서 경고한다.
|
|
1394
|
+
- **`valueAsPrimitive` 를 켜지 않는다.** 기본값(옵션 객체)이면 고른 값이 라벨을 함께 들고 다녀, 그 옵션이 지금 페이지나 검색 결과에서 빠져도 트리거에 이름이 그대로 남는다. 원시값만 들고 있으면 그 자리에 코드가 뜬다.
|
|
1395
|
+
- **계층 목록도 페이지로 받을 수 있다.** 단 이어붙일 때 **이미 있는 그룹의 `children` 에 이어야** 한다 — 같은 그룹을 새 항목으로 또 밀어 넣으면 목록에 같은 헤더가 두 번 뜬다.
|
|
1396
|
+
|
|
1397
|
+
```tsx
|
|
1398
|
+
const [query, setQuery] = useState('');
|
|
1399
|
+
const [page, setPage] = useState(0);
|
|
1400
|
+
const [options, setOptions] = useState<SSelectOption[]>([]);
|
|
1401
|
+
|
|
1402
|
+
useEffect(() => {
|
|
1403
|
+
let cancelled = false; // 늦게 온 이전 요청이 최신 목록을 덮지 않게
|
|
1404
|
+
setLoading(true);
|
|
1405
|
+
fetchClients(query, page).then(res => {
|
|
1406
|
+
if (cancelled) return;
|
|
1407
|
+
setOptions(prev => (page === 0 ? res.items : [...prev, ...res.items])); // 갈아끼우지 않고 이어붙인다
|
|
1408
|
+
setHasMore(res.hasMore);
|
|
1409
|
+
setLoading(false);
|
|
1410
|
+
});
|
|
1411
|
+
return () => {
|
|
1412
|
+
cancelled = true;
|
|
1413
|
+
};
|
|
1414
|
+
}, [query, page]);
|
|
1415
|
+
|
|
1416
|
+
<SSelect
|
|
1417
|
+
label="거래처"
|
|
1418
|
+
width="lg"
|
|
1419
|
+
options={options}
|
|
1420
|
+
hasMore={hasMore}
|
|
1421
|
+
loading={loading}
|
|
1422
|
+
onReachEnd={() => setPage(p => p + 1)}
|
|
1423
|
+
showSearch
|
|
1424
|
+
serverSearch
|
|
1425
|
+
onSearchChange={q => {
|
|
1426
|
+
setPage(0); // 새 검색은 첫 페이지부터
|
|
1427
|
+
setQuery(q);
|
|
1428
|
+
}}
|
|
1429
|
+
value={value}
|
|
1430
|
+
onValueChange={setValue}
|
|
1431
|
+
/>;
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1372
1434
|
#### 3-7-3. 켜고 끄는 셋 — SCheckbox vs SSwitch vs SToggle
|
|
1373
1435
|
|
|
1374
1436
|
셋 다 on/off 지만 **값이 언제 반영되는지**가 다르다. 이걸 틀리면 사용자가 저장 버튼을 찾다가 못 찾거나, 눌렀는데 반영이 안 돼 다시 누른다.
|
|
@@ -2169,6 +2231,45 @@ export default function ProductDetailPage() {
|
|
|
2169
2231
|
|
|
2170
2232
|
본문 래퍼는 `className` 을 받지 않으므로(여백은 `padding` prop 으로만 받는다) 루트에서 내려 준다. **하단 액션 영역이 있으면 본문이 더 이상 마지막 자식이 아니다** — 그때는 `[&>div:nth-last-child(2)]` 로 겨눈다. 흔한 구성은 아니다 — 대부분은 `STable` 이 자기 안에서 스크롤하므로 여기까지 갈 일이 없다.
|
|
2171
2233
|
|
|
2234
|
+
### 4-6. 로그인 화면 — SLoginCard
|
|
2235
|
+
|
|
2236
|
+
앱 셸이 아직 없는 유일한 화면이다. `SLayout`·`SGnb`·`SPage` 가 없고, 회색 바탕 위에 카드 하나만 선다.
|
|
2237
|
+
|
|
2238
|
+
```tsx
|
|
2239
|
+
<div className="flex h-screen items-stretch justify-center bg-(--sys-color-bg-neutralLight) p-sd-24">
|
|
2240
|
+
<SLoginCard
|
|
2241
|
+
product="AI assistant"
|
|
2242
|
+
title="셀메이트 AI 어시스턴트와 함께 자료 찾는 시간은 줄이고, 업무는 빠르게!"
|
|
2243
|
+
description="(사내 답변은 사내 문서를 기반으로 답변합니다.)"
|
|
2244
|
+
onLogin={signIn}
|
|
2245
|
+
footerNote="계정이 없다면 담당자에게 문의를 요청해 주세요."
|
|
2246
|
+
/>
|
|
2247
|
+
</div>
|
|
2248
|
+
```
|
|
2249
|
+
|
|
2250
|
+
**바깥 한 겹은 앱이 만든다.** 카드는 화면을 잡지 않는다 — 덕분에 로그인 화면 밖(세션 만료 재인증 등)에서도 같은 카드를 그대로 쓴다. 그 한 겹은 위 세 가지만 하면 되고, 그 이상은 하지 않는다.
|
|
2251
|
+
|
|
2252
|
+
- 바탕은 `bg-(--sys-color-bg-neutralLight)`, 여백은 `p-sd-24`. **여백은 카드가 최소 폭(368px)을 지킬 수 있는 값으로 둔다** — 좌우 합이 창 폭에서 368px 을 남기지 못하면 카드가 넘친다
|
|
2253
|
+
- `items-stretch` — 카드가 남은 높이를 채운다. `items-center` 로 두면 카드가 내용 높이로 줄어든다
|
|
2254
|
+
- `justify-center` — 카드를 가로 가운데에
|
|
2255
|
+
|
|
2256
|
+
| Prop | 용도 |
|
|
2257
|
+
| --- | --- |
|
|
2258
|
+
| `product` | 로고 아래 제품명 (`AI assistant` · `account` · `chat`) |
|
|
2259
|
+
| `title` | 제품을 한 줄로 소개하는 문장 |
|
|
2260
|
+
| `description` | 소개 바로 아래 붙는 보조 설명. 소개와 한 문단으로 읽힌다 — 없으면 생략한다 |
|
|
2261
|
+
| `onLogin` | 로그인 버튼을 눌렀을 때. **버튼은 카드가 그리므로 동작만 넘긴다** — 문구·색·크기는 세 제품에서 같아야 한다 |
|
|
2262
|
+
| `footerNote` | 버튼 아래 안내 문구 |
|
|
2263
|
+
|
|
2264
|
+
**문구 말고는 주지 않는다.** 라운드·간격·타이포·버튼 색은 카드가 쥐고 있어 제품이 달라도 같은 화면이 선다.
|
|
2265
|
+
|
|
2266
|
+
**크기는 부모가 정한다 — `className` 으로 폭을 박지 않는다.** 카드는 폭을 368~640px 사이에서 부모에 맞추고 높이도 부모가 준 만큼 채운다. 밖에서 고정하면 상하한이 무의미해져, 좁은 화면에서 함께 좁아져야 할 카드가 그대로 남아 넘친다. 좁게 세워야 하면 카드가 아니라 바깥 한 겹을 좁힌다.
|
|
2267
|
+
|
|
2268
|
+
**Sellmate 워드마크와 로그인 버튼은 카드가 직접 그린다 — prop 이 없다.** 세 제품이 같은 로고·같은 버튼 문구(`통합 계정으로 로그인`)로 서므로 넘길 것도, 갈아 끼울 것도 없다. 앱이 주는 것은 문구와 `onLogin` 동작뿐이다.
|
|
2269
|
+
|
|
2270
|
+
**로그인 카드 옆에 다른 블록을 두지 않는다.** 이 화면에서 할 일은 로그인 하나이고, 옆에 공지·배너가 붙는 순간 그 뜻이 깨진다. 안내가 필요하면 `description` 이나 `footerNote` 로 넣는다.
|
|
2271
|
+
|
|
2272
|
+
|
|
2172
2273
|
---
|
|
2173
2274
|
|
|
2174
2275
|
## 5. 자가 점검 체크리스트
|
|
@@ -2209,6 +2310,7 @@ export default function ProductDetailPage() {
|
|
|
2209
2310
|
- [ ] 날짜를 `YYYY-MM-DD` 로 표기했는가 (`toLocaleDateString()` ❌, §1-4)
|
|
2210
2311
|
- [ ] 필드 폭을 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`)으로 줬는가 — px 직접 지정 ❌ (§4-3)
|
|
2211
2312
|
- [ ] 조회 조건으로 쓰는 셀렉트·날짜·시간 피커에 `clearable` 을 줬는가, 그 상태가 `null` 을 담을 수 있는가 (§3-7-4 — 필수 입력 필드에는 켜지 않는다)
|
|
2313
|
+
- [ ] 서버에서 페이지 단위로 받는 `SSelect` 에 `onReachEnd` 와 `hasMore`·`loading`·`serverSearch` 를 함께 줬는가, 늦게 온 응답을 버리는 cleanup 이 있는가 (§3-7-2 — 렌더 최적화는 DS 가 알아서 한다)
|
|
2212
2314
|
- [ ] `SKeyValueTable` 의 짧은 행에 `tdColSpan` 을 주어 전체 열 수를 채웠는가 (§4-3 — 안 채우면 그 구간의 행 구분선이 끊긴다)
|
|
2213
2315
|
- [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가
|
|
2214
2316
|
- [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
|
|
@@ -2219,6 +2321,7 @@ export default function ProductDetailPage() {
|
|
|
2219
2321
|
- [ ] 창을 띄울 때 §3-3-1 판별 순서를 따랐는가 (그 자체가 화면 → `SPopup` / 실행 여부만 확정 → `SModal.confirm` / 모달 안에서 작성 → `SActionModal`)
|
|
2220
2322
|
- [ ] 작업용 모달을 `SActionModal` + `SModal.create` 로 만들었는가 (직접 오버레이 ❌)
|
|
2221
2323
|
- [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4), 카드 안에서 닫히는 액션도 같은 prop 으로 넘겼는가 (§3-7-8)
|
|
2324
|
+
- [ ] 아래에 버튼이 있는 자리(모달·드로어·카드)의 `STextarea autogrow` 에 `maxRows` 를 함께 줬는가 (§3-7-1 — 없으면 긴 글이 버튼을 화면 밖으로 밀어낸다)
|
|
2222
2325
|
- [ ] 앱 부트스트랩의 Provider 안쪽에 `<SModalOutlet />` 이 한 번 렌더되어 있는가 (§4-1 — 없으면 모달 안에서 앱 훅이 죽는다), 그 대신으로 모달 컴포넌트를 Provider 로 다시 감싸지 않았는가
|
|
2223
2326
|
- [ ] 고른 컴포넌트를 §2-0 의 제 층에 놓았는가 (요소를 `SPage` 에 직접 놓지 않았는가, 블록을 `div` 로 감싸지 않았는가)
|
|
2224
2327
|
- [ ] §2-0 포함 규칙을 지켰는가 (카드 안 카드 ❌, 표 셀 안 블록 ❌)
|
|
@@ -60,6 +60,7 @@ export const BUTTON_SIZES = ['xs', 'sm', 'md', 'lg'] as const;
|
|
|
60
60
|
- [SFooter](../SFooter)
|
|
61
61
|
- [SKeyValueTable](../SKeyValueTable)
|
|
62
62
|
- [SLoadingModal](../SLoadingModal)
|
|
63
|
+
- [SLoginCard](../SLoginCard)
|
|
63
64
|
- [SPopover](../SPopover)
|
|
64
65
|
- [SToast](../SToast)
|
|
65
66
|
|
|
@@ -79,6 +80,7 @@ graph TD;
|
|
|
79
80
|
SFooter --> SButton
|
|
80
81
|
SKeyValueTable --> SButton
|
|
81
82
|
SLoadingModal --> SButton
|
|
83
|
+
SLoginCard --> SButton
|
|
82
84
|
SPopover --> SButton
|
|
83
85
|
SToast --> SButton
|
|
84
86
|
style SButton fill:#f9f,stroke:#333,stroke-width:4px
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# SChatInput
|
|
2
|
+
|
|
3
|
+
> 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
|
|
4
|
+
|
|
5
|
+
### SChatInput
|
|
6
|
+
|
|
7
|
+
#### Props
|
|
8
|
+
|
|
9
|
+
| Prop | Type | Default | Description |
|
|
10
|
+
|------|------|---------|-------------|
|
|
11
|
+
| `value?` | `string` | `''` | 입력값 (제어) |
|
|
12
|
+
| `placeholder?` | `string` | `DEFAULT_PLACEHOLDER` | 안내 문구. 세 제품이 같은 말로 서도록 기본값을 쥐고 있다 |
|
|
13
|
+
| `disabled?` | `boolean` | `false` | 비활성 |
|
|
14
|
+
| `busy?` | `boolean` | `false` | 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 — 답이 오는 동안 또 보내지 못하게 막는 자리다. |
|
|
15
|
+
| `footerAction?` | `ReactNode` | — | 입력창 아래 보조 액션 슬롯. 없으면 그 줄이 통째로 빠진다. 상담 종료처럼 **대화 자체를 끝내는, 전송과 층이 다른 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 입력창은 가운데에 세우는 것까지만 한다 — `STextLink` 를 넣는 것이 보통이다. |
|
|
16
|
+
| `maxRows?` | `number` | `DEFAULT_MAX_ROWS` | 입력창이 자랄 수 있는 최대 줄 수. 여기 닿으면 더 자라지 않고 그 안에서 스크롤한다. |
|
|
17
|
+
| `className?` | `string` | — | 바깥 컨테이너 className (안쪽 textarea 가 아니다) |
|
|
18
|
+
| `style?` | `CSSProperties` | — | 바깥 컨테이너 style |
|
|
19
|
+
| `textareaClassName?` | `string` | — | 안쪽 textarea className |
|
|
20
|
+
|
|
21
|
+
#### Events
|
|
22
|
+
|
|
23
|
+
| Event | Type | Description |
|
|
24
|
+
|-------|------|-------------|
|
|
25
|
+
| `onValueChange` | `(value: string) => void` | 값 변경 |
|
|
26
|
+
| `onSend` | `(value: string) => void` | 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다. **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다. |
|
|
27
|
+
| `onAttach` | `() => void` | 파일 첨부. **주면 입력창 왼쪽에 첨부 버튼이 선다** |
|
|
28
|
+
| `onStop` | `() => void` | 정지. `busy` 일 때 정지 버튼을 누르면 불린다 |
|
|
29
|
+
|
|
30
|
+
## Dependencies
|
|
31
|
+
|
|
32
|
+
### Depends on
|
|
33
|
+
|
|
34
|
+
- [SGhostButton](../SGhostButton)
|
|
35
|
+
- [STextarea](../STextarea)
|
|
36
|
+
|
|
37
|
+
### Graph
|
|
38
|
+
|
|
39
|
+
```mermaid
|
|
40
|
+
graph TD;
|
|
41
|
+
SChatInput --> SGhostButton
|
|
42
|
+
SChatInput --> STextarea
|
|
43
|
+
style SChatInput fill:#f9f,stroke:#333,stroke-width:4px
|
|
44
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode, type TextareaHTMLAttributes } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 안쪽 textarea 로 그대로 흘러가는 네이티브 속성 — `SInput`·`STextarea` 와 같은 방식이다.
|
|
4
|
+
* 컴포넌트가 직접 다루는 것만 뺀다.
|
|
5
|
+
*/
|
|
6
|
+
export interface SChatInputProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange' | 'placeholder' | 'disabled' | 'rows' | 'className' | 'style'> {
|
|
7
|
+
/** 입력값 (제어) */
|
|
8
|
+
value?: string;
|
|
9
|
+
/** 값 변경 */
|
|
10
|
+
onValueChange?: (value: string) => void;
|
|
11
|
+
/** 안내 문구. 세 제품이 같은 말로 서도록 기본값을 쥐고 있다 */
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
/** 비활성 */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다.
|
|
17
|
+
*
|
|
18
|
+
* **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다.
|
|
19
|
+
*/
|
|
20
|
+
onSend?: (value: string) => void;
|
|
21
|
+
/** 파일 첨부. **주면 입력창 왼쪽에 첨부 버튼이 선다** */
|
|
22
|
+
onAttach?: () => void;
|
|
23
|
+
/**
|
|
24
|
+
* 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 —
|
|
25
|
+
* 답이 오는 동안 또 보내지 못하게 막는 자리다.
|
|
26
|
+
*/
|
|
27
|
+
busy?: boolean;
|
|
28
|
+
/** 정지. `busy` 일 때 정지 버튼을 누르면 불린다 */
|
|
29
|
+
onStop?: () => void;
|
|
30
|
+
/**
|
|
31
|
+
* 입력창 아래 보조 액션 슬롯. 없으면 그 줄이 통째로 빠진다.
|
|
32
|
+
*
|
|
33
|
+
* 상담 종료처럼 **대화 자체를 끝내는, 전송과 층이 다른 동작**이 오는 자리다. 무엇을 놓을지는
|
|
34
|
+
* 앱이 정하고, 입력창은 가운데에 세우는 것까지만 한다 — `STextLink` 를 넣는 것이 보통이다.
|
|
35
|
+
*/
|
|
36
|
+
footerAction?: ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* 입력창이 자랄 수 있는 최대 줄 수. 여기 닿으면 더 자라지 않고 그 안에서 스크롤한다.
|
|
39
|
+
*/
|
|
40
|
+
maxRows?: number;
|
|
41
|
+
/** 바깥 컨테이너 className (안쪽 textarea 가 아니다) */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** 바깥 컨테이너 style */
|
|
44
|
+
style?: CSSProperties;
|
|
45
|
+
/** 안쪽 textarea className */
|
|
46
|
+
textareaClassName?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* SChatInput — 대화 화면 맨 아래에 서는 입력창.
|
|
50
|
+
*
|
|
51
|
+
* 첨부·입력·전송을 한 줄로 세우고, 필요하면 그 아래 보조 액션(상담 종료 등)을 붙인다.
|
|
52
|
+
* Enter 로 보내고 Shift+Enter 로 줄을 바꾼다 — 한글 조합 중의 Enter 는 보내지 않는다.
|
|
53
|
+
*
|
|
54
|
+
* **글이 길어지면 입력창이 자란다** — `maxRows` 줄까지 늘어나고, 거기 닿으면 그 안에서
|
|
55
|
+
* 스크롤한다. 상한이 없으면 대화가 화면 밖으로 밀려난다.
|
|
56
|
+
*
|
|
57
|
+
* **응답을 기다리는 동안에는 `busy` 를 켠다** — 전송 버튼이 정지 버튼으로 바뀌고 Enter 전송이
|
|
58
|
+
* 잠겨, 답이 오는 중에 또 보내는 일이 막힌다.
|
|
59
|
+
*/
|
|
60
|
+
export declare const SChatInput: import("react").ForwardRefExoticComponent<SChatInputProps & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SChatInput, type SChatInputProps } from './SChatInput';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SChatMessage
|
|
2
|
+
|
|
3
|
+
> 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
|
|
4
|
+
|
|
5
|
+
### SChatMessage
|
|
6
|
+
|
|
7
|
+
#### Props
|
|
8
|
+
|
|
9
|
+
| Prop | Type | Default | Description |
|
|
10
|
+
|------|------|---------|-------------|
|
|
11
|
+
| `direction` | `SChatMessageDirection` | `'incoming'` | 받은 메시지(왼쪽)인지 보낸 메시지(오른쪽)인지. **아바타와 이름 유무도 여기서 갈린다** — 받은 메시지에만 아바타와 `sender` 가 선다. 보낸 메시지는 내가 보냈다는 것이 자리로 이미 드러나 얼굴도 이름도 필요 없다. |
|
|
12
|
+
| `sender?` | `ReactNode` | — | 말풍선 위에 붙는 보낸 사람 이름. 없으면 자리째 빠진다. **받은 메시지에만 쓰인다** — 보낸 메시지에는 넘겨도 그리지 않는다(`direction` 참고). 앱이 데이터를 한 번만 매핑해 방향과 무관하게 넘길 수 있도록 막지 않고 무시한다. |
|
|
13
|
+
| `time?` | `ReactNode` | — | 말풍선 아래에 붙는 시각. 표기는 앱이 정해 문자열로 넘긴다 — 대화 화면의 시각은 `오후 5:52` 처럼 읽는 표기를 쓰므로 카드가 서식을 강제하지 않는다 (AGENTS.md §1-4 예외). |
|
|
14
|
+
| `readStatus?` | `SChatMessageReadStatus` | — | 상대가 읽었는지. 시각 왼쪽에 `읽음`·`안 읽음` 으로 붙고, 주지 않으면 표시하지 않는다. **내가 보낸 메시지에만 붙인다** — 받은 메시지에 읽음 표시를 다는 것은 뜻이 없다. |
|
|
15
|
+
| `avatarSrc?` | `string` | — | 아바타 이미지 경로. 없으면 사람 아이콘 자리표시가 선다. **받은 메시지에만 쓰인다** — 보낸 메시지에는 아바타가 서지 않는다(`direction` 참고). |
|
|
16
|
+
| `avatarAlt?` | `string` | — | 아바타 대체 텍스트 |
|
|
17
|
+
| `continued?` | `boolean` | `false` | 같은 사람이 이어서 말한 묶음인지. 켜면 **아바타와 `sender` 를 그리지 않는다** — 말풍선이 위 묶음과 같은 선에서 시작해 한 사람의 말로 이어 읽힌다. 받은 메시지에서는 아바타 자리를 비워 둔 채 남긴다 — 안 남기면 말풍선이 44px 왼쪽으로 밀려 위 묶음과 어긋난다. 보낸 메시지는 아바타가 없으므로 이름만 숨긴다. `time`·`readStatus` 는 그대로 나온다. 시간이 벌어져 묶음을 나눴어도 얼굴은 한 번만 보이면 되고 시각은 묶음마다 필요하기 때문이다. **얼마나 벌어져야 묶음을 나눌지는 앱이 정한다** — 30분이든 하루든 대화의 성격에 달렸으므로 컴포넌트는 기준을 갖지 않고 스위치만 연다. |
|
|
18
|
+
| `variant?` | `SChatMessageVariant` | `'default'` | 말풍선 종류. `template` 이면 말풍선이 옅은 파랑 면으로 서서 미리 등록된 서식으로 보내진 메시지임을 드러낸다. **`direction` 과 별개 축이라** 받은 메시지에도 보낸 메시지에도 붙는다. |
|
|
19
|
+
| `actions?` | `ReactNode` | — | 시각 아래에 붙는 동작 슬롯. 없으면 그 줄이 통째로 빠진다. 「상담원 연결」처럼 **이 메시지에 이어서 할 수 있는 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 메시지는 줄로 세우고 방향에 맞춰 정렬하는 것까지만 한다. 시각과 같은 선에서 시작한다 — 말풍선 끝에서 한 칸 안이다. |
|
|
20
|
+
| `messages?` | `ReactNode[]` | — | 말풍선 목록. **한 항목이 말풍선 하나**가 되어 위에서 아래로 쌓인다. 한 사람이 연달아 말하면 아바타·이름·시각은 한 번만 서고 말풍선만 늘어난다 — 그 묶음이 이 컴포넌트 한 개다. 말풍선 안에는 글뿐 아니라 버튼 같은 요소도 올 수 있다. |
|
|
21
|
+
|
|
22
|
+
## Types
|
|
23
|
+
|
|
24
|
+
### SChatMessageDirection
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
export type SChatMessageDirection = (typeof CHAT_MESSAGE_DIRECTIONS)[number];
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### SChatMessageReadStatus
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export type SChatMessageReadStatus = (typeof CHAT_MESSAGE_READ_STATUSES)[number];
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### SChatMessageVariant
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
export type SChatMessageVariant = (typeof CHAT_MESSAGE_VARIANTS)[number];
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### CHAT_MESSAGE_DIRECTIONS
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
/** 메시지가 오는 쪽 — 받은 메시지(incoming)는 왼쪽, 보낸 메시지(outgoing)는 오른쪽에 선다. */
|
|
46
|
+
export const CHAT_MESSAGE_DIRECTIONS = ['incoming', 'outgoing'] as const;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### CHAT_MESSAGE_READ_STATUSES
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
/** 상대가 내 메시지를 읽었는지. 시각 왼쪽에 붙는다. */
|
|
53
|
+
export const CHAT_MESSAGE_READ_STATUSES = ['read', 'unread'] as const;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### CHAT_MESSAGE_VARIANTS
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
/**
|
|
60
|
+
* 말풍선의 종류. `template` 은 미리 등록된 서식으로 보내진 메시지다.
|
|
61
|
+
*
|
|
62
|
+
* **`direction` 과 별개 축이다** — 받은 메시지도 보낸 메시지도 템플릿일 수 있다.
|
|
63
|
+
*/
|
|
64
|
+
export const CHAT_MESSAGE_VARIANTS = ['default', 'template'] as const;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Dependencies
|
|
68
|
+
|
|
69
|
+
### Depends on
|
|
70
|
+
|
|
71
|
+
- [SIcon](../SIcon)
|
|
72
|
+
- [SImage](../SImage)
|
|
73
|
+
|
|
74
|
+
### Graph
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
graph TD;
|
|
78
|
+
SChatMessage --> SIcon
|
|
79
|
+
SChatMessage --> SImage
|
|
80
|
+
style SChatMessage fill:#f9f,stroke:#333,stroke-width:4px
|
|
81
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
/** 메시지가 오는 쪽 — 받은 메시지(incoming)는 왼쪽, 보낸 메시지(outgoing)는 오른쪽에 선다. */
|
|
3
|
+
export declare const CHAT_MESSAGE_DIRECTIONS: readonly ["incoming", "outgoing"];
|
|
4
|
+
export type SChatMessageDirection = (typeof CHAT_MESSAGE_DIRECTIONS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* 말풍선의 종류. `template` 은 미리 등록된 서식으로 보내진 메시지다.
|
|
7
|
+
*
|
|
8
|
+
* **`direction` 과 별개 축이다** — 받은 메시지도 보낸 메시지도 템플릿일 수 있다.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CHAT_MESSAGE_VARIANTS: readonly ["default", "template"];
|
|
11
|
+
export type SChatMessageVariant = (typeof CHAT_MESSAGE_VARIANTS)[number];
|
|
12
|
+
/** 상대가 내 메시지를 읽었는지. 시각 왼쪽에 붙는다. */
|
|
13
|
+
export declare const CHAT_MESSAGE_READ_STATUSES: readonly ["read", "unread"];
|
|
14
|
+
export type SChatMessageReadStatus = (typeof CHAT_MESSAGE_READ_STATUSES)[number];
|
|
15
|
+
export interface SChatMessageProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
16
|
+
/**
|
|
17
|
+
* 받은 메시지(왼쪽)인지 보낸 메시지(오른쪽)인지.
|
|
18
|
+
*
|
|
19
|
+
* **아바타와 이름 유무도 여기서 갈린다** — 받은 메시지에만 아바타와 `sender` 가 선다.
|
|
20
|
+
* 보낸 메시지는 내가 보냈다는 것이 자리로 이미 드러나 얼굴도 이름도 필요 없다.
|
|
21
|
+
*/
|
|
22
|
+
direction: SChatMessageDirection;
|
|
23
|
+
/**
|
|
24
|
+
* 말풍선 위에 붙는 보낸 사람 이름. 없으면 자리째 빠진다.
|
|
25
|
+
*
|
|
26
|
+
* **받은 메시지에만 쓰인다** — 보낸 메시지에는 넘겨도 그리지 않는다(`direction` 참고).
|
|
27
|
+
* 앱이 데이터를 한 번만 매핑해 방향과 무관하게 넘길 수 있도록 막지 않고 무시한다.
|
|
28
|
+
*/
|
|
29
|
+
sender?: ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* 말풍선 아래에 붙는 시각. 표기는 앱이 정해 문자열로 넘긴다 — 대화 화면의 시각은
|
|
32
|
+
* `오후 5:52` 처럼 읽는 표기를 쓰므로 카드가 서식을 강제하지 않는다 (AGENTS.md §1-4 예외).
|
|
33
|
+
*/
|
|
34
|
+
time?: ReactNode;
|
|
35
|
+
/**
|
|
36
|
+
* 상대가 읽었는지. 시각 왼쪽에 `읽음`·`안 읽음` 으로 붙고, 주지 않으면 표시하지 않는다.
|
|
37
|
+
*
|
|
38
|
+
* **내가 보낸 메시지에만 붙인다** — 받은 메시지에 읽음 표시를 다는 것은 뜻이 없다.
|
|
39
|
+
*/
|
|
40
|
+
readStatus?: SChatMessageReadStatus;
|
|
41
|
+
/**
|
|
42
|
+
* 아바타 이미지 경로. 없으면 사람 아이콘 자리표시가 선다.
|
|
43
|
+
*
|
|
44
|
+
* **받은 메시지에만 쓰인다** — 보낸 메시지에는 아바타가 서지 않는다(`direction` 참고).
|
|
45
|
+
*/
|
|
46
|
+
avatarSrc?: string;
|
|
47
|
+
/** 아바타 대체 텍스트 */
|
|
48
|
+
avatarAlt?: string;
|
|
49
|
+
/**
|
|
50
|
+
* 같은 사람이 이어서 말한 묶음인지. 켜면 **아바타와 `sender` 를 그리지 않는다** —
|
|
51
|
+
* 말풍선이 위 묶음과 같은 선에서 시작해 한 사람의 말로 이어 읽힌다.
|
|
52
|
+
*
|
|
53
|
+
* 받은 메시지에서는 아바타 자리를 비워 둔 채 남긴다 — 안 남기면 말풍선이 44px 왼쪽으로 밀려
|
|
54
|
+
* 위 묶음과 어긋난다. 보낸 메시지는 아바타가 없으므로 이름만 숨긴다.
|
|
55
|
+
*
|
|
56
|
+
* `time`·`readStatus` 는 그대로 나온다. 시간이 벌어져 묶음을 나눴어도 얼굴은 한 번만 보이면
|
|
57
|
+
* 되고 시각은 묶음마다 필요하기 때문이다.
|
|
58
|
+
*
|
|
59
|
+
* **얼마나 벌어져야 묶음을 나눌지는 앱이 정한다** — 30분이든 하루든 대화의 성격에 달렸으므로
|
|
60
|
+
* 컴포넌트는 기준을 갖지 않고 스위치만 연다.
|
|
61
|
+
*/
|
|
62
|
+
continued?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* 말풍선 종류. `template` 이면 말풍선이 옅은 파랑 면으로 서서 미리 등록된 서식으로 보내진
|
|
65
|
+
* 메시지임을 드러낸다. **`direction` 과 별개 축이라** 받은 메시지에도 보낸 메시지에도 붙는다.
|
|
66
|
+
*/
|
|
67
|
+
variant?: SChatMessageVariant;
|
|
68
|
+
/**
|
|
69
|
+
* 시각 아래에 붙는 동작 슬롯. 없으면 그 줄이 통째로 빠진다.
|
|
70
|
+
*
|
|
71
|
+
* 「상담원 연결」처럼 **이 메시지에 이어서 할 수 있는 동작**이 오는 자리다. 무엇을 놓을지는
|
|
72
|
+
* 앱이 정하고, 메시지는 줄로 세우고 방향에 맞춰 정렬하는 것까지만 한다.
|
|
73
|
+
*
|
|
74
|
+
* 시각과 같은 선에서 시작한다 — 말풍선 끝에서 한 칸 안이다.
|
|
75
|
+
*/
|
|
76
|
+
actions?: ReactNode;
|
|
77
|
+
/**
|
|
78
|
+
* 말풍선 목록. **한 항목이 말풍선 하나**가 되어 위에서 아래로 쌓인다.
|
|
79
|
+
*
|
|
80
|
+
* 한 사람이 연달아 말하면 아바타·이름·시각은 한 번만 서고 말풍선만 늘어난다 —
|
|
81
|
+
* 그 묶음이 이 컴포넌트 한 개다. 말풍선 안에는 글뿐 아니라 버튼 같은 요소도 올 수 있다.
|
|
82
|
+
*/
|
|
83
|
+
messages?: ReactNode[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* SChatMessage — 대화 화면에서 한 사람이 한 번에 말한 묶음.
|
|
87
|
+
*
|
|
88
|
+
* 아바타·보낸 사람(받은 메시지만)·말풍선·시각을 한 줄로 세운다. **말풍선은 `messages` 항목 수만큼 쌓이고**
|
|
89
|
+
* 아바타·이름·시각은 그 묶음에 한 번만 붙는다 — 연달아 말한 여러 줄이 한 덩어리로 읽힌다.
|
|
90
|
+
*
|
|
91
|
+
* 시각 아래에 동작을 붙여야 하면 `actions` 슬롯에 넣는다 — 시각과 같은 선에서 시작해 방향에
|
|
92
|
+
* 맞춰 정렬된다.
|
|
93
|
+
*
|
|
94
|
+
* 시간이 벌어져 묶음을 나눠야 하는데 말한 사람은 같을 때는 `continued` 를 켠다 — 아바타 자리는
|
|
95
|
+
* 유지한 채 얼굴과 이름만 빠져, 두 묶음이 한 사람의 말로 이어 읽힌다.
|
|
96
|
+
`direction` 하나로 받은 메시지(왼쪽)와
|
|
97
|
+
* 보낸 메시지(오른쪽)가 갈리고, 그에 따라 정렬과 말풍선 색이 함께 뒤집힌다.
|
|
98
|
+
*
|
|
99
|
+
* **말풍선 폭은 내용이 정한다** — 짧은 메시지는 글자만큼만 차지하고, 길면 부모 폭에서 접힌다.
|
|
100
|
+
* 대화 폭을 제한하려면 이 컴포넌트가 아니라 담는 쪽을 좁힌다.
|
|
101
|
+
*/
|
|
102
|
+
export declare const SChatMessage: import("react").ForwardRefExoticComponent<SChatMessageProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SChatMessage, CHAT_MESSAGE_DIRECTIONS, CHAT_MESSAGE_READ_STATUSES, CHAT_MESSAGE_VARIANTS, type SChatMessageDirection, type SChatMessageReadStatus, type SChatMessageVariant, type SChatMessageProps, } from './SChatMessage';
|
|
@@ -27,14 +27,19 @@ export type SFooterBg = 'white' | 'grey';
|
|
|
27
27
|
### SFooterButton
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
/**
|
|
31
|
+
* footer 우측 기본 액션 버튼 설정 — `SButton` 의 prop 을 그대로 받는다.
|
|
32
|
+
*
|
|
33
|
+
* 골라 담은 부분집합으로 두지 않는 이유: `SButton` 에 prop 이 늘 때마다 이 목록만 조용히
|
|
34
|
+
* 뒤처져, 아이콘 하나 붙이려 해도 footer 에서는 길이 없는 상태가 된다.
|
|
35
|
+
*
|
|
36
|
+
* **`size` 만 뺀다.** 버튼 크기는 놓이는 위치가 정하고 하단 액션 영역은 언제나 `md` 다
|
|
37
|
+
* (AGENTS.md §3-5-2 — 예외가 없는 규칙이라 문서가 아니라 타입이 지킨다).
|
|
38
|
+
*
|
|
39
|
+
* `label`·`color` 는 주지 않으면 footer 가 주 액션 기본값(`'확인'`·`'primary'`)으로 잡는다.
|
|
40
|
+
* 파괴적 액션이라 `danger` 가 필요한 경우는 열어 둔다 (§3-5-3).
|
|
41
|
+
*/
|
|
42
|
+
export type SFooterButton = Omit<SButtonProps, 'size'>;
|
|
38
43
|
```
|
|
39
44
|
|
|
40
45
|
## Dependencies
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
-
import { type
|
|
2
|
+
import { type SButtonProps } from '../SButton';
|
|
3
3
|
export type SFooterBg = 'white' | 'grey';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
/**
|
|
5
|
+
* footer 우측 기본 액션 버튼 설정 — `SButton` 의 prop 을 그대로 받는다.
|
|
6
|
+
*
|
|
7
|
+
* 골라 담은 부분집합으로 두지 않는 이유: `SButton` 에 prop 이 늘 때마다 이 목록만 조용히
|
|
8
|
+
* 뒤처져, 아이콘 하나 붙이려 해도 footer 에서는 길이 없는 상태가 된다.
|
|
9
|
+
*
|
|
10
|
+
* **`size` 만 뺀다.** 버튼 크기는 놓이는 위치가 정하고 하단 액션 영역은 언제나 `md` 다
|
|
11
|
+
* (AGENTS.md §3-5-2 — 예외가 없는 규칙이라 문서가 아니라 타입이 지킨다).
|
|
12
|
+
*
|
|
13
|
+
* `label`·`color` 는 주지 않으면 footer 가 주 액션 기본값(`'확인'`·`'primary'`)으로 잡는다.
|
|
14
|
+
* 파괴적 액션이라 `danger` 가 필요한 경우는 열어 둔다 (§3-5-3).
|
|
15
|
+
*/
|
|
16
|
+
export type SFooterButton = Omit<SButtonProps, 'size'>;
|
|
12
17
|
/**
|
|
13
18
|
* @internal
|
|
14
19
|
* 모달·드로어·팝업·카드 하단 영역 공통 props.
|
|
@@ -48,6 +48,7 @@ export type SGhostButtonIntent = 'default' | 'danger' | 'action' | 'subAction' |
|
|
|
48
48
|
- [SBarcodeInput](../SBarcodeInput)
|
|
49
49
|
- [SCalendar](../SCalendar)
|
|
50
50
|
- [SCalendarBoard](../SCalendarBoard)
|
|
51
|
+
- [SChatInput](../SChatInput)
|
|
51
52
|
- [SChip](../SChip)
|
|
52
53
|
- [SDatePicker](../SDatePicker)
|
|
53
54
|
- [SDateRangePicker](../SDateRangePicker)
|
|
@@ -81,6 +82,7 @@ graph TD;
|
|
|
81
82
|
SBarcodeInput --> SGhostButton
|
|
82
83
|
SCalendar --> SGhostButton
|
|
83
84
|
SCalendarBoard --> SGhostButton
|
|
85
|
+
SChatInput --> SGhostButton
|
|
84
86
|
SChip --> SGhostButton
|
|
85
87
|
SDatePicker --> SGhostButton
|
|
86
88
|
SDateRangePicker --> SGhostButton
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
- [SAccountListBox](../SAccountListBox)
|
|
24
24
|
- [SButton](../SButton)
|
|
25
25
|
- [SCallout](../SCallout)
|
|
26
|
+
- [SChatMessage](../SChatMessage)
|
|
26
27
|
- [SCheckbox](../SCheckbox)
|
|
27
28
|
- [SChipFilter](../SChipFilter)
|
|
28
29
|
- [SChipInput](../SChipInput)
|
|
@@ -66,6 +67,7 @@ graph TD;
|
|
|
66
67
|
SAccountListBox --> SIcon
|
|
67
68
|
SButton --> SIcon
|
|
68
69
|
SCallout --> SIcon
|
|
70
|
+
SChatMessage --> SIcon
|
|
69
71
|
SCheckbox --> SIcon
|
|
70
72
|
SChipFilter --> SIcon
|
|
71
73
|
SChipInput --> SIcon
|
|
@@ -88,6 +88,7 @@ export declare const ICONS: {
|
|
|
88
88
|
readonly shield: (p: IconRenderProps) => import("react").JSX.Element;
|
|
89
89
|
readonly sidebar: (p: IconRenderProps) => import("react").JSX.Element;
|
|
90
90
|
readonly star: (p: IconRenderProps) => import("react").JSX.Element;
|
|
91
|
+
readonly stop: (p: IconRenderProps) => import("react").JSX.Element;
|
|
91
92
|
readonly store: (p: IconRenderProps) => import("react").JSX.Element;
|
|
92
93
|
readonly synchronize: (p: IconRenderProps) => import("react").JSX.Element;
|
|
93
94
|
readonly thumbsDown: (p: IconRenderProps) => import("react").JSX.Element;
|
|
@@ -43,6 +43,10 @@ export const IMAGE_FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'] as
|
|
|
43
43
|
|
|
44
44
|
## Dependencies
|
|
45
45
|
|
|
46
|
+
### Used by
|
|
47
|
+
|
|
48
|
+
- [SChatMessage](../SChatMessage)
|
|
49
|
+
|
|
46
50
|
### Depends on
|
|
47
51
|
|
|
48
52
|
- [SCircleProgress](../SCircleProgress)
|
|
@@ -54,5 +58,6 @@ export const IMAGE_FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'] as
|
|
|
54
58
|
graph TD;
|
|
55
59
|
SImage --> SCircleProgress
|
|
56
60
|
SImage --> SIcon
|
|
61
|
+
SChatMessage --> SImage
|
|
57
62
|
style SImage fill:#f9f,stroke:#333,stroke-width:4px
|
|
58
63
|
```
|