@makitt.io/mds-mcp-server 0.1.2 → 0.2.0
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/README.md +142 -41
- package/dist/catalog.d.ts +16 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +383 -0
- package/dist/catalog.js.map +1 -0
- package/dist/data/catalog.json +42535 -5019
- package/dist/data/playbook/ai-fill.md +62 -49
- package/dist/data/playbook/anti-patterns.md +112 -110
- package/dist/data/playbook/array-input.md +95 -50
- package/dist/data/playbook/async-states.md +71 -61
- package/dist/data/playbook/data-grid.md +177 -80
- package/dist/data/playbook/feedback.md +107 -88
- package/dist/data/playbook/form.md +164 -134
- package/dist/data/playbook/overlay.md +98 -89
- package/dist/data/playbook/page-layout.md +96 -77
- package/dist/data/playbook/responsive-tokens.md +78 -59
- package/dist/data/recipes/admin-list-page.md +86 -0
- package/dist/data/recipes/async-state-section.md +60 -0
- package/dist/data/recipes/dashboard-overview.md +65 -0
- package/dist/data/recipes/detail-drawer.md +69 -0
- package/dist/data/recipes/modal-form.md +67 -0
- package/dist/data/recipes/settings-form-page.md +79 -0
- package/dist/index.d.ts +4 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -339
- package/dist/index.js.map +1 -1
- package/dist/loaders.d.ts +8 -0
- package/dist/loaders.d.ts.map +1 -0
- package/dist/loaders.js +120 -0
- package/dist/loaders.js.map +1 -0
- package/dist/recipes.d.ts +13 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.js +82 -0
- package/dist/recipes.js.map +1 -0
- package/dist/responses.d.ts +8 -0
- package/dist/responses.d.ts.map +1 -0
- package/dist/responses.js +25 -0
- package/dist/responses.js.map +1 -0
- package/dist/text.d.ts +4 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +20 -0
- package/dist/text.js.map +1 -0
- package/dist/tool-definitions.d.ts +3 -0
- package/dist/tool-definitions.d.ts.map +1 -0
- package/dist/tool-definitions.js +199 -0
- package/dist/tool-definitions.js.map +1 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +233 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +20 -16
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MDS Playbook — Array Input + Nested Entity
|
|
2
2
|
|
|
3
|
-
배열형 input (반복 필드) + 메인 form 안 sub-entity
|
|
3
|
+
배열형 input (반복 필드) + 메인 form 안 sub-entity 편집에 대한 약속.
|
|
4
4
|
|
|
5
5
|
> RHF `useFieldArray` 표준 + drawer 편집 패턴.
|
|
6
6
|
|
|
@@ -8,25 +8,25 @@
|
|
|
8
8
|
|
|
9
9
|
## 1. 4 패턴 분리
|
|
10
10
|
|
|
11
|
-
| 패턴
|
|
12
|
-
|
|
13
|
-
| **짧은 동질 배열** (태그 / 카테고리)
|
|
14
|
-
| **가변 row sub-form**
|
|
15
|
-
| **nested entity 다수** (정책 / 결제 옵션) | item 이 큰 form, 별 entity
|
|
16
|
-
| **reorder 가능**
|
|
11
|
+
| 패턴 | 사용 시점 | 컴포넌트 |
|
|
12
|
+
| ----------------------------------------- | ------------------------------------- | --------------------------------------------------- |
|
|
13
|
+
| **짧은 동질 배열** (태그 / 카테고리) | item = 단순 string, ≤ 20 개 | `<ChipToggle>` (compounds) |
|
|
14
|
+
| **가변 row sub-form** | item 이 small form (예: variant 가격) | `useFieldArray` + `<Stack>` row + add/remove |
|
|
15
|
+
| **nested entity 다수** (정책 / 결제 옵션) | item 이 큰 form, 별 entity | `useFieldArray` row 요약 + click → **Drawer 편집** |
|
|
16
|
+
| **reorder 가능** | drag handle 으로 순서 변경 | `<Tree>` (compounds) 또는 `useFieldArray` + dnd-kit |
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
20
|
## 2. 사용 시점 결정표
|
|
21
21
|
|
|
22
|
-
| 케이스
|
|
23
|
-
|
|
24
|
-
| "상품 태그 (text chip 다수)"
|
|
25
|
-
| "주문 항목 (상품 + 수량 + 가격 3 필드 row)"
|
|
22
|
+
| 케이스 | 답 |
|
|
23
|
+
| -------------------------------------------------------------- | ------------------------------------------------------ |
|
|
24
|
+
| "상품 태그 (text chip 다수)" | `<ChipToggle>` (다중 토글) |
|
|
25
|
+
| "주문 항목 (상품 + 수량 + 가격 3 필드 row)" | `useFieldArray` + Stack row + IconButton remove |
|
|
26
26
|
| "변형 (variant: 색 / size / 가격 / 재고 4 필드, 각 row 가 큼)" | `useFieldArray` + 행 요약 + click → Drawer 의 sub-form |
|
|
27
|
-
| "정책 (각 정책이 큰 entity)"
|
|
28
|
-
| "image 다수 + reorder"
|
|
29
|
-
| "FAQ list (질문/답 row, 다수)"
|
|
27
|
+
| "정책 (각 정책이 큰 entity)" | nested drawer-form (overlay.md §7) |
|
|
28
|
+
| "image 다수 + reorder" | `<Tree>` (compounds) 또는 dnd-kit + useFieldArray |
|
|
29
|
+
| "FAQ list (질문/답 row, 다수)" | useFieldArray + Stack + Drawer 의 큰 답변 input |
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
name="tags"
|
|
40
40
|
control={control}
|
|
41
41
|
render={({ field }) => (
|
|
42
|
-
<ChipToggle.Group
|
|
42
|
+
<ChipToggle.Group
|
|
43
|
+
value={field.value}
|
|
44
|
+
onValueChange={field.onChange}
|
|
45
|
+
multiple
|
|
46
|
+
>
|
|
43
47
|
<ChipToggle.Item value="new">신상</ChipToggle.Item>
|
|
44
48
|
<ChipToggle.Item value="sale">세일</ChipToggle.Item>
|
|
45
49
|
<ChipToggle.Item value="featured">추천</ChipToggle.Item>
|
|
@@ -51,7 +55,8 @@
|
|
|
51
55
|
|
|
52
56
|
- 다중 선택 = `multiple` prop
|
|
53
57
|
- 단일 선택 = 그 prop 없음 (default)
|
|
54
|
-
- caller 가 새 chip 추가 불가 (predefined list 만) — 새 추가 = `Combobox` 또는
|
|
58
|
+
- caller 가 새 chip 추가 불가 (predefined list 만) — 새 추가 = `Combobox` 또는
|
|
59
|
+
별도 input
|
|
55
60
|
|
|
56
61
|
---
|
|
57
62
|
|
|
@@ -73,11 +78,18 @@ function VariantPriceForm() {
|
|
|
73
78
|
<Stack key={field.id} direction="row" gap="2" align="center">
|
|
74
79
|
<Field.Root>
|
|
75
80
|
<Field.Label visuallyHidden>이름</Field.Label>
|
|
76
|
-
<TextField
|
|
81
|
+
<TextField
|
|
82
|
+
{...register(`variants.${idx}.name`)}
|
|
83
|
+
placeholder="이름"
|
|
84
|
+
/>
|
|
77
85
|
</Field.Root>
|
|
78
86
|
<Field.Root>
|
|
79
87
|
<Field.Label visuallyHidden>가격</Field.Label>
|
|
80
|
-
<NumberInput
|
|
88
|
+
<NumberInput
|
|
89
|
+
{...register(`variants.${idx}.price`)}
|
|
90
|
+
step={100}
|
|
91
|
+
suffix="원"
|
|
92
|
+
/>
|
|
81
93
|
</Field.Root>
|
|
82
94
|
<IconButton
|
|
83
95
|
aria-label={`${field.name} 삭제`}
|
|
@@ -103,21 +115,25 @@ function VariantPriceForm() {
|
|
|
103
115
|
|
|
104
116
|
### Add / Remove UI 표준
|
|
105
117
|
|
|
106
|
-
| 위치
|
|
107
|
-
|
|
108
|
-
| Add 버튼
|
|
109
|
-
| Remove 버튼
|
|
110
|
-
| 위험 (이미 저장된 entity 삭제) | `Modal.confirm` 확인 후 remove | —
|
|
118
|
+
| 위치 | 컴포넌트 | 라벨 |
|
|
119
|
+
| ------------------------------ | ------------------------------ | -------------------------------------------------------- |
|
|
120
|
+
| Add 버튼 | row 마지막 아래 (또는 위) | `Button variant="ghost" leadingIcon=<PlusIcon>{...}추가` |
|
|
121
|
+
| Remove 버튼 | row 끝 우측 | `IconButton variant="ghost"` + aria-label |
|
|
122
|
+
| 위험 (이미 저장된 entity 삭제) | `Modal.confirm` 확인 후 remove | — |
|
|
111
123
|
|
|
112
124
|
---
|
|
113
125
|
|
|
114
126
|
## 5. nested entity 다수 — drawer 편집
|
|
115
127
|
|
|
116
|
-
큰 sub-entity (variant / 정책 / 결제 옵션) — 행 요약 + click → Drawer 의 full
|
|
128
|
+
큰 sub-entity (variant / 정책 / 결제 옵션) — 행 요약 + click → Drawer 의 full
|
|
129
|
+
form.
|
|
117
130
|
|
|
118
131
|
```tsx
|
|
119
132
|
function PolicyList() {
|
|
120
|
-
const { fields, append, remove, update } = useFieldArray({
|
|
133
|
+
const { fields, append, remove, update } = useFieldArray({
|
|
134
|
+
control,
|
|
135
|
+
name: 'policies',
|
|
136
|
+
});
|
|
121
137
|
const [editIdx, setEditIdx] = useState<number | null>(null);
|
|
122
138
|
|
|
123
139
|
return (
|
|
@@ -135,21 +151,38 @@ function PolicyList() {
|
|
|
135
151
|
</Card.Root>
|
|
136
152
|
))}
|
|
137
153
|
</Stack>
|
|
138
|
-
<Button
|
|
154
|
+
<Button
|
|
155
|
+
variant="ghost"
|
|
156
|
+
leadingIcon={<PlusIcon />}
|
|
157
|
+
onClick={() => setEditIdx(fields.length)}
|
|
158
|
+
>
|
|
139
159
|
정책 추가
|
|
140
160
|
</Button>
|
|
141
|
-
<Drawer.Root
|
|
161
|
+
<Drawer.Root
|
|
162
|
+
open={editIdx !== null}
|
|
163
|
+
onOpenChange={(o) => !o && setEditIdx(null)}
|
|
164
|
+
>
|
|
142
165
|
<Drawer.Content side="right">
|
|
143
166
|
<PolicyForm
|
|
144
|
-
initialValue={
|
|
167
|
+
initialValue={
|
|
168
|
+
editIdx !== null && editIdx < fields.length
|
|
169
|
+
? fields[editIdx]
|
|
170
|
+
: undefined
|
|
171
|
+
}
|
|
145
172
|
onSubmit={(data) => {
|
|
146
|
-
if (editIdx !== null && editIdx < fields.length)
|
|
173
|
+
if (editIdx !== null && editIdx < fields.length)
|
|
174
|
+
update(editIdx, data);
|
|
147
175
|
else append(data);
|
|
148
176
|
setEditIdx(null);
|
|
149
177
|
}}
|
|
150
|
-
onDelete={
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
onDelete={
|
|
179
|
+
editIdx !== null && editIdx < fields.length
|
|
180
|
+
? () => {
|
|
181
|
+
remove(editIdx);
|
|
182
|
+
setEditIdx(null);
|
|
183
|
+
}
|
|
184
|
+
: undefined
|
|
185
|
+
}
|
|
153
186
|
/>
|
|
154
187
|
</Drawer.Content>
|
|
155
188
|
</Drawer.Root>
|
|
@@ -166,12 +199,17 @@ function PolicyList() {
|
|
|
166
199
|
## 6. Reorder
|
|
167
200
|
|
|
168
201
|
mds 의 자체 dnd 없음. 옵션:
|
|
202
|
+
|
|
169
203
|
- **Tree (compounds)** — Radix 기반 nested reorder
|
|
170
204
|
- **useFieldArray + dnd-kit** — drag handle 의 manual integration
|
|
171
205
|
|
|
172
206
|
```tsx
|
|
173
207
|
import { DndContext, closestCenter } from '@dnd-kit/core';
|
|
174
|
-
import {
|
|
208
|
+
import {
|
|
209
|
+
SortableContext,
|
|
210
|
+
useSortable,
|
|
211
|
+
verticalListSortingStrategy,
|
|
212
|
+
} from '@dnd-kit/sortable';
|
|
175
213
|
|
|
176
214
|
function SortableArray() {
|
|
177
215
|
const { fields, move } = useFieldArray({ control, name: 'items' });
|
|
@@ -184,7 +222,10 @@ function SortableArray() {
|
|
|
184
222
|
if (oldIndex !== -1 && newIndex !== -1) move(oldIndex, newIndex);
|
|
185
223
|
}}
|
|
186
224
|
>
|
|
187
|
-
<SortableContext
|
|
225
|
+
<SortableContext
|
|
226
|
+
items={fields.map((f) => f.id)}
|
|
227
|
+
strategy={verticalListSortingStrategy}
|
|
228
|
+
>
|
|
188
229
|
{fields.map((field, idx) => (
|
|
189
230
|
<SortableRow key={field.id} id={field.id} {...field} />
|
|
190
231
|
))}
|
|
@@ -198,43 +239,46 @@ function SortableArray() {
|
|
|
198
239
|
|
|
199
240
|
## 7. Mobile 변형
|
|
200
241
|
|
|
201
|
-
| Pattern
|
|
202
|
-
|
|
203
|
-
| ChipToggle
|
|
242
|
+
| Pattern | Mobile |
|
|
243
|
+
| --------------------------- | ----------------------------------------------------- |
|
|
244
|
+
| ChipToggle | wrap full-width, scroll horizontal if many |
|
|
204
245
|
| useFieldArray + Stack (row) | `direction="column"` (vertical) + add / remove 그대로 |
|
|
205
|
-
| nested entity (Drawer)
|
|
206
|
-
| Reorder (dnd-kit)
|
|
246
|
+
| nested entity (Drawer) | Drawer → bottom Sheet (overlay.md §8) |
|
|
247
|
+
| Reorder (dnd-kit) | touch-friendly drag handle |
|
|
207
248
|
|
|
208
249
|
---
|
|
209
250
|
|
|
210
251
|
## 8. 안티 패턴
|
|
211
252
|
|
|
212
|
-
- ❌ **무한 깊은 nested** (drawer 안 drawer 안 drawer) — 2 단 max (메인 form +
|
|
253
|
+
- ❌ **무한 깊은 nested** (drawer 안 drawer 안 drawer) — 2 단 max (메인 form +
|
|
254
|
+
sub-entity drawer)
|
|
213
255
|
- ❌ **inline edit + drawer 편집 동시** — 한쪽만
|
|
214
256
|
- ❌ **add 버튼이 row 사이** — row 끝에만 (mental scan 깨짐)
|
|
215
257
|
- ❌ **remove 의 confirm 없음** (이미 저장된 entity) — `Modal.confirm`
|
|
216
258
|
- ❌ **0 row 의 empty state 없음** — `<EmptyState size="sm">` 또는 placeholder
|
|
217
|
-
- ❌ **row 의 a11y 없는 reorder** (drag only) — keyboard 도 가능 (dnd-kit 자체
|
|
259
|
+
- ❌ **row 의 a11y 없는 reorder** (drag only) — keyboard 도 가능 (dnd-kit 자체
|
|
260
|
+
keyboard sensor)
|
|
218
261
|
- ❌ **submit 시 useFieldArray 의 빈 row 도 valid** — Zod schema 의 .min(1) 검증
|
|
219
262
|
|
|
220
263
|
---
|
|
221
264
|
|
|
222
265
|
## 9. Cross-cutting
|
|
223
266
|
|
|
224
|
-
| Axis
|
|
225
|
-
|
|
226
|
-
| **Responsive** | row → column (mobile)
|
|
227
|
-
| **A11y**
|
|
228
|
-
| **i18n**
|
|
229
|
-
| **Catalog**
|
|
230
|
-
| **Validation** | Zod `array().min(N).max(N)`
|
|
267
|
+
| Axis | 적용 |
|
|
268
|
+
| -------------- | ---------------------------------------------------------------------------- |
|
|
269
|
+
| **Responsive** | row → column (mobile) |
|
|
270
|
+
| **A11y** | each row 의 aria-label / remove button aria-label / dnd-kit keyboard support |
|
|
271
|
+
| **i18n** | "추가" / "삭제" / "변형" 등 모두 `t()` |
|
|
272
|
+
| **Catalog** | useFieldArray 의 standard pattern 자동 catalog |
|
|
273
|
+
| **Validation** | Zod `array().min(N).max(N)` |
|
|
231
274
|
|
|
232
275
|
---
|
|
233
276
|
|
|
234
277
|
## 10. TBD
|
|
235
278
|
|
|
236
279
|
1. **dnd-kit mds 통합** — `<SortableArray>` 같은 mds wrapper 컴포넌트 추가
|
|
237
|
-
2. **drawer 안 isDirty confirm** — 메인 form
|
|
280
|
+
2. **drawer 안 isDirty confirm** — 메인 form isDirty 와 drawer isDirty 의 통합
|
|
281
|
+
처리
|
|
238
282
|
3. **add 위치** — row 끝 vs row 사이 (행간 + 버튼) 둘 다 허용 vs 표준화
|
|
239
283
|
4. **empty state 표준** — useFieldArray 의 0 row 시 표시 패턴
|
|
240
284
|
5. **Bulk add** — CSV / paste 로 다 row 한 번에 (변형 가격 list 같은)
|
|
@@ -246,4 +290,5 @@ function SortableArray() {
|
|
|
246
290
|
- [form.md](./form.md) — useFieldArray 의 form 안 통합 (Step 4.1)
|
|
247
291
|
- [overlay.md](./overlay.md) — nested drawer 합성 규칙 (Step 4.4)
|
|
248
292
|
- [async-states.md](./async-states.md) — 0 row 의 empty state (Step 4.7)
|
|
249
|
-
- [data-grid.md](./data-grid.md) — DataGrid 의 row 자체가 다른 entity 의 array
|
|
293
|
+
- [data-grid.md](./data-grid.md) — DataGrid 의 row 자체가 다른 entity 의 array
|
|
294
|
+
(Step 4.3)
|
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
|
|
10
10
|
## 1. 5 Micro-pattern 분리
|
|
11
11
|
|
|
12
|
-
| Pattern
|
|
13
|
-
|
|
14
|
-
| **Page-level loading**
|
|
15
|
-
| **Section-level loading** | Card / Section 안 일부 데이터 fetch
|
|
16
|
-
| **Inline loading**
|
|
17
|
-
| **Action loading**
|
|
18
|
-
| **Background loading**
|
|
12
|
+
| Pattern | 언제 | 컴포넌트 | UX |
|
|
13
|
+
| ------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
14
|
+
| **Page-level loading** | route 진입 시 초기 데이터 fetch (예: `/customers` 진입) | `SkeletonRows` (DataGrid) / `<Skeleton variant="page">` / Next.js `loading.tsx` | shell + sidebar 유지 + content area 만 skeleton |
|
|
15
|
+
| **Section-level loading** | Card / Section 안 일부 데이터 fetch | `<Skeleton>` 내부 fill | 다른 section 은 정상 |
|
|
16
|
+
| **Inline loading** | DataGrid row / Table cell 내부 | `SkeletonRows` (Table.Body 안) | row 자리 차지 + animation |
|
|
17
|
+
| **Action loading** | mutation 진행 중 (button click → 응답 대기) | `<Button loading>` (자체 spinner + 비활성) | 사용자가 누른 button 만 loading |
|
|
18
|
+
| **Background loading** | refetch / polling (사용자가 trigger 안 함) | indicator X 또는 corner toast | 화면 멈춤 X |
|
|
19
19
|
|
|
20
20
|
> **핵심 차이** — 사용자 의식 여부.
|
|
21
|
+
>
|
|
21
22
|
> - Page / Section / Inline = 사용자가 보고 있음 → skeleton (예상 layout)
|
|
22
23
|
> - Action = 사용자가 trigger → button loading (응답 대기 명확)
|
|
23
24
|
> - Background = 사용자 모름 → indicator 없음 또는 작게
|
|
@@ -26,28 +27,29 @@
|
|
|
26
27
|
|
|
27
28
|
## 2. Error 상태
|
|
28
29
|
|
|
29
|
-
| 상황
|
|
30
|
-
|
|
31
|
-
| **Page-level error** (route 진입 시 fetch 실패) | `ErrorState` (patterns) — error icon + 메시지 + "다시 시도" 버튼
|
|
32
|
-
| **Section-level error**
|
|
33
|
-
| **DataGrid 의 error**
|
|
34
|
-
| **Action error** (mutation 실패)
|
|
35
|
-
| **Form 422**
|
|
36
|
-
| **Field 검증 error**
|
|
30
|
+
| 상황 | 컴포넌트 | 위치 |
|
|
31
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------- | ----------------- |
|
|
32
|
+
| **Page-level error** (route 진입 시 fetch 실패) | `ErrorState` (patterns) — error icon + 메시지 + "다시 시도" 버튼 | content area 전체 |
|
|
33
|
+
| **Section-level error** | `ErrorState size="sm"` 또는 inline error message | Card / Section 안 |
|
|
34
|
+
| **DataGrid 의 error** | `DataGrid errorState` prop — default `ErrorState` 또는 caller override | toolbar 아래 |
|
|
35
|
+
| **Action error** (mutation 실패) | `toast.error("저장 실패", { description: getErrorDisplayMessage(error) })` | global toast |
|
|
36
|
+
| **Form 422** | `Banner` 상단 + `Field.Error` inline (form.md §4 참조) | form 안 |
|
|
37
|
+
| **Field 검증 error** | `Field.Error` slot | input 아래 |
|
|
37
38
|
|
|
38
|
-
> **mds 의 `ErrorState`** = `EmptyState variant="error"` 의 wrapper.
|
|
39
|
-
>
|
|
39
|
+
> **mds 의 `ErrorState`** = `EmptyState variant="error"` 의 wrapper. `error`
|
|
40
|
+
> prop 으로 unknown 받아 자동 description 추출 (`getErrorDisplayMessage` from
|
|
41
|
+
> `@makitt/shared-utils`).
|
|
40
42
|
|
|
41
43
|
---
|
|
42
44
|
|
|
43
45
|
## 3. Empty 상태
|
|
44
46
|
|
|
45
|
-
| 상황
|
|
46
|
-
|
|
47
|
-
| **첫 사용자** (한 번도 안 만들었음)
|
|
48
|
-
| **필터 결과 0**
|
|
49
|
-
| **이전엔 있었지만 지금 0** (예: 모두 삭제) | `EmptyState variant="default"`
|
|
50
|
-
| **DataGrid empty**
|
|
47
|
+
| 상황 | 컴포넌트 | 메시지 패턴 |
|
|
48
|
+
| ------------------------------------------ | ------------------------------------------------- | --------------------------------------------- |
|
|
49
|
+
| **첫 사용자** (한 번도 안 만들었음) | `EmptyState variant="onboarding"` | icon + 안내 + primary action ("첫 X 만들기") |
|
|
50
|
+
| **필터 결과 0** | `EmptyState variant="search"` | icon + "검색 결과 없음" + "필터 초기화" |
|
|
51
|
+
| **이전엔 있었지만 지금 0** (예: 모두 삭제) | `EmptyState variant="default"` | icon + 메시지 만 (action 없음 또는 secondary) |
|
|
52
|
+
| **DataGrid empty** | `DataGrid emptyState` prop default — `EmptyState` | toolbar 아래 |
|
|
51
53
|
|
|
52
54
|
### Empty 의 의도 명확화
|
|
53
55
|
|
|
@@ -59,72 +61,78 @@
|
|
|
59
61
|
|
|
60
62
|
## 4. 결정표 (lookup)
|
|
61
63
|
|
|
62
|
-
| 케이스
|
|
63
|
-
|
|
64
|
-
| "page 진입 시 list 로딩 중"
|
|
65
|
-
| "card 안 stat 로딩 중"
|
|
66
|
-
| "button click → mutation 응답 대기" | `<Button loading>`
|
|
67
|
-
| "background refetch"
|
|
68
|
-
| "page 진입 실패 (네트워크)"
|
|
69
|
-
| "list 의 검색 결과 0"
|
|
70
|
-
| "고객 한 번도 안 만들었음"
|
|
71
|
-
| "form submit → 422 검증"
|
|
72
|
-
| "form submit → 500 네트워크"
|
|
73
|
-
| "mutation success 즉시 toast"
|
|
64
|
+
| 케이스 | 답 |
|
|
65
|
+
| ----------------------------------- | ------------------------------------------------------------ |
|
|
66
|
+
| "page 진입 시 list 로딩 중" | `SkeletonRows` (table) / `Skeleton` (page) |
|
|
67
|
+
| "card 안 stat 로딩 중" | `<Skeleton variant="text">` 또는 `<Skeleton variant="rect">` |
|
|
68
|
+
| "button click → mutation 응답 대기" | `<Button loading>` |
|
|
69
|
+
| "background refetch" | indicator 없음 (또는 corner `toast.info("동기화 중")`) |
|
|
70
|
+
| "page 진입 실패 (네트워크)" | `<ErrorState error={err} onRetry={refetch}>` content area |
|
|
71
|
+
| "list 의 검색 결과 0" | `<EmptyState variant="search" action={resetFilters}>` |
|
|
72
|
+
| "고객 한 번도 안 만들었음" | `<EmptyState variant="onboarding" action={createCustomer}>` |
|
|
73
|
+
| "form submit → 422 검증" | `<Banner>` 상단 요약 + `<Field.Error>` 분배 |
|
|
74
|
+
| "form submit → 500 네트워크" | `toast.error(...)` |
|
|
75
|
+
| "mutation success 즉시 toast" | `toast.success(...)` |
|
|
74
76
|
|
|
75
77
|
---
|
|
76
78
|
|
|
77
79
|
## 5. Mobile 변형
|
|
78
80
|
|
|
79
|
-
| Pattern
|
|
80
|
-
|
|
81
|
-
| Page-level skeleton
|
|
82
|
-
| Section skeleton
|
|
83
|
-
| Inline (table row) skeleton | DataGrid → card list (Step 4.3) 의 card skeleton
|
|
84
|
-
| Action button loading
|
|
85
|
-
| Background
|
|
81
|
+
| Pattern | Mobile 변형 |
|
|
82
|
+
| --------------------------- | --------------------------------------------------------------------------- |
|
|
83
|
+
| Page-level skeleton | shell sidebar → drawer (Step 4.5 Page Layout) + content full-width skeleton |
|
|
84
|
+
| Section skeleton | 그대로 (vertical stack) |
|
|
85
|
+
| Inline (table row) skeleton | DataGrid → card list (Step 4.3) 의 card skeleton |
|
|
86
|
+
| Action button loading | 그대로 (full-width 가능) |
|
|
87
|
+
| Background | 그대로 |
|
|
86
88
|
|
|
87
89
|
---
|
|
88
90
|
|
|
89
91
|
## 6. 안티 패턴
|
|
90
92
|
|
|
91
|
-
- ❌ **Spinner 사용** (`<Spinner>`) — skeleton 우선. Spinner 는 의도 모호 /
|
|
92
|
-
|
|
93
|
+
- ❌ **Spinner 사용** (`<Spinner>`) — skeleton 우선. Spinner 는 의도 모호 /
|
|
94
|
+
placeholder X / 시간 불확실
|
|
95
|
+
- ❌ **Loading 중 화면 깜빡임** — skeleton 의 시간 = 최소 200ms (너무 빨리
|
|
96
|
+
사라지면 깜빡임)
|
|
93
97
|
- ❌ **Action button loading 외 indicator** (button + 별도 progress bar) — 중복
|
|
94
98
|
- ❌ **Background 의 modal loading** — 사용자 인터럽트
|
|
95
99
|
- ❌ **Empty 의 "다시 시도" 만** — empty != error. `EmptyState` 의 의도 다름
|
|
96
|
-
- ❌ **Error 시 toast 만** — page-level 은 ErrorState content area. toast 는
|
|
97
|
-
|
|
100
|
+
- ❌ **Error 시 toast 만** — page-level 은 ErrorState content area. toast 는
|
|
101
|
+
background / action
|
|
102
|
+
- ❌ **Skeleton 의 정확하지 않은 layout** — skeleton 가 실 content 와 다른 shape
|
|
103
|
+
→ 깜빡임 효과
|
|
98
104
|
|
|
99
105
|
---
|
|
100
106
|
|
|
101
107
|
## 7. Cross-cutting
|
|
102
108
|
|
|
103
|
-
| Axis
|
|
104
|
-
|
|
105
|
-
| **Responsive** | 모든 micro-pattern 의 mobile 변형 명시
|
|
106
|
-
| **A11y**
|
|
107
|
-
| **Telemetry**
|
|
108
|
-
| **i18n**
|
|
109
|
+
| Axis | 적용 |
|
|
110
|
+
| -------------- | ------------------------------------------------------------------------------------- |
|
|
111
|
+
| **Responsive** | 모든 micro-pattern 의 mobile 변형 명시 |
|
|
112
|
+
| **A11y** | Skeleton 의 `aria-busy="true"` + `aria-label="로딩 중"`. ErrorState 의 `role="alert"` |
|
|
113
|
+
| **Telemetry** | error 의 자동 log (Sentry 등) — Step 7 Web Playbook |
|
|
114
|
+
| **i18n** | Error / Empty 메시지 모두 `t()` 통과 |
|
|
109
115
|
|
|
110
116
|
---
|
|
111
117
|
|
|
112
118
|
## 8. 5 Pattern 의 실 사용 예 (apps/web 도메인)
|
|
113
119
|
|
|
114
|
-
| 페이지
|
|
115
|
-
|
|
116
|
-
| `/merchant/customers` (list)
|
|
117
|
-
| `/merchant/products/new` (form)
|
|
118
|
-
| `/merchant/orders/[orderId]` (detail) | Page (header + body skeleton) + Action (status change) + Background (poll for refund webhook)
|
|
120
|
+
| 페이지 | Pattern 사용 |
|
|
121
|
+
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
122
|
+
| `/merchant/customers` (list) | Page (SkeletonRows) + Section (KPI card stat skeleton) + Inline (row sparkline skeleton) + Action (bulk action button) + Background (auto-refresh polling) |
|
|
123
|
+
| `/merchant/products/new` (form) | Section (form 안 reference data fetch skeleton) + Action (Save button) + Form Error (422 → Banner + Field.Error) |
|
|
124
|
+
| `/merchant/orders/[orderId]` (detail) | Page (header + body skeleton) + Action (status change) + Background (poll for refund webhook) |
|
|
119
125
|
|
|
120
126
|
---
|
|
121
127
|
|
|
122
128
|
## 9. TBD — 사용자 합의 대기
|
|
123
129
|
|
|
124
|
-
1. **Spinner 사용 정책** — 완전 금지 vs 특수 case (예: button 안 inline, 또는
|
|
130
|
+
1. **Spinner 사용 정책** — 완전 금지 vs 특수 case (예: button 안 inline, 또는
|
|
131
|
+
background corner indicator)
|
|
125
132
|
2. **Skeleton 의 최소 표시 시간** — 200ms 깜빡임 방지. 정확 ms 결정
|
|
126
133
|
3. **Background refetch indicator** — 완전 silent vs corner toast / progress bar
|
|
127
|
-
4. **Empty 의 자동 onboarding 감지** — first-time user 판단 방법 (서버 flag /
|
|
134
|
+
4. **Empty 의 자동 onboarding 감지** — first-time user 판단 방법 (서버 flag /
|
|
135
|
+
localStorage / row 0 시 모두 onboarding)
|
|
128
136
|
5. **Polling interval** — 표준 ms (예: 30s) 또는 page 마다 다름
|
|
129
137
|
|
|
130
138
|
---
|
|
@@ -133,5 +141,7 @@
|
|
|
133
141
|
|
|
134
142
|
- [form.md](./form.md) — Form 의 검증 / 저장 / submit 흐름 (Step 4.1)
|
|
135
143
|
- [feedback.md](./feedback.md) — Toast / Notification 의 일시 vs 영속 (Step 4.2)
|
|
136
|
-
- [data-grid.md](./data-grid.md) — DataGrid 의 loading/error/empty state prop
|
|
137
|
-
|
|
144
|
+
- [data-grid.md](./data-grid.md) — DataGrid 의 loading/error/empty state prop
|
|
145
|
+
(Step 4.3)
|
|
146
|
+
- [page-layout.md](./page-layout.md) — Page-level skeleton 의 shell 위치 (Step
|
|
147
|
+
4.5)
|