sellmate-design-system-react 9.0.0-beta.90 → 9.0.0-beta.91
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 +33 -13
- package/dist/components/SChatInput/README.md +8 -2
- package/dist/components/SChatInput/SChatInput.d.ts +54 -4
- package/dist/components/SDatePicker/README.md +1 -0
- package/dist/components/SDatePicker/SDatePicker.d.ts +9 -1
- package/dist/components/SDateRangePicker/README.md +1 -0
- package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +9 -1
- package/dist/components/SExpansionItem/README.md +1 -0
- package/dist/components/SExpansionItem/SExpansionItem.d.ts +10 -0
- package/dist/components/SGhostButton/README.md +3 -1
- package/dist/components/SGhostButton/SGhostButton.d.ts +18 -3
- package/dist/index.cjs +429 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +429 -167
- package/dist/index.js.map +1 -1
- package/dist/lib/date.d.ts +9 -0
- package/dist/llms-full.txt +47 -16
- package/dist/llms.txt +33 -13
- package/dist/styles.css +3 -6
- package/dist/theme.css +0 -6
- package/package.json +1 -1
package/dist/lib/date.d.ts
CHANGED
|
@@ -14,3 +14,12 @@ export interface DayCell {
|
|
|
14
14
|
/** year/month(1-12)에 대한 6주 그리드 셀 목록 (앞뒤 달 채움 포함) */
|
|
15
15
|
export declare function monthCells(year: number, month: number): DayCell[];
|
|
16
16
|
export declare const WEEK_LABELS: readonly ["일", "월", "화", "수", "목", "금", "토"];
|
|
17
|
+
/** 요일 꼬리(` 월요일`)가 글자 크기 `fontSize`(px)에서 차지하는 폭(px) — 피커가 폭을 이만큼 넓힌다 */
|
|
18
|
+
export declare const weekdaySuffixWidth: (fontSize: number) => number;
|
|
19
|
+
/**
|
|
20
|
+
* "YYYY-MM-DD" 또는 "YYYY-MM-DD HH:mm" 의 날짜 뒤에 요일을 붙인 표시 문자열.
|
|
21
|
+
* "2026-10-19" → "2026-10-19 월요일"
|
|
22
|
+
* "2026-10-19 09:00" → "2026-10-19 월요일 09:00"
|
|
23
|
+
* 날짜로 읽히지 않으면 그대로 돌려준다.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withWeekday(value: string): string;
|
package/dist/llms-full.txt
CHANGED
|
@@ -1443,6 +1443,7 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1443
1443
|
- **`ariaLabel` 은 필수다.** 라벨이 없어 스크린리더가 읽을 것이 아이콘밖에 없다. 뜻이 덜 명확한 아이콘은 **`tooltipText`** 로 이름을 띄운다.
|
|
1444
1444
|
- 여러 개를 나란히 둘 때 간격은 **`gap-sd-4` 고정** — 가로·세로가 같다 (§2-2).
|
|
1445
1445
|
- **라벨이 필요하거나 화면의 주 액션이면 `SGhostButton` 이 아니라 `SButton` 이다.** 위계가 낮다는 이유만으로 `SGhostButton` 을 고르지 않는다 — 라벨이 붙는 저강조 액션은 `SButton` 의 `outline` 조합(§3-5-1)이다.
|
|
1446
|
+
- **다른 화면·문서로 가는 아이콘 버튼은 `href` 를 준다.** 링크(`<a>`)로 그려져 가운데 클릭·⌘클릭·주소 복사가 듣는다. `onClick` 에서 `location`·`window.open` 으로 옮기거나 `<a>` 로 감싸지 않는다. 새 탭이면 `target="_blank"` 만 준다 — `rel` 은 컴포넌트가 붙인다.
|
|
1446
1447
|
|
|
1447
1448
|
```tsx
|
|
1448
1449
|
// 테이블 행 끝 인라인 액션 — 수정은 action, 행 삭제는 default, 간격은 gap-sd-4
|
|
@@ -1460,6 +1461,8 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1460
1461
|
✅ <SGhostButton icon="close" ariaLabel="닫기" onClick={onClose} /> // 모달 닫기 = default
|
|
1461
1462
|
✅ <SGhostButton icon="copy" ariaLabel="상품코드 복사" tooltipText="복사" /> // 뜻이 덜 명확 → 툴팁
|
|
1462
1463
|
❌ <SGhostButton icon="download" ariaLabel="엑셀 다운로드" /> // 목록의 주 액션 → SButton
|
|
1464
|
+
✅ <SGhostButton icon="helpOutline" ariaLabel="도움말" href={HELP_URL} target="_blank" />
|
|
1465
|
+
❌ <SGhostButton icon="helpOutline" ariaLabel="도움말" onClick={() => window.open(HELP_URL)} /> // 가는 버튼 → href
|
|
1463
1466
|
```
|
|
1464
1467
|
|
|
1465
1468
|
##### `SMeatballButton` — ⋯ 뒤에 메뉴를 접어 둘 때
|
|
@@ -1885,6 +1888,8 @@ useEffect(() => {
|
|
|
1885
1888
|
| `STimePicker` | md | lg |
|
|
1886
1889
|
| `STimeRangePicker` | md | lg (오전/오후 표시는 두 사이즈 모두 lg) |
|
|
1887
1890
|
|
|
1891
|
+
`SDatePicker`·`SDateRangePicker` 에 `showWeekday` 를 켜면 날짜 뒤에 요일이 붙어(`2026-10-19 월요일`) 붙는 글자(띄어쓰기 + 요일 3자) 폭만큼 위 표의 하한·상한이 넓어진다 — 등급이 바뀌는 것이 아니다. 늘어난 폭은 컴포넌트가 스스로 잡으므로 요일 때문에 `width` 를 따로 주지 않는다. 표시만 바뀌고 값 형식은 그대로라, **요일을 붙이려고 값을 가공하거나 `hint` 에 요일을 따로 적지 않는다.**
|
|
1892
|
+
|
|
1888
1893
|
`SDateRangePicker` 가 한 등급씩 위인 것은 값이 `YYYY-MM-DD ~ YYYY-MM-DD` 로 두 배가 넘기 때문이다. 같은 이유로 `STimeRangePicker` 의 오전/오후 모드도 sm 에서 한 등급 위를 쓴다 — 그 모드의 최소 폭이 md 등급을 이미 넘어, 그대로 두면 하한이 상한을 넘어 상한이 무력해진다.
|
|
1889
1894
|
|
|
1890
1895
|
네 컴포넌트 모두 이 상한을 `maxWidth` 로 덮을 수 있다 — 등급을 주면 그 등급이 상한이 되고, `width="100%" maxWidth="100%"` 면 부모 폭을 그대로 채운다. **폭이 이미 좁게 정해진 자리(팝오버·좁은 카드)에서 쓴다.**
|
|
@@ -1983,7 +1988,7 @@ const [from, setFrom] = useState<string | null>(null);
|
|
|
1983
1988
|
- **펼침 버튼·들여쓰기는 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `treeColumn` 으로 옮긴다. 그 컬럼은 왼쪽 정렬로 두고, 셀 내용은 `render`·`format` 으로 바꾼다 — `renderCell` 은 펼침 버튼을 넣을 자리가 없어 에러가 난다.
|
|
1984
1989
|
- **셀 병합은 가로(`colSpan`)만 한다.** 트리 컬럼이 아닌 컬럼은 `renderCell` 로 `<td>` 를 바꿀 수 있고, 한 행 안에서 끝나는 `colSpan` 은 접고 펴도 그대로다. **`rowSpan` 으로 세로 병합하지 않는다** — 행이 접히면 사라진 행 몫까지 병합이 남아 아래 행의 칸을 덮고, 덮인 행의 칸은 옆 컬럼으로 밀려난다. 부모와 하위 행에 같은 값이 반복되면 부모 행에만 쓰고 하위 행은 비워 둔다.
|
|
1985
1990
|
- **펼침 영역은 트리 컬럼 셀 전체다.** 하위 행이 있는 행은 펼침 버튼뿐 아니라 그 셀 어디를 눌러도 접고 편다. 다른 셀을 누르면 행 클릭(`onRowClick`)이다 — 트리 컬럼 셀을 누른 것은 행 클릭으로 가지 않는다.
|
|
1986
|
-
-
|
|
1991
|
+
- **펼침·접힘 모션은 컴포넌트가 넣는다.** 하위 행이 부모 아래에서 서랍처럼 펼쳐지고 접히며, 아래 행들도 함께 내려가고 올라온다. 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)에 애니메이션·`transform`·`clip-path` 를 직접 주지 않는다 — 컴포넌트가 행의 위치를 움직이는 것과 겹친다. `onExpandedChange` 는 모션을 기다리지 않고 누른 즉시 온다.
|
|
1987
1992
|
- **트리 컬럼에 링크·버튼을 넣어도 된다.** 셀 안의 링크·버튼·입력을 누르면 그 동작만 일어나고 펼침은 일어나지 않는다 — `stopPropagation` 을 직접 넣지 않는다. 상품명을 상세 링크로 거는 것처럼 쓴다.
|
|
1988
1993
|
- **선택은 계층을 따른다.** 부모를 고르면 접힌 하위 행까지 함께 골라지고, 일부만 골라진 부모는 "일부 선택"으로 그려진다. 하위 행이 모두 골라지면 부모도 `selected` 에 들어간다 — 처리 대상이 잎 행뿐이면 받은 목록에서 하위 행이 없는 행만 걸러 쓴다.
|
|
1989
1994
|
- **가로로 스크롤되는 표는 트리 컬럼을 왼쪽에 고정한다**(`stickyColumn={{ left: 1 }}`, 트리 컬럼이 첫 컬럼일 때). 스크롤로 들여쓰기가 밀려나면 어느 행이 누구의 하위인지 읽히지 않는다. 체크박스 열은 `selectable` 이면 늘 고정되므로 개수에 넣지 않는다.
|
|
@@ -2032,6 +2037,13 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
2032
2037
|
- **리스트 밖 단독 `SExpansionItem` 은 본문을 다루지 않는다.** 선택하는 목록을 담으려면 `SExpansionList` 로 감싼다.
|
|
2033
2038
|
|
|
2034
2039
|
- **하위를 가지지 않는 항목은 `SExpansionItem` 이 아니라 `SListItem`** 으로 둔다. 펼칠 것이 없는데 펼침 항목으로 만들면 화살표만 남는다.
|
|
2040
|
+
- **제목 줄에 누르는 컨트롤(⋯ 메뉴·아이콘 버튼)은 `headerAction` 에 둔다.** 제목 줄은 통째로 여닫는 버튼이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼이 중첩되고 누를 때마다 함께 여닫힌다. `headerAction` 은 그 버튼 밖에 그려져 눌러도 여닫히지 않는다 — `stopPropagation` 이나 제어 모드(`expanded`)로 막지 않는다. 태그·건수처럼 보기만 하는 것은 그대로 `trailing` 이다.
|
|
2041
|
+
|
|
2042
|
+
```tsx
|
|
2043
|
+
✅ <SExpansionItem title="상품 문의" trailing={<STag label="3" color="grey" />}
|
|
2044
|
+
headerAction={<SMeatballButton size="xs" items={folderMenu} onItemClick={v => onFolderAction(id, v)} />}>…</SExpansionItem>
|
|
2045
|
+
❌ <SExpansionItem title="상품 문의" trailing={<SMeatballButton … />}>…</SExpansionItem> {/* 버튼 안 버튼 — 누르면 함께 여닫힌다 */}
|
|
2046
|
+
```
|
|
2035
2047
|
|
|
2036
2048
|
#### 3-7-8. SCard vs SSectionHeaderCard
|
|
2037
2049
|
|
|
@@ -4518,7 +4530,8 @@ export default function StoreSetupPage() {
|
|
|
4518
4530
|
- **날짜가 바뀌는 자리에는 날짜 구분선을 끼운다** — 가로 `SDivider` 두 개 사이에 날짜(`typo-body-sm-default` + `text-fg-tertiary`). 일어난 사실이 아니라 흐름의 경계라 시스템 안내 알약(`SChatSystemMessage`)을 쓰지 않는다. **날짜는 `2026년 10월 6일 화요일`** 로 쓴다 — 말풍선 시각과 같은 대화 예외다(§1-4). `toLocaleDateString()` 은 로케일에 따라 결과가 바뀌므로 쓰지 않고 연·월·일·요일을 직접 이어 붙인다. 가로 구분선은 기본이 전체 폭이라 한 줄에 둘을 놓을 때는 `className="w-auto flex-1"` 로 남은 폭을 나눠 갖게 한다. 앞뒤 간격은 시스템 안내와 같은 토큰이다. 날짜가 다르면 같은 시각이어도 한 묶음으로 합치지 않는다.
|
|
4519
4531
|
- 대화의 시각은 `오후 5:52` 로 쓴다 (§1-4 대화 예외). 읽음 표시는 보낸 메시지에만, 템플릿으로 보낸 글은 `variant="template"` 로 면을 나눈다.
|
|
4520
4532
|
- **입력 중인 글은 페이지가 상담마다 든다.** 상담을 옮겨도 쓰던 글이 남고, 템플릿 패널이 그 글에 넣을 수 있다. 템플릿은 누르면 **입력창에 넣는다** — 바로 보내지 않고 상담사가 고쳐 보낸다. 보내기 전 첨부는 입력창 위에 `SChatAttachedFile` 로 쌓는다.
|
|
4521
|
-
- **첨부를 받는 대화면 `
|
|
4533
|
+
- **첨부를 받는 대화면 `onAttachFiles` 와 `onPasteFiles` 에 같은 함수를 넘긴다** — 첨부 버튼으로 고른 파일과 붙여넣은 파일·캡처 이미지를 같은 자리에 쌓는다. 파일 선택 창은 입력창이 연다 — `<input type="file">` 을 따로 두지 않는다. `onAttach` 는 서버 파일함 모달처럼 앱이 직접 고르는 흐름에만 쓴다. 붙여넣기에서 파일을 골라내고 글로 들어가지 않게 막는 것은 입력창이 한다 — `onPaste` 로 직접 처리하지 않는다. 첨부가 쌓여 있으면 `hasAttachments` 를 켠다 — 글 없이 파일만 보낼 수 있고, 이때 `onSend` 에는 빈 문자열이 온다.
|
|
4534
|
+
- **입력창의 일부만 막을 때는 그 동작만 잠근다.** 첨부가 올라가는 중이면 `sendDisabled` 로 전송만 잠근다 — 글을 적고 파일을 더 붙이는 것은 된다. 첨부 수가 상한에 닿았으면 `attachDisabled` 로 첨부를 잠그고 이유는 `attachTooltip` 으로 알린다 — 파일 붙여넣기도 함께 막힌다. 상한 아래에서 여러 파일이 한 번에 들어와 넘치는 것은 앱이 `onPasteFiles` 에서 잘라 낸다. `disabled` 는 입력까지 잠그므로 종료된 대화에만, `busy` 는 정지 버튼으로 바뀌므로 응답을 기다릴 때만 쓴다.
|
|
4522
4535
|
- **상담 종료는 `SChatInput` 의 `footerAction` 이다** — 전송과 층이 다른 동작이라 입력창 아래 보조 슬롯(`STextLink`)에 두고, 실행 전에 `SModal.confirm` 으로 묻는다. 종료된 상담은 입력창을 `disabled` 로 잠그고 종료 링크를 뺀다.
|
|
4523
4536
|
- 목록의 상태는 `STag` 다 — 색 점(`SBadge`)만으로 찍지 않는다 (§3-1). 템플릿 분류는 `SExpansionList` 루트 하나에 분류(`SExpansionItem`) 안 템플릿(`SListItem`)을 바로 넣는다 (§3-7-7).
|
|
4524
4537
|
|
|
@@ -4742,7 +4755,14 @@ function Conversation({
|
|
|
4742
4755
|
}) {
|
|
4743
4756
|
const initial = useConsultMessages(consult.id);
|
|
4744
4757
|
const [entries, setEntries] = useState<ChatEntry[]>(initial);
|
|
4745
|
-
|
|
4758
|
+
// 보내기 전 첨부. 같은 파일을 두 번 붙일 수 있으므로 이름이 아니라 id 로 가른다
|
|
4759
|
+
const [attached, setAttached] = useState<{ id: number; file: File }[]>([]);
|
|
4760
|
+
const nextAttachId = useRef(0);
|
|
4761
|
+
// 첨부 버튼으로 고른 파일과 붙여넣은 파일을 같은 함수로 받아 같은 자리에 쌓는다
|
|
4762
|
+
const addFiles = (files: File[]) => {
|
|
4763
|
+
const added = files.map(file => ({ id: nextAttachId.current++, file }));
|
|
4764
|
+
setAttached(current => [...current, ...added]);
|
|
4765
|
+
};
|
|
4746
4766
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
4747
4767
|
const closed = consult.status === 'closed';
|
|
4748
4768
|
|
|
@@ -4772,13 +4792,13 @@ function Conversation({
|
|
|
4772
4792
|
},
|
|
4773
4793
|
]
|
|
4774
4794
|
: []),
|
|
4775
|
-
...attached.map((
|
|
4795
|
+
...attached.map(({ id, file }) => ({
|
|
4776
4796
|
kind: 'message' as const,
|
|
4777
|
-
id: `f${current.length}-${
|
|
4797
|
+
id: `f${current.length}-${id}`,
|
|
4778
4798
|
direction: 'outgoing' as const,
|
|
4779
4799
|
date,
|
|
4780
4800
|
time,
|
|
4781
|
-
file: { name, size:
|
|
4801
|
+
file: { name: file.name, size: file.size },
|
|
4782
4802
|
})),
|
|
4783
4803
|
]);
|
|
4784
4804
|
onDraftChange('');
|
|
@@ -4869,11 +4889,11 @@ function Conversation({
|
|
|
4869
4889
|
<div className="flex flex-col gap-sd-8 py-sd-16">
|
|
4870
4890
|
{attached.length > 0 && (
|
|
4871
4891
|
<div className="flex flex-row flex-wrap gap-sd-8 px-sd-24">
|
|
4872
|
-
{attached.map(
|
|
4892
|
+
{attached.map(({ id, file }) => (
|
|
4873
4893
|
<SChatAttachedFile
|
|
4874
|
-
key={
|
|
4875
|
-
name={name}
|
|
4876
|
-
onRemove={() => setAttached(current => current.filter(
|
|
4894
|
+
key={id}
|
|
4895
|
+
name={file.name}
|
|
4896
|
+
onRemove={() => setAttached(current => current.filter(a => a.id !== id))}
|
|
4877
4897
|
/>
|
|
4878
4898
|
))}
|
|
4879
4899
|
</div>
|
|
@@ -4883,9 +4903,9 @@ function Conversation({
|
|
|
4883
4903
|
disabled={closed}
|
|
4884
4904
|
onValueChange={onDraftChange}
|
|
4885
4905
|
onSend={send}
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
onPasteFiles={
|
|
4906
|
+
// 파일 선택 창은 입력창이 연다 — <input type="file"> 을 따로 두지 않는다
|
|
4907
|
+
onAttachFiles={addFiles}
|
|
4908
|
+
onPasteFiles={addFiles}
|
|
4889
4909
|
// 첨부가 쌓여 있으면 글 없이도 보낼 수 있다
|
|
4890
4910
|
hasAttachments={attached.length > 0}
|
|
4891
4911
|
// 상담 종료는 전송과 층이 다른 동작 — 입력창 아래 보조 액션 슬롯이다
|
|
@@ -6427,6 +6447,11 @@ export type SCalloutMessage = string | SCalloutMessage[];
|
|
|
6427
6447
|
| `placeholder?` | `string` | `DEFAULT_PLACEHOLDER` | 안내 문구. 세 제품이 같은 말로 서도록 기본값을 쥐고 있다 |
|
|
6428
6448
|
| `disabled?` | `boolean` | `false` | 비활성 |
|
|
6429
6449
|
| `hasAttachments?` | `boolean` | `false` | 보낼 첨부가 쌓여 있는지. 켜면 글이 비어도 전송 버튼이 열린다 — 파일·이미지만 보낼 수 있다. 첨부 목록은 앱이 쥐므로 입력창은 그 유무만 받는다. 첨부를 받는 대화면 쌓인 첨부 수로 켠다. |
|
|
6450
|
+
| `attachAccept?` | `string` | — | 파일 선택 창에서 고를 수 있는 형식 (`<input accept>` 와 같다). 예: `'image/*,.pdf'` |
|
|
6451
|
+
| `attachMultiple?` | `boolean` | `true` | 파일 선택 창에서 여러 개를 한 번에 고를 수 있는지 |
|
|
6452
|
+
| `attachDisabled?` | `boolean` | `false` | 첨부를 잠근다 — 첨부 버튼(파일 선택 창)이 잠기고, **파일 붙여넣기도 받지 않는다**. 입력과 전송은 그대로 둔다. 첨부 수가 상한에 닿았을 때처럼 **더 붙일 수 없는 자리**에 쓴다. 버튼만 잠그면 캡처 이미지를 붙여넣어 상한을 넘길 수 있으므로 붙여넣기도 함께 막는다 — 파일이 든 붙여넣기는 버려지고 (`onPasteFiles` 가 불리지 않는다) 파일 이름이 글로 들어가지도 않는다. 글만 붙여넣는 것은 된다. 왜 잠겼는지는 `attachTooltip` 으로 알린다. |
|
|
6453
|
+
| `attachTooltip?` | `string` | — | 첨부 버튼에 마우스를 올리면 뜨는 툴팁. 받을 수 있는 파일 종류·크기나, 잠긴 이유를 적는다. 버튼의 접근성 이름("파일 첨부")은 바뀌지 않는다. |
|
|
6454
|
+
| `sendDisabled?` | `boolean` | `false` | 전송을 잠근다 — 전송 버튼과 Enter 전송이 함께 잠기고, 입력과 첨부는 그대로 둔다. 첨부가 올라가는 중처럼 **아직 보낼 수 없지만 적고 붙이는 것은 되는 자리**에 쓴다. 응답을 기다리는 중이면 이것이 아니라 `busy` 를 켠다 — 그쪽은 정지 버튼으로 바뀐다. |
|
|
6430
6455
|
| `busy?` | `boolean` | `false` | 응답을 기다리는 중인지. 켜면 전송 버튼 자리에 정지 버튼이 서고 Enter 전송이 잠긴다 — 답이 오는 동안 또 보내지 못하게 막는 자리다. |
|
|
6431
6456
|
| `footerAction?` | `ReactNode` | — | 입력창 아래 보조 액션 슬롯. 없으면 그 줄이 통째로 빠진다. 상담 종료처럼 **대화 자체를 끝내는, 전송과 층이 다른 동작**이 오는 자리다. 무엇을 놓을지는 앱이 정하고, 입력창은 가운데에 세우는 것까지만 한다 — `STextLink` 를 넣는 것이 보통이다. |
|
|
6432
6457
|
| `maxRows?` | `number` | `DEFAULT_MAX_ROWS` | 입력창이 자랄 수 있는 최대 줄 수. 여기 닿으면 더 자라지 않고 그 안에서 스크롤한다. |
|
|
@@ -6440,8 +6465,9 @@ export type SCalloutMessage = string | SCalloutMessage[];
|
|
|
6440
6465
|
|-------|------|-------------|
|
|
6441
6466
|
| `onValueChange` | `(value: string) => void` | 값 변경 |
|
|
6442
6467
|
| `onSend` | `(value: string) => void` | 전송. 전송 버튼을 누르거나 Enter 를 쳤을 때 현재 값을 넘긴다. **값이 비면 전송 버튼이 잠긴다** — 빈 메시지는 보낼 수 없다. 단 `hasAttachments` 가 켜져 있으면 글 없이도 보낼 수 있고, 이때 빈 문자열을 넘긴다. |
|
|
6443
|
-
| `
|
|
6444
|
-
| `
|
|
6468
|
+
| `onAttachFiles` | `(files: File[]) => void` | 첨부 버튼으로 고른 파일. **주면 입력창 왼쪽에 첨부 버튼이 서고, 누르면 파일 선택 창이 열린다.** 붙여넣기(`onPasteFiles`)와 같은 모양으로 받는다 — 두 길을 같은 함수로 받아 같은 자리에 쌓는다. 파일 선택 창은 입력창이 연다. 앱이 `<input type="file">` 을 따로 두지 않는다. |
|
|
6469
|
+
| `onAttach` | `() => void` | 첨부 버튼을 누름. **주면 입력창 왼쪽에 첨부 버튼이 선다** — 파일 선택 창은 열리지 않는다. 서버 파일함 모달처럼 **앱이 직접 고르는 흐름**에만 쓴다. 기기의 파일을 고르는 것이면 `onAttachFiles` 를 준다. 둘 다 주면 `onAttachFiles` 가 쓰이고 이것은 불리지 않는다. |
|
|
6470
|
+
| `onPasteFiles` | `(files: File[]) => void` | 붙여넣은 파일. 클립보드에 파일(캡처한 이미지 포함)이 있으면 불린다. **파일이 있으면 글자 붙여넣기를 막는다** — 그대로 두면 파일 이름이 글로 함께 들어간다. 파일이 없는 붙여넣기는 평소대로 글로 들어간다. 받은 파일을 어디에 쌓을지는 앱이 정한다 — 첨부 버튼으로 고른 파일(`onAttachFiles`)과 같은 함수로 받아 같은 자리에 쌓는 것이 보통이다. |
|
|
6445
6471
|
| `onStop` | `() => void` | 정지. `busy` 일 때 정지 버튼을 누르면 불린다 |
|
|
6446
6472
|
|
|
6447
6473
|
## Dependencies
|
|
@@ -7371,6 +7397,7 @@ export const CONTEXT_ALERT_INTENTS = [
|
|
|
7371
7397
|
| `placeholder?` | `string` | — | |
|
|
7372
7398
|
| `selectable?` | `[string, string]` | — | 선택 가능 범위 [시작, 종료] |
|
|
7373
7399
|
| `disabled?` | `boolean` | `false` | |
|
|
7400
|
+
| `showWeekday?` | `boolean` | `false` | 트리거에 보이는 날짜 뒤에 요일을 붙인다 — `2026-10-19 월요일`. 표시만 바뀌고 `value`·`onValueChange` 의 형식(`YYYY-MM-DD`)은 그대로다. `mode="date"` 에서만 동작한다 (월·연도에는 요일이 없다). 붙는 글자(띄어쓰기 + 요일 3자)만큼 내장 폭 하한·상한이 넓어진다. |
|
|
7374
7401
|
| `clearable?` | `boolean` | `false` | 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다. **필수 입력 필드에는 켜지 않는다** — 지우면 다시 고르기 전까지 폼이 통과하지 못한다. `STimePicker` · `STimeRangePicker` · `SSelect` 의 `clearable` 과 같은 규칙이다. |
|
|
7375
7402
|
| `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
|
|
7376
7403
|
| `maxWidth?` | `SFieldWidth` | — | 컨트롤 최대 너비 — 폭 등급 · 숫자=px · CSS 길이. 지정하지 않으면 값 길이(`YYYY-MM-DD`)에 맞춘 내장 상한(`size='md'` → `lg`, `'sm'` → `md`)이 걸려, `width="100%"` 를 받아도 행 전체로 늘어나지 않는다. 팝오버처럼 폭이 이미 좁게 정해진 자리에서 그 폭을 그대로 채워야 하면 `width="100%"` 와 함께 `maxWidth="100%"` 를 준다. `0` 은 상한을 아예 걸지 않는다 — 부모보다 넓어지는 것까지 허용해야 할 때만 쓴다. |
|
|
@@ -7466,6 +7493,7 @@ export type SDatePickerSize = SFieldSize;
|
|
|
7466
7493
|
| `selectable?` | `[string, string]` | — | |
|
|
7467
7494
|
| `maxRange?` | `number` | — | 최대 선택 일수 |
|
|
7468
7495
|
| `showTimePicker?` | `boolean` | `false` | 시간(시:분) 선택 푸터 사용 여부. true이면 value는 "YYYY-MM-DD HH:mm" 형식이 되고, 캘린더 하단에 시작·종료 시간 입력이 표시된다. |
|
|
7496
|
+
| `showWeekday?` | `boolean` | `false` | 트리거에 보이는 시작·종료 날짜 뒤에 요일을 붙인다 — `2026-10-19 월요일 ~ 2026-10-25 일요일`. `showTimePicker` 와 함께 쓰면 요일이 날짜와 시간 사이에 온다 (`2026-10-19 월요일 09:00`). 표시만 바뀌고 `value`·`onValueChange` 의 형식은 그대로다. 붙는 글자(띄어쓰기 + 요일 3자, 양쪽)만큼 내장 폭 하한·상한이 넓어진다. |
|
|
7469
7497
|
| `disabled?` | `boolean` | `false` | |
|
|
7470
7498
|
| `clearable?` | `boolean` | `false` | 선택값 지우기 버튼. 값이 있을 때만 나타나고, 누르면 `onValueChange` 로 `null` 이 온다. **필수 입력 필드에는 켜지 않는다** — 지우면 다시 고르기 전까지 폼이 통과하지 못한다. `SDatePicker` · `STimePicker` · `SSelect` 의 `clearable` 과 같은 규칙이다. |
|
|
7471
7499
|
| `width?` | `SFieldWidth` | — | 컨트롤 너비 — 폭 등급(`'xs' | 'sm' | 'md' | 'lg' | 'xl'`) · 숫자=px · CSS 길이. 등급은 `--cmp-field-width-*` 토큰으로 풀리므로 토큰이 바뀌면 화면이 따라간다. 값 길이에 상한이 없으면 `"100%"` 로 두어 행 전체를 쓴다. |
|
|
@@ -8180,6 +8208,7 @@ export const SEDITOR_TOOLBAR_ITEMS = [
|
|
|
8180
8208
|
| `depth?` | `1 \| 2 \| 3` | `1` | 중첩 단계. Figma 기준 1~3단계. `SExpansionList` 안에서는 주지 않아도 중첩에서 정해진다 — 직접 주면 그 값이 우선한다. |
|
|
8181
8209
|
| `leading?` | `ReactNode \| SExpansionItemRenderProp` | — | 타이틀 앞에 표시할 아이콘/콘텐츠 |
|
|
8182
8210
|
| `trailing?` | `ReactNode` | — | 타이틀 뒤에 표시할 태그/콘텐츠 |
|
|
8211
|
+
| `headerAction?` | `ReactNode` | — | 제목 줄 오른쪽 끝에 놓는 컨트롤 슬롯 — ⋯ 메뉴·아이콘 버튼처럼 **누르는 것**이 오는 자리다. 제목 줄은 통째로 여닫는 `<button>` 이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼 안에 버튼이 중첩되고, 누를 때마다 항목이 함께 여닫힌다. 이 슬롯은 그 버튼 **밖**에 그려지므로 눌러도 여닫히지 않고, Tab 으로 따로 닿는다. 태그·숫자처럼 보기만 하는 것은 `trailing` 에 둔다. 항목이 `disabled` 여도 슬롯은 잠그지 않는다 — 잠글지는 넣은 컨트롤에 앱이 정한다. |
|
|
8183
8212
|
| `bodyPadding?` | `boolean` | `true` | body 내부 패딩 사용 여부. `SExpansionList` 안에서 본문이 목록(`SListItem`·`SExpansionItem` 만, 또는 `SExpansionList`)이면 기본으로 꺼진다. |
|
|
8184
8213
|
| `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
|
|
8185
8214
|
| `expanded?` | `boolean` | — | |
|
|
@@ -8587,6 +8616,8 @@ export interface SFormValidationError {
|
|
|
8587
8616
|
| `ariaExpanded?` | `boolean` | — | disclosure 버튼일 때 펼침 상태 (aria-expanded) |
|
|
8588
8617
|
| `tooltipText?` | `string` | — | 호버 시 표시할 툴팁 텍스트 |
|
|
8589
8618
|
| `disabled?` | `boolean` | `false` | 비활성 |
|
|
8619
|
+
| `href?` | `string` | — | 이동할 주소. **주면 `<button>` 대신 `<a>` 로 그린다** — 가운데 클릭·⌘클릭·주소 복사가 링크처럼 듣는다. 눌러서 다른 화면·문서로 가는 버튼이면 `onClick` 에서 `location` 을 바꾸지 말고 이것을 준다. 비활성이면 주소를 떼어 눌러도 가지 않는다. |
|
|
8620
|
+
| `target?` | `string` | — | `href` 를 여는 곳. `'_blank'` 면 `rel="noopener noreferrer"` 를 함께 붙인다 |
|
|
8590
8621
|
| `anchorClassName?` | `string` | — | tooltip anchor wrapper 클래스. tooltipText가 있을 때 위치 조정용으로 사용합니다. |
|
|
8591
8622
|
| `anchorStyle?` | `CSSProperties` | — | tooltip anchor wrapper 스타일. tooltipText가 있을 때 위치 조정용으로 사용합니다. |
|
|
8592
8623
|
| `className?` | `string` | — | |
|
|
@@ -8596,7 +8627,7 @@ export interface SFormValidationError {
|
|
|
8596
8627
|
|
|
8597
8628
|
| Event | Type | Description |
|
|
8598
8629
|
|-------|------|-------------|
|
|
8599
|
-
| `onClick` | `(e: MouseEvent<HTMLButtonElement>) => void` | 클릭 (sdClick) |
|
|
8630
|
+
| `onClick` | `(e: MouseEvent<HTMLButtonElement \| HTMLAnchorElement>) => void` | 클릭 (sdClick). `href` 와 함께 주면 이동 전에 불린다 |
|
|
8600
8631
|
|
|
8601
8632
|
## Types
|
|
8602
8633
|
|
package/dist/llms.txt
CHANGED
|
@@ -1444,6 +1444,7 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1444
1444
|
- **`ariaLabel` 은 필수다.** 라벨이 없어 스크린리더가 읽을 것이 아이콘밖에 없다. 뜻이 덜 명확한 아이콘은 **`tooltipText`** 로 이름을 띄운다.
|
|
1445
1445
|
- 여러 개를 나란히 둘 때 간격은 **`gap-sd-4` 고정** — 가로·세로가 같다 (§2-2).
|
|
1446
1446
|
- **라벨이 필요하거나 화면의 주 액션이면 `SGhostButton` 이 아니라 `SButton` 이다.** 위계가 낮다는 이유만으로 `SGhostButton` 을 고르지 않는다 — 라벨이 붙는 저강조 액션은 `SButton` 의 `outline` 조합(§3-5-1)이다.
|
|
1447
|
+
- **다른 화면·문서로 가는 아이콘 버튼은 `href` 를 준다.** 링크(`<a>`)로 그려져 가운데 클릭·⌘클릭·주소 복사가 듣는다. `onClick` 에서 `location`·`window.open` 으로 옮기거나 `<a>` 로 감싸지 않는다. 새 탭이면 `target="_blank"` 만 준다 — `rel` 은 컴포넌트가 붙인다.
|
|
1447
1448
|
|
|
1448
1449
|
```tsx
|
|
1449
1450
|
// 테이블 행 끝 인라인 액션 — 수정은 action, 행 삭제는 default, 간격은 gap-sd-4
|
|
@@ -1461,6 +1462,8 @@ tableRef.current.scrollToRow(row); // 복원
|
|
|
1461
1462
|
✅ <SGhostButton icon="close" ariaLabel="닫기" onClick={onClose} /> // 모달 닫기 = default
|
|
1462
1463
|
✅ <SGhostButton icon="copy" ariaLabel="상품코드 복사" tooltipText="복사" /> // 뜻이 덜 명확 → 툴팁
|
|
1463
1464
|
❌ <SGhostButton icon="download" ariaLabel="엑셀 다운로드" /> // 목록의 주 액션 → SButton
|
|
1465
|
+
✅ <SGhostButton icon="helpOutline" ariaLabel="도움말" href={HELP_URL} target="_blank" />
|
|
1466
|
+
❌ <SGhostButton icon="helpOutline" ariaLabel="도움말" onClick={() => window.open(HELP_URL)} /> // 가는 버튼 → href
|
|
1464
1467
|
```
|
|
1465
1468
|
|
|
1466
1469
|
##### `SMeatballButton` — ⋯ 뒤에 메뉴를 접어 둘 때
|
|
@@ -1886,6 +1889,8 @@ useEffect(() => {
|
|
|
1886
1889
|
| `STimePicker` | md | lg |
|
|
1887
1890
|
| `STimeRangePicker` | md | lg (오전/오후 표시는 두 사이즈 모두 lg) |
|
|
1888
1891
|
|
|
1892
|
+
`SDatePicker`·`SDateRangePicker` 에 `showWeekday` 를 켜면 날짜 뒤에 요일이 붙어(`2026-10-19 월요일`) 붙는 글자(띄어쓰기 + 요일 3자) 폭만큼 위 표의 하한·상한이 넓어진다 — 등급이 바뀌는 것이 아니다. 늘어난 폭은 컴포넌트가 스스로 잡으므로 요일 때문에 `width` 를 따로 주지 않는다. 표시만 바뀌고 값 형식은 그대로라, **요일을 붙이려고 값을 가공하거나 `hint` 에 요일을 따로 적지 않는다.**
|
|
1893
|
+
|
|
1889
1894
|
`SDateRangePicker` 가 한 등급씩 위인 것은 값이 `YYYY-MM-DD ~ YYYY-MM-DD` 로 두 배가 넘기 때문이다. 같은 이유로 `STimeRangePicker` 의 오전/오후 모드도 sm 에서 한 등급 위를 쓴다 — 그 모드의 최소 폭이 md 등급을 이미 넘어, 그대로 두면 하한이 상한을 넘어 상한이 무력해진다.
|
|
1890
1895
|
|
|
1891
1896
|
네 컴포넌트 모두 이 상한을 `maxWidth` 로 덮을 수 있다 — 등급을 주면 그 등급이 상한이 되고, `width="100%" maxWidth="100%"` 면 부모 폭을 그대로 채운다. **폭이 이미 좁게 정해진 자리(팝오버·좁은 카드)에서 쓴다.**
|
|
@@ -1984,7 +1989,7 @@ const [from, setFrom] = useState<string | null>(null);
|
|
|
1984
1989
|
- **펼침 버튼·들여쓰기는 컴포넌트가 그린다.** 기본은 보이는 첫 컬럼이고 `treeColumn` 으로 옮긴다. 그 컬럼은 왼쪽 정렬로 두고, 셀 내용은 `render`·`format` 으로 바꾼다 — `renderCell` 은 펼침 버튼을 넣을 자리가 없어 에러가 난다.
|
|
1985
1990
|
- **셀 병합은 가로(`colSpan`)만 한다.** 트리 컬럼이 아닌 컬럼은 `renderCell` 로 `<td>` 를 바꿀 수 있고, 한 행 안에서 끝나는 `colSpan` 은 접고 펴도 그대로다. **`rowSpan` 으로 세로 병합하지 않는다** — 행이 접히면 사라진 행 몫까지 병합이 남아 아래 행의 칸을 덮고, 덮인 행의 칸은 옆 컬럼으로 밀려난다. 부모와 하위 행에 같은 값이 반복되면 부모 행에만 쓰고 하위 행은 비워 둔다.
|
|
1986
1991
|
- **펼침 영역은 트리 컬럼 셀 전체다.** 하위 행이 있는 행은 펼침 버튼뿐 아니라 그 셀 어디를 눌러도 접고 편다. 다른 셀을 누르면 행 클릭(`onRowClick`)이다 — 트리 컬럼 셀을 누른 것은 행 클릭으로 가지 않는다.
|
|
1987
|
-
-
|
|
1992
|
+
- **펼침·접힘 모션은 컴포넌트가 넣는다.** 하위 행이 부모 아래에서 서랍처럼 펼쳐지고 접히며, 아래 행들도 함께 내려가고 올라온다. 모션을 줄인 환경에서는 꺼진다. 행(`rowClassName`)에 애니메이션·`transform`·`clip-path` 를 직접 주지 않는다 — 컴포넌트가 행의 위치를 움직이는 것과 겹친다. `onExpandedChange` 는 모션을 기다리지 않고 누른 즉시 온다.
|
|
1988
1993
|
- **트리 컬럼에 링크·버튼을 넣어도 된다.** 셀 안의 링크·버튼·입력을 누르면 그 동작만 일어나고 펼침은 일어나지 않는다 — `stopPropagation` 을 직접 넣지 않는다. 상품명을 상세 링크로 거는 것처럼 쓴다.
|
|
1989
1994
|
- **선택은 계층을 따른다.** 부모를 고르면 접힌 하위 행까지 함께 골라지고, 일부만 골라진 부모는 "일부 선택"으로 그려진다. 하위 행이 모두 골라지면 부모도 `selected` 에 들어간다 — 처리 대상이 잎 행뿐이면 받은 목록에서 하위 행이 없는 행만 걸러 쓴다.
|
|
1990
1995
|
- **가로로 스크롤되는 표는 트리 컬럼을 왼쪽에 고정한다**(`stickyColumn={{ left: 1 }}`, 트리 컬럼이 첫 컬럼일 때). 스크롤로 들여쓰기가 밀려나면 어느 행이 누구의 하위인지 읽히지 않는다. 체크박스 열은 `selectable` 이면 늘 고정되므로 개수에 넣지 않는다.
|
|
@@ -2033,6 +2038,13 @@ const [selectedId, setSelectedId] = useState<string>();
|
|
|
2033
2038
|
- **리스트 밖 단독 `SExpansionItem` 은 본문을 다루지 않는다.** 선택하는 목록을 담으려면 `SExpansionList` 로 감싼다.
|
|
2034
2039
|
|
|
2035
2040
|
- **하위를 가지지 않는 항목은 `SExpansionItem` 이 아니라 `SListItem`** 으로 둔다. 펼칠 것이 없는데 펼침 항목으로 만들면 화살표만 남는다.
|
|
2041
|
+
- **제목 줄에 누르는 컨트롤(⋯ 메뉴·아이콘 버튼)은 `headerAction` 에 둔다.** 제목 줄은 통째로 여닫는 버튼이라 `title`·`trailing` 에 컨트롤을 넣으면 버튼이 중첩되고 누를 때마다 함께 여닫힌다. `headerAction` 은 그 버튼 밖에 그려져 눌러도 여닫히지 않는다 — `stopPropagation` 이나 제어 모드(`expanded`)로 막지 않는다. 태그·건수처럼 보기만 하는 것은 그대로 `trailing` 이다.
|
|
2042
|
+
|
|
2043
|
+
```tsx
|
|
2044
|
+
✅ <SExpansionItem title="상품 문의" trailing={<STag label="3" color="grey" />}
|
|
2045
|
+
headerAction={<SMeatballButton size="xs" items={folderMenu} onItemClick={v => onFolderAction(id, v)} />}>…</SExpansionItem>
|
|
2046
|
+
❌ <SExpansionItem title="상품 문의" trailing={<SMeatballButton … />}>…</SExpansionItem> {/* 버튼 안 버튼 — 누르면 함께 여닫힌다 */}
|
|
2047
|
+
```
|
|
2036
2048
|
|
|
2037
2049
|
#### 3-7-8. SCard vs SSectionHeaderCard
|
|
2038
2050
|
|
|
@@ -4519,7 +4531,8 @@ export default function StoreSetupPage() {
|
|
|
4519
4531
|
- **날짜가 바뀌는 자리에는 날짜 구분선을 끼운다** — 가로 `SDivider` 두 개 사이에 날짜(`typo-body-sm-default` + `text-fg-tertiary`). 일어난 사실이 아니라 흐름의 경계라 시스템 안내 알약(`SChatSystemMessage`)을 쓰지 않는다. **날짜는 `2026년 10월 6일 화요일`** 로 쓴다 — 말풍선 시각과 같은 대화 예외다(§1-4). `toLocaleDateString()` 은 로케일에 따라 결과가 바뀌므로 쓰지 않고 연·월·일·요일을 직접 이어 붙인다. 가로 구분선은 기본이 전체 폭이라 한 줄에 둘을 놓을 때는 `className="w-auto flex-1"` 로 남은 폭을 나눠 갖게 한다. 앞뒤 간격은 시스템 안내와 같은 토큰이다. 날짜가 다르면 같은 시각이어도 한 묶음으로 합치지 않는다.
|
|
4520
4532
|
- 대화의 시각은 `오후 5:52` 로 쓴다 (§1-4 대화 예외). 읽음 표시는 보낸 메시지에만, 템플릿으로 보낸 글은 `variant="template"` 로 면을 나눈다.
|
|
4521
4533
|
- **입력 중인 글은 페이지가 상담마다 든다.** 상담을 옮겨도 쓰던 글이 남고, 템플릿 패널이 그 글에 넣을 수 있다. 템플릿은 누르면 **입력창에 넣는다** — 바로 보내지 않고 상담사가 고쳐 보낸다. 보내기 전 첨부는 입력창 위에 `SChatAttachedFile` 로 쌓는다.
|
|
4522
|
-
- **첨부를 받는 대화면 `
|
|
4534
|
+
- **첨부를 받는 대화면 `onAttachFiles` 와 `onPasteFiles` 에 같은 함수를 넘긴다** — 첨부 버튼으로 고른 파일과 붙여넣은 파일·캡처 이미지를 같은 자리에 쌓는다. 파일 선택 창은 입력창이 연다 — `<input type="file">` 을 따로 두지 않는다. `onAttach` 는 서버 파일함 모달처럼 앱이 직접 고르는 흐름에만 쓴다. 붙여넣기에서 파일을 골라내고 글로 들어가지 않게 막는 것은 입력창이 한다 — `onPaste` 로 직접 처리하지 않는다. 첨부가 쌓여 있으면 `hasAttachments` 를 켠다 — 글 없이 파일만 보낼 수 있고, 이때 `onSend` 에는 빈 문자열이 온다.
|
|
4535
|
+
- **입력창의 일부만 막을 때는 그 동작만 잠근다.** 첨부가 올라가는 중이면 `sendDisabled` 로 전송만 잠근다 — 글을 적고 파일을 더 붙이는 것은 된다. 첨부 수가 상한에 닿았으면 `attachDisabled` 로 첨부를 잠그고 이유는 `attachTooltip` 으로 알린다 — 파일 붙여넣기도 함께 막힌다. 상한 아래에서 여러 파일이 한 번에 들어와 넘치는 것은 앱이 `onPasteFiles` 에서 잘라 낸다. `disabled` 는 입력까지 잠그므로 종료된 대화에만, `busy` 는 정지 버튼으로 바뀌므로 응답을 기다릴 때만 쓴다.
|
|
4523
4536
|
- **상담 종료는 `SChatInput` 의 `footerAction` 이다** — 전송과 층이 다른 동작이라 입력창 아래 보조 슬롯(`STextLink`)에 두고, 실행 전에 `SModal.confirm` 으로 묻는다. 종료된 상담은 입력창을 `disabled` 로 잠그고 종료 링크를 뺀다.
|
|
4524
4537
|
- 목록의 상태는 `STag` 다 — 색 점(`SBadge`)만으로 찍지 않는다 (§3-1). 템플릿 분류는 `SExpansionList` 루트 하나에 분류(`SExpansionItem`) 안 템플릿(`SListItem`)을 바로 넣는다 (§3-7-7).
|
|
4525
4538
|
|
|
@@ -4743,7 +4756,14 @@ function Conversation({
|
|
|
4743
4756
|
}) {
|
|
4744
4757
|
const initial = useConsultMessages(consult.id);
|
|
4745
4758
|
const [entries, setEntries] = useState<ChatEntry[]>(initial);
|
|
4746
|
-
|
|
4759
|
+
// 보내기 전 첨부. 같은 파일을 두 번 붙일 수 있으므로 이름이 아니라 id 로 가른다
|
|
4760
|
+
const [attached, setAttached] = useState<{ id: number; file: File }[]>([]);
|
|
4761
|
+
const nextAttachId = useRef(0);
|
|
4762
|
+
// 첨부 버튼으로 고른 파일과 붙여넣은 파일을 같은 함수로 받아 같은 자리에 쌓는다
|
|
4763
|
+
const addFiles = (files: File[]) => {
|
|
4764
|
+
const added = files.map(file => ({ id: nextAttachId.current++, file }));
|
|
4765
|
+
setAttached(current => [...current, ...added]);
|
|
4766
|
+
};
|
|
4747
4767
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
4748
4768
|
const closed = consult.status === 'closed';
|
|
4749
4769
|
|
|
@@ -4773,13 +4793,13 @@ function Conversation({
|
|
|
4773
4793
|
},
|
|
4774
4794
|
]
|
|
4775
4795
|
: []),
|
|
4776
|
-
...attached.map((
|
|
4796
|
+
...attached.map(({ id, file }) => ({
|
|
4777
4797
|
kind: 'message' as const,
|
|
4778
|
-
id: `f${current.length}-${
|
|
4798
|
+
id: `f${current.length}-${id}`,
|
|
4779
4799
|
direction: 'outgoing' as const,
|
|
4780
4800
|
date,
|
|
4781
4801
|
time,
|
|
4782
|
-
file: { name, size:
|
|
4802
|
+
file: { name: file.name, size: file.size },
|
|
4783
4803
|
})),
|
|
4784
4804
|
]);
|
|
4785
4805
|
onDraftChange('');
|
|
@@ -4870,11 +4890,11 @@ function Conversation({
|
|
|
4870
4890
|
<div className="flex flex-col gap-sd-8 py-sd-16">
|
|
4871
4891
|
{attached.length > 0 && (
|
|
4872
4892
|
<div className="flex flex-row flex-wrap gap-sd-8 px-sd-24">
|
|
4873
|
-
{attached.map(
|
|
4893
|
+
{attached.map(({ id, file }) => (
|
|
4874
4894
|
<SChatAttachedFile
|
|
4875
|
-
key={
|
|
4876
|
-
name={name}
|
|
4877
|
-
onRemove={() => setAttached(current => current.filter(
|
|
4895
|
+
key={id}
|
|
4896
|
+
name={file.name}
|
|
4897
|
+
onRemove={() => setAttached(current => current.filter(a => a.id !== id))}
|
|
4878
4898
|
/>
|
|
4879
4899
|
))}
|
|
4880
4900
|
</div>
|
|
@@ -4884,9 +4904,9 @@ function Conversation({
|
|
|
4884
4904
|
disabled={closed}
|
|
4885
4905
|
onValueChange={onDraftChange}
|
|
4886
4906
|
onSend={send}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
onPasteFiles={
|
|
4907
|
+
// 파일 선택 창은 입력창이 연다 — <input type="file"> 을 따로 두지 않는다
|
|
4908
|
+
onAttachFiles={addFiles}
|
|
4909
|
+
onPasteFiles={addFiles}
|
|
4890
4910
|
// 첨부가 쌓여 있으면 글 없이도 보낼 수 있다
|
|
4891
4911
|
hasAttachments={attached.length > 0}
|
|
4892
4912
|
// 상담 종료는 전송과 층이 다른 동작 — 입력창 아래 보조 액션 슬롯이다
|
package/dist/styles.css
CHANGED
|
@@ -425,7 +425,6 @@
|
|
|
425
425
|
--animate-floating-out-bottom: floating-out-bottom 0.15s ease-in forwards;
|
|
426
426
|
--animate-floating-out-left: floating-out-left 0.15s ease-in forwards;
|
|
427
427
|
--animate-floating-out-right: floating-out-right 0.15s ease-in forwards;
|
|
428
|
-
--animate-table-row-in: floating-in-bottom 0.15s ease-out;
|
|
429
428
|
--animate-skeleton-wave: skeleton-wave 1.5s linear 0.5s infinite;
|
|
430
429
|
--animate-skeleton-pulse: skeleton-pulse 1.5s ease-in-out 0.5s infinite;
|
|
431
430
|
--animate-skeleton-fade: skeleton-fade 1.5s linear 0.5s infinite;
|
|
@@ -3779,6 +3778,9 @@
|
|
|
3779
3778
|
.pr-\(--cmp-chatMessage-row-paddingInset\) {
|
|
3780
3779
|
padding-right: var(--cmp-chatMessage-row-paddingInset);
|
|
3781
3780
|
}
|
|
3781
|
+
.pr-\(--cmp-listExpansionItem-title-gap\) {
|
|
3782
|
+
padding-right: var(--cmp-listExpansionItem-title-gap);
|
|
3783
|
+
}
|
|
3782
3784
|
.pr-\(--cmp-listExpansionItem-title-paddingX\) {
|
|
3783
3785
|
padding-right: var(--cmp-listExpansionItem-title-paddingX);
|
|
3784
3786
|
}
|
|
@@ -5800,11 +5802,6 @@
|
|
|
5800
5802
|
scrollbar-color: var(--color-grey-45) var(--color-white);
|
|
5801
5803
|
}
|
|
5802
5804
|
}
|
|
5803
|
-
.motion-safe\:animate-table-row-in {
|
|
5804
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
5805
|
-
animation: var(--animate-table-row-in);
|
|
5806
|
-
}
|
|
5807
|
-
}
|
|
5808
5805
|
.motion-reduce\:hidden {
|
|
5809
5806
|
@media (prefers-reduced-motion: reduce) {
|
|
5810
5807
|
display: none;
|
package/dist/theme.css
CHANGED
|
@@ -2470,12 +2470,6 @@
|
|
|
2470
2470
|
--animate-floating-out-left: floating-out-left 0.15s ease-in forwards;
|
|
2471
2471
|
--animate-floating-out-right: floating-out-right 0.15s ease-in forwards;
|
|
2472
2472
|
|
|
2473
|
-
/* 표 행 펼침 (STreeTable) — 여러 행을 연달아 열고 닫는 반복 조작이라 툴팁(0.35s)·STree(0.2s)보다 짧게 둔다.
|
|
2474
|
-
0.1s 아래로 내리면 움직임이 보이지 않아 깜빡임처럼 읽힌다.
|
|
2475
|
-
모양은 아래로 열리는 floating 과 같다 (위에서 4px 내려오며 나타남) */
|
|
2476
|
-
--animate-table-row-in: floating-in-bottom 0.15s ease-out;
|
|
2477
|
-
/* 접을 때는 모션 없이 바로 뺀다. 사라지는 모션을 넣으면 그 동안 아래 행이 기다렸다 올라와 굼떠 보인다 */
|
|
2478
|
-
|
|
2479
2473
|
/* 스켈레톤 (Quasar QSkeleton 포팅). 속도는 SSkeleton 이 animation-duration 으로 덮어쓴다 */
|
|
2480
2474
|
--animate-skeleton-wave: skeleton-wave 1.5s linear 0.5s infinite;
|
|
2481
2475
|
--animate-skeleton-pulse: skeleton-pulse 1.5s ease-in-out 0.5s infinite;
|