sellmate-design-system-react 9.0.0-beta.66 → 9.0.0-beta.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 한 항목을 짚으면 나머지가 물러나는 전환 — 막대·조각·점·값 라벨·범례가 **같은 속도로** 움직인다.
3
+ *
4
+ * 속도가 갈리면 같은 손짓 하나에 그림이 여러 번 나뉘어 반응하는 것처럼 보인다.
5
+ * 그래서 값을 각 차트에 두지 않고 여기 한 곳에 둔다.
6
+ *
7
+ * **전환 대상은 「흐려지는 것」뿐이다.** 짚은 항목이 커지는 것(막대·선의 값 라벨이 `typo-*` 로
8
+ * 굵어지는 것, 파이·도넛 조각이 부푸는 것)은 일부러 즉시 바꾼다 — 글자가 늘어나거나 도형이
9
+ * 자라는 중간 상태가 오히려 읽기 나쁘기 때문이다.
10
+ *
11
+ * ⚠️ **둘로 나뉜 이유**: 차트마다 흐려지는 방식이 다르다. 막대·조각·범례는 불투명도를 낮추지만,
12
+ * 선 차트의 점은 선 위에 앉아 있어 불투명도를 낮추면 밑의 선이 비쳐 지저분해진다. 그래서 점은
13
+ * 옅은 **색으로 갈아끼운다**(`linePointDimmedColor`). 전환할 속성이 다르니 클래스도 나뉜다 —
14
+ * 점에 `transition-opacity` 를 붙이면 아무 일도 일어나지 않는다.
15
+ */
16
+ /** 불투명도로 물러나는 것 — 막대 · 파이/도넛 조각 · 값 라벨 · 범례 */
17
+ export declare const CHART_DIM_OPACITY_TRANSITION = "transition-opacity duration-200 ease-out motion-reduce:transition-none";
18
+ /** 색으로 물러나는 것 — 선 차트의 점 (`transition-colors` 가 `fill` 을 포함한다) */
19
+ export declare const CHART_DIM_COLOR_TRANSITION = "transition-colors duration-200 ease-out motion-reduce:transition-none";
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 호버 미리보기(`SImage previewOnHover`)가 뜨는 자리를 계산한다.
3
+ *
4
+ * **시작점은 썸네일의 한가운데**다 — 미리보기의 좌상단 모서리가 썸네일 중앙에 놓인다.
5
+ * 그래서 미리보기는 늘 오른쪽 아래로 펼쳐지고, 원래 보던 썸네일을 가리지 않는다.
6
+ *
7
+ * 창 오른쪽·아래로 넘치면 **넘친 만큼만 왼쪽·위로 되민다**. 반대편으로 뒤집지 않는다 —
8
+ * 뒤집으면 커서와 미리보기의 관계가 순간 바뀌어 어느 썸네일의 미리보기인지 읽기 어렵다.
9
+ * (Radix 의 `avoidCollisions` 는 shift 와 flip 을 함께 켜므로 쓰지 않고, 이 계산으로 대신한다.)
10
+ */
11
+ /** 위치 계산에 필요한 만큼만 추린 사각형 — DOMRect 을 그대로 받을 수 있다 */
12
+ export interface HoverPreviewAnchorRect {
13
+ left: number;
14
+ top: number;
15
+ width: number;
16
+ height: number;
17
+ }
18
+ export interface HoverPreviewOriginInput {
19
+ /** 썸네일의 viewport 기준 사각형 */
20
+ anchor: HoverPreviewAnchorRect;
21
+ /** 미리보기 상자의 한 변(px). 정사각형이다 */
22
+ size: number;
23
+ viewportWidth: number;
24
+ viewportHeight: number;
25
+ /** 창 가장자리와 띄울 최소 간격 */
26
+ padding?: number;
27
+ }
28
+ /** 미리보기 상자의 좌상단 좌표 (viewport 기준) */
29
+ export declare function hoverPreviewOrigin({ anchor, size, viewportWidth, viewportHeight, padding, }: HoverPreviewOriginInput): {
30
+ x: number;
31
+ y: number;
32
+ };
@@ -587,6 +587,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
587
587
  | 아이콘을 넣는다 | `SIcon` | |
588
588
  | 셀메이트 로고를 넣는다 | `SLogo` (`SIcon` 과 달리 `size` 가 **높이**이고, 색을 주지 않으면 브랜드색이다) | §3-7-14 |
589
589
  | 사진·썸네일을 보여준다 (로딩·실패 상태 포함) | `SImage` | §3-7-12 |
590
+ | 작은 썸네일을 호버해서 크게 확인하게 한다 | `SImage previewOnHover` | 툴팁·팝오버로 직접 만들지 않는다 — 미리보기는 마우스를 통과시키고 원본의 로딩·실패도 스스로 표현한다. §3-7-12 |
590
591
  | 문장 안에서 다른 화면으로 보낸다 | `STextLink` | §3-5-6 |
591
592
 
592
593
  #### C. 동작을 실행시킨다
@@ -658,8 +659,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
658
659
 
659
660
  | 하려는 일 | 컴포넌트 | 갈림 |
660
661
  | --- | --- | --- |
661
- | 화면 전체를 잠그고 기다리게 한다 | `SLoadingModal` (또는 `SModal.loading()`) | §3-2 |
662
- | 특정 영역만 덮고 기다리게 한다 | `SLoadingContainer` | §3-2 |
662
+ | 화면 전체를 잠그고 기다리게 한다 | `loading.show()` — 진행률·실패 후 재시도 버튼이 필요하면 `SLoadingModal` (`SModal.loading()`) | §3-3-6 |
663
+ | 특정 영역만 덮고 기다리게 한다 | `SLoadingContainer` | §3-3-6 |
663
664
  | 진행률을 가로 막대로 보여준다 | `SLinearProgress` | §3-7-9 |
664
665
  | 진행률·대기를 원형으로 보여준다 | `SCircleProgress` | §3-7-9 |
665
666
 
@@ -689,7 +690,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
689
690
  | 작업 결과를 **일시적으로** 알림 | `SToast` (+ 루트에 `SToastContainer`) |
690
691
  | 진행 전 **확인/취소**를 받아야 할 때 | `SModal.confirm({...}).onOk(...)` — 무엇을 할지가 모달 밖에서 이미 정해진 경우다 (§3-3-2) |
691
692
  | 특정 UI 요소에 대한 **온보딩·기능 안내** | `SGuide` |
692
- | 로딩 중 화면 잠금 | `SLoadingModal` / 영역 로딩은 `SLoadingContainer` |
693
+ | 로딩 중 화면 잠금 | `loading.show()` (§3-3-6) / 영역 로딩은 `SLoadingContainer` |
693
694
 
694
695
  **`SCallout` 의 `type` 은 놓이는 면이 정한다.** 안내의 강도가 아니라 배경 대비의 문제다.
695
696
 
@@ -721,7 +722,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
721
722
  | --- | --- |
722
723
  | hover 시 **짧은 보조 설명** (상호작용 없음) | `STooltip` |
723
724
  | 클릭 시 **상호작용 가능한** 작은 콘텐츠 (메뉴·미니 폼) | `SPopover` |
724
- | 로딩 중 화면 잠금 | `SModal.loading(...)` / `SLoadingModal` |
725
+ | 로딩 중 화면 잠금 — 스피너와 메시지만 (기본) | `loading.show(...)` — 3-3-6 |
726
+ | 로딩 중 화면 잠금 — **진행률·실패 후 재시도 버튼**이 필요하다 | `SModal.loading(...)` / `SLoadingModal` |
725
727
  | 그 밖에 **흐름을 끊고 띄우는 창** | `SPopup` · `SActionModal` · `SModal.confirm` — 아래 3-3-1 판별 순서로 고른다 |
726
728
 
727
729
  #### 3-3-1. SPopup vs SActionModal vs SModal.confirm — 판별 순서
@@ -894,6 +896,78 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
894
896
 
895
897
  작성 중인 내용이 있을 때 닫기를 시도하면 이탈 안내를 띄우는 것은 **소비 앱 몫**이다. 디자인 시스템은 dirty 상태를 알지 못하므로 백드롭·ESC 를 일괄 차단할 뿐이다. 안내가 필요하면 앱이 자체 dirty 판정 후 `SModal.confirm` 으로 띄운다.
896
898
 
899
+ #### 3-3-6. 전역 로딩 오버레이 — `loading`
900
+
901
+ 화면 전체를 잠그고 기다리게 할 때 쓰는 명령형 싱글톤이다. 마운트할 것이 없다 — 첫 `show` 에 오버레이 루트가 `body` 에 자동으로 붙는다.
902
+
903
+ **표면은 둘뿐이다.**
904
+
905
+ | 하려는 일 | 사용 |
906
+ | --- | --- |
907
+ | 기다리게 한다 (거의 항상 이것) | `loading.show({ message })` → **핸들**. 부르면 걷고, `.update({ message })` 로 이 오버레이의 메시지만 바꾼다 |
908
+ | 화면 전환·에러 복구로 짝을 더는 맞출 수 없다 | `loading.hideAll()` — 비상구다. 평소에는 쓰지 않는다 |
909
+
910
+ **걷는 것은 `finally` 에 둔다.** 빠뜨리면 그 즉시 화면이 갇히고, 정상 경로에서는 아무 표시가 없다.
911
+
912
+ ```tsx
913
+ const done = loading.show({ message: '저장 중입니다...' });
914
+ try {
915
+ const payload = buildPayload(form);
916
+ done.update({ message: '전송 중입니다...' }); // 이 오버레이의 메시지만 바뀐다
917
+ await api.submit(payload);
918
+ } finally {
919
+ done(); // 이 오버레이만 걷는다
920
+ }
921
+ ```
922
+
923
+ 한 화면에서 같은 모양이 반복되면 **앱이 자기 헬퍼를 둔다.** 디자인 시스템은 원시 도구만 준다.
924
+
925
+ ```tsx
926
+ // 소비 앱 쪽 유틸 — DS 가 가지지 않는다
927
+ export const withLoading = async <T,>(task: Promise<T>, options?: { message?: string }) => {
928
+ const done = loading.show(options);
929
+ try {
930
+ return await task;
931
+ } finally {
932
+ done();
933
+ }
934
+ };
935
+ ```
936
+
937
+ - **반환 핸들을 반드시 잡는다.** 잡지 않으면 그 오버레이를 걷을 방법이 `hideAll()`(= 전부 걷기) 밖에 남지 않는다.
938
+ - **메시지를 바꾸려고 `show()` 를 다시 부르지 않는다.** 하나 더 쌓여 한 번 걷는 것으로는 닫히지 않는다. 핸들의 `update()` 를 쓴다.
939
+ - 겹쳐 부르면 그만큼 쌓이고 **마지막 하나가 걷힐 때** 닫힌다. 목록 재조회와 상세 저장이 겹쳐도 먼저 끝난 쪽이 아직 진행 중인 작업의 화면 잠금을 풀지 않는다. 화면에 그려지는 오버레이는 몇 개를 띄우든 **하나**이고, 표시되는 메시지는 맨 위 것이다.
940
+
941
+ ##### 제거된 API — 올라오면서 바꿀 것
942
+
943
+ 「스택 맨 위를 집는」 **위치 기준 API 를 전부 없앴다.** 호출자가 누구인지 볼 수 없어 겹친 작업끼리 남의 것을 걷거나 덮었고, 화면이 영영 닫히지 않는 사고의 원인이었다.
944
+
945
+ | 없어진 것 | 무엇이었나 | 무엇으로 바꾸나 |
946
+ | --- | --- | --- |
947
+ | ❌ `loading.hide()` | 맨 위 **하나**를 걷었다 | **거의 전부 `show()` 핸들**을 `finally` 에서 부르는 것으로 바뀐다. `hideAll()` 은 라우트 이탈·에러 복구처럼 **진행 중이던 남의 작업까지 정리하는 것이 의도였던** 소수의 자리에만 |
948
+ | ❌ `loading.update()` | 맨 위 항목의 메시지를 바꿨다 | `show()` 핸들의 `update()` |
949
+ | ❌ `SLoadingViewport` | 뷰포트를 직접 배치하게 했다 | 트리에서 뺀다. 루트는 첫 `show` 에 자동으로 붙으므로 직접 두면 오버레이가 **두 겹**이 되어 딤만 진해졌다 |
950
+
951
+ ⚠️ **`hideAll()` 로 일괄 치환하지 않는다.** `hide()` 가 참조 카운트 이전에는 「전부 걷기」였으므로 그쪽이 맞아 보이지만, 그 시절 호출자의 의도는 **「내가 띄운 걸 닫는다」**였다 — 큐가 있다는 것을 몰랐을 뿐이다. `hideAll()` 로 옮기면 겹친 화면에서 **아직 진행 중인 작업의 잠금까지 풀어** 재클릭·중복 요청이 난다. 기본은 핸들이다.
952
+
953
+ > **왜 이 실수가 쉬운가** — `show()` 를 몇 번 부르든 **화면에 그려지는 오버레이는 하나**다. 내부 카운트는 화면에 드러나지 않으므로 `hide()` 뒤에 오버레이가 남아도 「안 닫혔다」로만 보이고 「하나 남았다」로는 보이지 않는다. 그래서 위치로 짝을 맞추는 방식은 눈으로 검증할 수 없고, 핸들로 소유권을 명시하는 것이 유일한 방법이다.
954
+
955
+ ⚠️ **`show()` 가 두 번 불리는 경로도 함께 본다.** 같은 변경에서 `show()` 의 의미도 바뀌었다 — 그전에는 다시 부르는 것이 **갱신**이었고 지금은 하나 더 쌓인다. 그래서 아래는 `hide()` 를 안 썼어도 오버레이를 남긴다.
956
+
957
+ ```tsx
958
+ ❌ loading.show({ message: '1/2 ...' });
959
+ loading.show({ message: '2/2 ...' }); // 그때는 갱신, 지금은 하나 더 쌓인다
960
+ done(); // 하나만 걷힌다 → 남는다
961
+
962
+ ✅ const done = loading.show({ message: '1/2 ...' });
963
+ done.update({ message: '2/2 ...' });
964
+ done();
965
+ ```
966
+
967
+ > **원본(Quasar `Loading`)과의 차이는 둘뿐이고 둘 다 안전 때문이다.** 원본의 인자 없는 `hide()` 는 「전부 걷기」이므로 이름을 되쓰면 기존 호출의 의미가 컴파일 에러 없이 바뀐다 — 그래서 `hideAll()` 이다. 그리고 원본은 겹친 작업을 앱이 준 `group` 이름으로 가르지만(안 주면 먼저 끝난 쪽이 남의 잠금을 푼다), 여기서는 `show()` 마다 항목이 따로 생겨 앱이 무엇을 빠뜨려도 그 일이 없다.
968
+
969
+ > **영역만 덮을 때는 `SLoadingContainer` 를 직접 배치한다** (`open` prop 제어). `loading` 은 화면 전체를 잠그는 전역 오버레이 전용이다.
970
+
897
971
  ### 3-4. 테이블 컬럼 — 정렬·너비·헤더
898
972
 
899
973
  #### 정렬과 너비는 같은 표에서 정한다
@@ -1918,13 +1992,23 @@ const [selectedId, setSelectedId] = useState<string>();
1918
1992
  - **크기는 `ratio` 와 너비로 준다.** `ratio` 기본값이 `1`(정사각형)이라 너비만 주면 정사각형이 된다. `className`/`style` 로 높이를 직접 주면 그쪽이 이기고 `ratio` 는 무시된다.
1919
1993
  - 상자를 채우는 방식은 `fit`(기본 `cover`), 붙는 위치는 `position` 이다. 안쪽 `<img>` 에 네이티브 속성이 필요하면 `imgProps` 로 넘긴다 — 목록에서 고스트 드래그를 막는 `imgProps={{ draggable: false }}` 가 대표적이다.
1920
1994
  - 썸네일이 많은 목록에서는 `loadingShowDelay` 를 준다. 캐시된 이미지가 즉시 로드될 때 스피너가 한 프레임 번쩍이는 것을 막는다.
1995
+ - **썸네일이 작아 내용을 알아볼 수 없으면 `previewOnHover` 를 켠다.** 호버하면 큰 이미지가 포털로 뜬다 — 상품 목록에서 사진을 확인하려고 상세 화면까지 들어가는 일을 없앤다. **툴팁이나 팝오버로 직접 만들지 않는다**: 미리보기는 마우스를 통과시켜 아래 행의 클릭을 가로채지 않고, 원본의 로딩·실패도 스스로 표현한다.
1996
+ - 목록 썸네일과 원본 URL 이 다르면 원본을 `previewSrc` 로 준다. 주지 않으면 썸네일을 그대로 확대해서 흐릿하다.
1997
+ - **뜨는 자리를 앱이 정하지 않는다.** 미리보기는 썸네일 한가운데에서 오른쪽 아래로 펼쳐지고, 창 밖으로 넘치면 넘친 만큼만 왼쪽·위로 밀린다(반대편으로 뒤집히지 않는다). 방향을 고르는 prop 은 없다.
1998
+ - 크기는 `previewSize` 로 줄일 수 있고 **상한은 200px** 이다. 그보다 큰 값을 줘도 200 으로 잘린다 — 미리보기가 화면을 덮지 않게 하는 규격이다.
1999
+ - 미리보기 상자는 정사각형이고 이미지는 `contain` 으로 들어간다 — 세로로 긴 이미지도 잘리지 않는다. `ratio`·`fit` 은 썸네일 쪽에만 적용된다.
1921
2000
 
1922
2001
  ```tsx
1923
2002
  ✅ <SImage src={item.thumbnailUrl} alt={item.name} style={{ width: 64 }} /> {/* 정사각 썸네일 */}
1924
2003
  ✅ <SImage src={banner} ratio={16 / 9} className="w-full" /> {/* 가로형 배너 */}
2004
+ ✅ <SImage src={item.thumbnailUrl} previewOnHover previewSrc={item.imageUrl} {/* 호버로 원본 확인 */}
2005
+ alt={item.name} style={{ width: 64 }} />
1925
2006
 
1926
2007
  ❌ <img src={item.thumbnailUrl} /> {/* 로딩·실패 상태가 없다 */}
1927
2008
  ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
2009
+ ❌ <STooltip content={<img src={item.imageUrl} />}> {/* previewOnHover 를 쓴다 */}
2010
+ <SImage src={item.thumbnailUrl} />
2011
+ </STooltip>
1928
2012
  ```
1929
2013
 
1930
2014
  #### 3-7-14. 로고 — SLogo
@@ -3118,6 +3202,7 @@ export default function ExcelFilePopupPage() {
3118
3202
  - [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4), 카드 안에서 닫히는 액션도 같은 prop 으로 넘겼는가 (§3-7-8)
3119
3203
  - [ ] 아래에 버튼이 있는 자리(모달·드로어·카드)의 `STextarea autogrow` 에 `maxRows` 를 함께 줬는가 (§3-7-1 — 없으면 긴 글이 버튼을 화면 밖으로 밀어낸다)
3120
3204
  - [ ] 앱 부트스트랩의 Provider 안쪽에 `<SModalOutlet />` 이 한 번 렌더되어 있는가 (§4-1 — 없으면 모달 안에서 앱 훅이 죽는다), 그 대신으로 모달 컴포넌트를 Provider 로 다시 감싸지 않았는가
3205
+ - [ ] 전역 로딩의 `loading.show()` 반환 핸들을 잡아 `finally` 에서 그것만 불렀는가, 메시지를 바꾸려고 `show()` 를 두 번 부르지는 않았는가 (§3-3-6 — 걷기를 빠뜨리면 화면이 갇히고, 두 번 부르면 하나 더 쌓여 오버레이가 남는다)
3121
3206
  - [ ] 고른 컴포넌트를 §2-0 의 제 층에 놓았는가 (요소를 `SPage` 에 직접 놓지 않았는가, 블록을 `div` 로 감싸지 않았는가)
3122
3207
  - [ ] §2-0 포함 규칙을 지켰는가 (카드 안 카드 ❌, 표 셀 안 블록 ❌)
3123
3208
  - [ ] 블록 순서가 §2-0 순서와 맞는가 (제목 → 안내 → 필터 → 툴바 → 본문 → 페이지네이션 → 하단 액션)
@@ -6767,6 +6852,9 @@ export type SGuideMessage = string | SGuideMessage[];
6767
6852
  | `loadingSlot?` | `ReactNode` | — | 로딩 중 표시를 직접 지정한다 (기본: 스피너) |
6768
6853
  | `errorSlot?` | `ReactNode` | — | 실패·빈 상태 표시를 직접 지정한다 (기본: 빈 이미지 아이콘) |
6769
6854
  | `imgProps?` | `ImgHTMLAttributes<HTMLImageElement>` | — | 안쪽 `<img>` 로 넘길 네이티브 속성 (srcSet · sizes · crossOrigin · draggable 등) |
6855
+ | `previewOnHover?` | `boolean` | `false` | 호버하면 큰 이미지를 포털로 띄운다. 썸네일이 작아 내용을 알아볼 수 없는 자리에 쓴다 |
6856
+ | `previewSrc?` | `string` | — | 미리보기에 띄울 경로. 원본 URL 이 썸네일과 다를 때 준다 (기본: `src` 와 같은 경로) |
6857
+ | `previewSize?` | `number` | `PREVIEW_MAX_SIZE` | 미리보기 상자의 한 변(px). 상자는 정사각형이고 이미지는 `contain` 으로 들어간다. **200px 이 상한이라 더 큰 값을 줘도 200 으로 잘린다** — 미리보기가 화면을 덮지 않게 하는 규격이다. |
6770
6858
 
6771
6859
  #### Events
6772
6860
 
package/dist/llms.txt CHANGED
@@ -588,6 +588,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
588
588
  | 아이콘을 넣는다 | `SIcon` | |
589
589
  | 셀메이트 로고를 넣는다 | `SLogo` (`SIcon` 과 달리 `size` 가 **높이**이고, 색을 주지 않으면 브랜드색이다) | §3-7-14 |
590
590
  | 사진·썸네일을 보여준다 (로딩·실패 상태 포함) | `SImage` | §3-7-12 |
591
+ | 작은 썸네일을 호버해서 크게 확인하게 한다 | `SImage previewOnHover` | 툴팁·팝오버로 직접 만들지 않는다 — 미리보기는 마우스를 통과시키고 원본의 로딩·실패도 스스로 표현한다. §3-7-12 |
591
592
  | 문장 안에서 다른 화면으로 보낸다 | `STextLink` | §3-5-6 |
592
593
 
593
594
  #### C. 동작을 실행시킨다
@@ -659,8 +660,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
659
660
 
660
661
  | 하려는 일 | 컴포넌트 | 갈림 |
661
662
  | --- | --- | --- |
662
- | 화면 전체를 잠그고 기다리게 한다 | `SLoadingModal` (또는 `SModal.loading()`) | §3-2 |
663
- | 특정 영역만 덮고 기다리게 한다 | `SLoadingContainer` | §3-2 |
663
+ | 화면 전체를 잠그고 기다리게 한다 | `loading.show()` — 진행률·실패 후 재시도 버튼이 필요하면 `SLoadingModal` (`SModal.loading()`) | §3-3-6 |
664
+ | 특정 영역만 덮고 기다리게 한다 | `SLoadingContainer` | §3-3-6 |
664
665
  | 진행률을 가로 막대로 보여준다 | `SLinearProgress` | §3-7-9 |
665
666
  | 진행률·대기를 원형으로 보여준다 | `SCircleProgress` | §3-7-9 |
666
667
 
@@ -690,7 +691,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
690
691
  | 작업 결과를 **일시적으로** 알림 | `SToast` (+ 루트에 `SToastContainer`) |
691
692
  | 진행 전 **확인/취소**를 받아야 할 때 | `SModal.confirm({...}).onOk(...)` — 무엇을 할지가 모달 밖에서 이미 정해진 경우다 (§3-3-2) |
692
693
  | 특정 UI 요소에 대한 **온보딩·기능 안내** | `SGuide` |
693
- | 로딩 중 화면 잠금 | `SLoadingModal` / 영역 로딩은 `SLoadingContainer` |
694
+ | 로딩 중 화면 잠금 | `loading.show()` (§3-3-6) / 영역 로딩은 `SLoadingContainer` |
694
695
 
695
696
  **`SCallout` 의 `type` 은 놓이는 면이 정한다.** 안내의 강도가 아니라 배경 대비의 문제다.
696
697
 
@@ -722,7 +723,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
722
723
  | --- | --- |
723
724
  | hover 시 **짧은 보조 설명** (상호작용 없음) | `STooltip` |
724
725
  | 클릭 시 **상호작용 가능한** 작은 콘텐츠 (메뉴·미니 폼) | `SPopover` |
725
- | 로딩 중 화면 잠금 | `SModal.loading(...)` / `SLoadingModal` |
726
+ | 로딩 중 화면 잠금 — 스피너와 메시지만 (기본) | `loading.show(...)` — 3-3-6 |
727
+ | 로딩 중 화면 잠금 — **진행률·실패 후 재시도 버튼**이 필요하다 | `SModal.loading(...)` / `SLoadingModal` |
726
728
  | 그 밖에 **흐름을 끊고 띄우는 창** | `SPopup` · `SActionModal` · `SModal.confirm` — 아래 3-3-1 판별 순서로 고른다 |
727
729
 
728
730
  #### 3-3-1. SPopup vs SActionModal vs SModal.confirm — 판별 순서
@@ -895,6 +897,78 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
895
897
 
896
898
  작성 중인 내용이 있을 때 닫기를 시도하면 이탈 안내를 띄우는 것은 **소비 앱 몫**이다. 디자인 시스템은 dirty 상태를 알지 못하므로 백드롭·ESC 를 일괄 차단할 뿐이다. 안내가 필요하면 앱이 자체 dirty 판정 후 `SModal.confirm` 으로 띄운다.
897
899
 
900
+ #### 3-3-6. 전역 로딩 오버레이 — `loading`
901
+
902
+ 화면 전체를 잠그고 기다리게 할 때 쓰는 명령형 싱글톤이다. 마운트할 것이 없다 — 첫 `show` 에 오버레이 루트가 `body` 에 자동으로 붙는다.
903
+
904
+ **표면은 둘뿐이다.**
905
+
906
+ | 하려는 일 | 사용 |
907
+ | --- | --- |
908
+ | 기다리게 한다 (거의 항상 이것) | `loading.show({ message })` → **핸들**. 부르면 걷고, `.update({ message })` 로 이 오버레이의 메시지만 바꾼다 |
909
+ | 화면 전환·에러 복구로 짝을 더는 맞출 수 없다 | `loading.hideAll()` — 비상구다. 평소에는 쓰지 않는다 |
910
+
911
+ **걷는 것은 `finally` 에 둔다.** 빠뜨리면 그 즉시 화면이 갇히고, 정상 경로에서는 아무 표시가 없다.
912
+
913
+ ```tsx
914
+ const done = loading.show({ message: '저장 중입니다...' });
915
+ try {
916
+ const payload = buildPayload(form);
917
+ done.update({ message: '전송 중입니다...' }); // 이 오버레이의 메시지만 바뀐다
918
+ await api.submit(payload);
919
+ } finally {
920
+ done(); // 이 오버레이만 걷는다
921
+ }
922
+ ```
923
+
924
+ 한 화면에서 같은 모양이 반복되면 **앱이 자기 헬퍼를 둔다.** 디자인 시스템은 원시 도구만 준다.
925
+
926
+ ```tsx
927
+ // 소비 앱 쪽 유틸 — DS 가 가지지 않는다
928
+ export const withLoading = async <T,>(task: Promise<T>, options?: { message?: string }) => {
929
+ const done = loading.show(options);
930
+ try {
931
+ return await task;
932
+ } finally {
933
+ done();
934
+ }
935
+ };
936
+ ```
937
+
938
+ - **반환 핸들을 반드시 잡는다.** 잡지 않으면 그 오버레이를 걷을 방법이 `hideAll()`(= 전부 걷기) 밖에 남지 않는다.
939
+ - **메시지를 바꾸려고 `show()` 를 다시 부르지 않는다.** 하나 더 쌓여 한 번 걷는 것으로는 닫히지 않는다. 핸들의 `update()` 를 쓴다.
940
+ - 겹쳐 부르면 그만큼 쌓이고 **마지막 하나가 걷힐 때** 닫힌다. 목록 재조회와 상세 저장이 겹쳐도 먼저 끝난 쪽이 아직 진행 중인 작업의 화면 잠금을 풀지 않는다. 화면에 그려지는 오버레이는 몇 개를 띄우든 **하나**이고, 표시되는 메시지는 맨 위 것이다.
941
+
942
+ ##### 제거된 API — 올라오면서 바꿀 것
943
+
944
+ 「스택 맨 위를 집는」 **위치 기준 API 를 전부 없앴다.** 호출자가 누구인지 볼 수 없어 겹친 작업끼리 남의 것을 걷거나 덮었고, 화면이 영영 닫히지 않는 사고의 원인이었다.
945
+
946
+ | 없어진 것 | 무엇이었나 | 무엇으로 바꾸나 |
947
+ | --- | --- | --- |
948
+ | ❌ `loading.hide()` | 맨 위 **하나**를 걷었다 | **거의 전부 `show()` 핸들**을 `finally` 에서 부르는 것으로 바뀐다. `hideAll()` 은 라우트 이탈·에러 복구처럼 **진행 중이던 남의 작업까지 정리하는 것이 의도였던** 소수의 자리에만 |
949
+ | ❌ `loading.update()` | 맨 위 항목의 메시지를 바꿨다 | `show()` 핸들의 `update()` |
950
+ | ❌ `SLoadingViewport` | 뷰포트를 직접 배치하게 했다 | 트리에서 뺀다. 루트는 첫 `show` 에 자동으로 붙으므로 직접 두면 오버레이가 **두 겹**이 되어 딤만 진해졌다 |
951
+
952
+ ⚠️ **`hideAll()` 로 일괄 치환하지 않는다.** `hide()` 가 참조 카운트 이전에는 「전부 걷기」였으므로 그쪽이 맞아 보이지만, 그 시절 호출자의 의도는 **「내가 띄운 걸 닫는다」**였다 — 큐가 있다는 것을 몰랐을 뿐이다. `hideAll()` 로 옮기면 겹친 화면에서 **아직 진행 중인 작업의 잠금까지 풀어** 재클릭·중복 요청이 난다. 기본은 핸들이다.
953
+
954
+ > **왜 이 실수가 쉬운가** — `show()` 를 몇 번 부르든 **화면에 그려지는 오버레이는 하나**다. 내부 카운트는 화면에 드러나지 않으므로 `hide()` 뒤에 오버레이가 남아도 「안 닫혔다」로만 보이고 「하나 남았다」로는 보이지 않는다. 그래서 위치로 짝을 맞추는 방식은 눈으로 검증할 수 없고, 핸들로 소유권을 명시하는 것이 유일한 방법이다.
955
+
956
+ ⚠️ **`show()` 가 두 번 불리는 경로도 함께 본다.** 같은 변경에서 `show()` 의 의미도 바뀌었다 — 그전에는 다시 부르는 것이 **갱신**이었고 지금은 하나 더 쌓인다. 그래서 아래는 `hide()` 를 안 썼어도 오버레이를 남긴다.
957
+
958
+ ```tsx
959
+ ❌ loading.show({ message: '1/2 ...' });
960
+ loading.show({ message: '2/2 ...' }); // 그때는 갱신, 지금은 하나 더 쌓인다
961
+ done(); // 하나만 걷힌다 → 남는다
962
+
963
+ ✅ const done = loading.show({ message: '1/2 ...' });
964
+ done.update({ message: '2/2 ...' });
965
+ done();
966
+ ```
967
+
968
+ > **원본(Quasar `Loading`)과의 차이는 둘뿐이고 둘 다 안전 때문이다.** 원본의 인자 없는 `hide()` 는 「전부 걷기」이므로 이름을 되쓰면 기존 호출의 의미가 컴파일 에러 없이 바뀐다 — 그래서 `hideAll()` 이다. 그리고 원본은 겹친 작업을 앱이 준 `group` 이름으로 가르지만(안 주면 먼저 끝난 쪽이 남의 잠금을 푼다), 여기서는 `show()` 마다 항목이 따로 생겨 앱이 무엇을 빠뜨려도 그 일이 없다.
969
+
970
+ > **영역만 덮을 때는 `SLoadingContainer` 를 직접 배치한다** (`open` prop 제어). `loading` 은 화면 전체를 잠그는 전역 오버레이 전용이다.
971
+
898
972
  ### 3-4. 테이블 컬럼 — 정렬·너비·헤더
899
973
 
900
974
  #### 정렬과 너비는 같은 표에서 정한다
@@ -1919,13 +1993,23 @@ const [selectedId, setSelectedId] = useState<string>();
1919
1993
  - **크기는 `ratio` 와 너비로 준다.** `ratio` 기본값이 `1`(정사각형)이라 너비만 주면 정사각형이 된다. `className`/`style` 로 높이를 직접 주면 그쪽이 이기고 `ratio` 는 무시된다.
1920
1994
  - 상자를 채우는 방식은 `fit`(기본 `cover`), 붙는 위치는 `position` 이다. 안쪽 `<img>` 에 네이티브 속성이 필요하면 `imgProps` 로 넘긴다 — 목록에서 고스트 드래그를 막는 `imgProps={{ draggable: false }}` 가 대표적이다.
1921
1995
  - 썸네일이 많은 목록에서는 `loadingShowDelay` 를 준다. 캐시된 이미지가 즉시 로드될 때 스피너가 한 프레임 번쩍이는 것을 막는다.
1996
+ - **썸네일이 작아 내용을 알아볼 수 없으면 `previewOnHover` 를 켠다.** 호버하면 큰 이미지가 포털로 뜬다 — 상품 목록에서 사진을 확인하려고 상세 화면까지 들어가는 일을 없앤다. **툴팁이나 팝오버로 직접 만들지 않는다**: 미리보기는 마우스를 통과시켜 아래 행의 클릭을 가로채지 않고, 원본의 로딩·실패도 스스로 표현한다.
1997
+ - 목록 썸네일과 원본 URL 이 다르면 원본을 `previewSrc` 로 준다. 주지 않으면 썸네일을 그대로 확대해서 흐릿하다.
1998
+ - **뜨는 자리를 앱이 정하지 않는다.** 미리보기는 썸네일 한가운데에서 오른쪽 아래로 펼쳐지고, 창 밖으로 넘치면 넘친 만큼만 왼쪽·위로 밀린다(반대편으로 뒤집히지 않는다). 방향을 고르는 prop 은 없다.
1999
+ - 크기는 `previewSize` 로 줄일 수 있고 **상한은 200px** 이다. 그보다 큰 값을 줘도 200 으로 잘린다 — 미리보기가 화면을 덮지 않게 하는 규격이다.
2000
+ - 미리보기 상자는 정사각형이고 이미지는 `contain` 으로 들어간다 — 세로로 긴 이미지도 잘리지 않는다. `ratio`·`fit` 은 썸네일 쪽에만 적용된다.
1922
2001
 
1923
2002
  ```tsx
1924
2003
  ✅ <SImage src={item.thumbnailUrl} alt={item.name} style={{ width: 64 }} /> {/* 정사각 썸네일 */}
1925
2004
  ✅ <SImage src={banner} ratio={16 / 9} className="w-full" /> {/* 가로형 배너 */}
2005
+ ✅ <SImage src={item.thumbnailUrl} previewOnHover previewSrc={item.imageUrl} {/* 호버로 원본 확인 */}
2006
+ alt={item.name} style={{ width: 64 }} />
1926
2007
 
1927
2008
  ❌ <img src={item.thumbnailUrl} /> {/* 로딩·실패 상태가 없다 */}
1928
2009
  ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
2010
+ ❌ <STooltip content={<img src={item.imageUrl} />}> {/* previewOnHover 를 쓴다 */}
2011
+ <SImage src={item.thumbnailUrl} />
2012
+ </STooltip>
1929
2013
  ```
1930
2014
 
1931
2015
  #### 3-7-14. 로고 — SLogo
@@ -3119,6 +3203,7 @@ export default function ExcelFilePopupPage() {
3119
3203
  - [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4), 카드 안에서 닫히는 액션도 같은 prop 으로 넘겼는가 (§3-7-8)
3120
3204
  - [ ] 아래에 버튼이 있는 자리(모달·드로어·카드)의 `STextarea autogrow` 에 `maxRows` 를 함께 줬는가 (§3-7-1 — 없으면 긴 글이 버튼을 화면 밖으로 밀어낸다)
3121
3205
  - [ ] 앱 부트스트랩의 Provider 안쪽에 `<SModalOutlet />` 이 한 번 렌더되어 있는가 (§4-1 — 없으면 모달 안에서 앱 훅이 죽는다), 그 대신으로 모달 컴포넌트를 Provider 로 다시 감싸지 않았는가
3206
+ - [ ] 전역 로딩의 `loading.show()` 반환 핸들을 잡아 `finally` 에서 그것만 불렀는가, 메시지를 바꾸려고 `show()` 를 두 번 부르지는 않았는가 (§3-3-6 — 걷기를 빠뜨리면 화면이 갇히고, 두 번 부르면 하나 더 쌓여 오버레이가 남는다)
3122
3207
  - [ ] 고른 컴포넌트를 §2-0 의 제 층에 놓았는가 (요소를 `SPage` 에 직접 놓지 않았는가, 블록을 `div` 로 감싸지 않았는가)
3123
3208
  - [ ] §2-0 포함 규칙을 지켰는가 (카드 안 카드 ❌, 표 셀 안 블록 ❌)
3124
3209
  - [ ] 블록 순서가 §2-0 순서와 맞는가 (제목 → 안내 → 필터 → 툴바 → 본문 → 페이지네이션 → 하단 액션)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sellmate-design-system-react",
3
- "version": "9.0.0-beta.66",
3
+ "version": "9.0.0-beta.67",
4
4
  "description": "Sellmate Design System — React (TypeScript + Tailwind v4) port",
5
5
  "license": "UNLICENSED",
6
6
  "keywords": [