@makitt.io/mds-mcp-server 0.1.3 → 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.
Files changed (55) hide show
  1. package/README.md +135 -34
  2. package/dist/catalog.d.ts +16 -0
  3. package/dist/catalog.d.ts.map +1 -0
  4. package/dist/catalog.js +383 -0
  5. package/dist/catalog.js.map +1 -0
  6. package/dist/data/catalog.json +41643 -4127
  7. package/dist/data/playbook/ai-fill.md +61 -48
  8. package/dist/data/playbook/anti-patterns.md +112 -110
  9. package/dist/data/playbook/array-input.md +94 -49
  10. package/dist/data/playbook/async-states.md +71 -61
  11. package/dist/data/playbook/data-grid.md +118 -101
  12. package/dist/data/playbook/feedback.md +103 -84
  13. package/dist/data/playbook/form.md +164 -134
  14. package/dist/data/playbook/overlay.md +97 -88
  15. package/dist/data/playbook/page-layout.md +95 -76
  16. package/dist/data/playbook/responsive-tokens.md +77 -58
  17. package/dist/data/recipes/admin-list-page.md +86 -0
  18. package/dist/data/recipes/async-state-section.md +60 -0
  19. package/dist/data/recipes/dashboard-overview.md +65 -0
  20. package/dist/data/recipes/detail-drawer.md +69 -0
  21. package/dist/data/recipes/modal-form.md +67 -0
  22. package/dist/data/recipes/settings-form-page.md +79 -0
  23. package/dist/index.d.ts +4 -23
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +25 -339
  26. package/dist/index.js.map +1 -1
  27. package/dist/loaders.d.ts +8 -0
  28. package/dist/loaders.d.ts.map +1 -0
  29. package/dist/loaders.js +120 -0
  30. package/dist/loaders.js.map +1 -0
  31. package/dist/recipes.d.ts +13 -0
  32. package/dist/recipes.d.ts.map +1 -0
  33. package/dist/recipes.js +82 -0
  34. package/dist/recipes.js.map +1 -0
  35. package/dist/responses.d.ts +8 -0
  36. package/dist/responses.d.ts.map +1 -0
  37. package/dist/responses.js +25 -0
  38. package/dist/responses.js.map +1 -0
  39. package/dist/text.d.ts +4 -0
  40. package/dist/text.d.ts.map +1 -0
  41. package/dist/text.js +20 -0
  42. package/dist/text.js.map +1 -0
  43. package/dist/tool-definitions.d.ts +3 -0
  44. package/dist/tool-definitions.d.ts.map +1 -0
  45. package/dist/tool-definitions.js +199 -0
  46. package/dist/tool-definitions.js.map +1 -0
  47. package/dist/tools.d.ts +4 -0
  48. package/dist/tools.d.ts.map +1 -0
  49. package/dist/tools.js +233 -0
  50. package/dist/tools.js.map +1 -0
  51. package/dist/types.d.ts +107 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +10 -0
  54. package/dist/types.js.map +1 -0
  55. package/package.json +20 -16
@@ -8,25 +8,25 @@
8
8
 
9
9
  ## 1. 4 패턴 분리
10
10
 
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 |
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 다수)" | `<ChipToggle>` (다중 토글) |
25
- | "주문 항목 (상품 + 수량 + 가격 3 필드 row)" | `useFieldArray` + Stack row + IconButton remove |
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)" | nested drawer-form (overlay.md §7) |
28
- | "image 다수 + reorder" | `<Tree>` (compounds) 또는 dnd-kit + useFieldArray |
29
- | "FAQ list (질문/답 row, 다수)" | useFieldArray + Stack + Drawer 의 큰 답변 input |
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 value={field.value} onValueChange={field.onChange} multiple>
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` 또는 별도 input
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 {...register(`variants.${idx}.name`)} placeholder="이름" />
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 {...register(`variants.${idx}.price`)} step={100} suffix="원" />
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 버튼 | row 마지막 아래 (또는 위) | `Button variant="ghost" leadingIcon=<PlusIcon>{...}추가` |
109
- | Remove 버튼 | row 끝 우측 | `IconButton variant="ghost"` + aria-label |
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 form.
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({ control, name: 'policies' });
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 variant="ghost" leadingIcon={<PlusIcon />} onClick={() => setEditIdx(fields.length)}>
154
+ <Button
155
+ variant="ghost"
156
+ leadingIcon={<PlusIcon />}
157
+ onClick={() => setEditIdx(fields.length)}
158
+ >
139
159
  정책 추가
140
160
  </Button>
141
- <Drawer.Root open={editIdx !== null} onOpenChange={(o) => !o && setEditIdx(null)}>
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={editIdx !== null && editIdx < fields.length ? fields[editIdx] : undefined}
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) update(editIdx, data);
173
+ if (editIdx !== null && editIdx < fields.length)
174
+ update(editIdx, data);
147
175
  else append(data);
148
176
  setEditIdx(null);
149
177
  }}
150
- onDelete={editIdx !== null && editIdx < fields.length
151
- ? () => { remove(editIdx); setEditIdx(null); }
152
- : undefined}
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 { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
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 items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}>
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 | Mobile |
202
- |---|---|
203
- | ChipToggle | wrap full-width, scroll horizontal if many |
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) | Drawer → bottom Sheet (overlay.md §8) |
206
- | Reorder (dnd-kit) | touch-friendly drag handle |
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 + sub-entity drawer)
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 자체 keyboard sensor)
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** | each row 의 aria-label / remove button aria-label / dnd-kit keyboard support |
228
- | **i18n** | "추가" / "삭제" / "변형" 등 모두 `t()` |
229
- | **Catalog** | useFieldArray 의 standard pattern 자동 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 isDirty 와 drawer isDirty 의 통합 처리
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 (Step 4.3)
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 | 언제 | 컴포넌트 | 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 |
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 + 메시지 + "다시 시도" 버튼 | content area 전체 |
32
- | **Section-level error** | `ErrorState size="sm"` 또는 inline error message | Card / Section 안 |
33
- | **DataGrid 의 error** | `DataGrid errorState` prop — default `ErrorState` 또는 caller override | toolbar 아래 |
34
- | **Action error** (mutation 실패) | `toast.error("저장 실패", { description: getErrorDisplayMessage(error) })` | global toast |
35
- | **Form 422** | `Banner` 상단 + `Field.Error` inline (form.md §4 참조) | form 안 |
36
- | **Field 검증 error** | `Field.Error` slot | input 아래 |
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
- > `error` prop 으로 unknown 받아 자동 description 추출 (`getErrorDisplayMessage` from `@makitt/shared-utils`).
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
- | **첫 사용자** (한 번도 안 만들었음) | `EmptyState variant="onboarding"` | icon + 안내 + primary action ("첫 X 만들기") |
48
- | **필터 결과 0** | `EmptyState variant="search"` | icon + "검색 결과 없음" + "필터 초기화" |
49
- | **이전엔 있었지만 지금 0** (예: 모두 삭제) | `EmptyState variant="default"` | icon + 메시지 만 (action 없음 또는 secondary) |
50
- | **DataGrid empty** | `DataGrid emptyState` prop default — `EmptyState` | toolbar 아래 |
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 로딩 중" | `SkeletonRows` (table) / `Skeleton` (page) |
65
- | "card 안 stat 로딩 중" | `<Skeleton variant="text">` 또는 `<Skeleton variant="rect">` |
66
- | "button click → mutation 응답 대기" | `<Button loading>` |
67
- | "background refetch" | indicator 없음 (또는 corner `toast.info("동기화 중")`) |
68
- | "page 진입 실패 (네트워크)" | `<ErrorState error={err} onRetry={refetch}>` content area |
69
- | "list 의 검색 결과 0" | `<EmptyState variant="search" action={resetFilters}>` |
70
- | "고객 한 번도 안 만들었음" | `<EmptyState variant="onboarding" action={createCustomer}>` |
71
- | "form submit → 422 검증" | `<Banner>` 상단 요약 + `<Field.Error>` 분배 |
72
- | "form submit → 500 네트워크" | `toast.error(...)` |
73
- | "mutation success 즉시 toast" | `toast.success(...)` |
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 | Mobile 변형 |
80
- |---|---|
81
- | Page-level skeleton | shell sidebar → drawer (Step 4.5 Page Layout) + content full-width skeleton |
82
- | Section skeleton | 그대로 (vertical stack) |
83
- | Inline (table row) skeleton | DataGrid → card list (Step 4.3) 의 card skeleton |
84
- | Action button loading | 그대로 (full-width 가능) |
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 는 의도 모호 / placeholder X / 시간 불확실
92
- - **Loading 중 화면 깜빡임** — skeleton 의 시간 = 최소 200ms (너무 빨리 사라지면 깜빡임)
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 는 background / action
97
- - **Skeleton 의 정확하지 않은 layout** — skeleton 가 실 content 와 다른 shape → 깜빡임 효과
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** | Skeleton 의 `aria-busy="true"` + `aria-label="로딩 중"`. ErrorState 의 `role="alert"` |
107
- | **Telemetry** | error 의 자동 log (Sentry 등) — Step 7 Web Playbook |
108
- | **i18n** | Error / Empty 메시지 모두 `t()` 통과 |
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
- | 페이지 | Pattern 사용 |
115
- |---|---|
116
- | `/merchant/customers` (list) | Page (SkeletonRows) + Section (KPI card stat skeleton) + Inline (row sparkline skeleton) + Action (bulk action button) + Background (auto-refresh polling) |
117
- | `/merchant/products/new` (form) | Section (form 안 reference data fetch skeleton) + Action (Save button) + Form Error (422 → Banner + Field.Error) |
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, 또는 background corner indicator)
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 / localStorage / row 0 시 모두 onboarding)
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 (Step 4.3)
137
- - [page-layout.md](./page-layout.md) — Page-level skeleton 의 shell 위치 (Step 4.5)
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)