@makitt.io/mds-mcp-server 0.1.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.
@@ -0,0 +1,137 @@
1
+ # MDS Playbook — Async States (loading / error / empty / skeleton)
2
+
3
+ 데이터 fetch / mutation 진행 중 / 실패 / 빈 결과 표시 약속.
4
+
5
+ > **5 micro-pattern** 명확 분리 — Page / Section / Inline / Action / Background.
6
+ > 헷갈리지 말 것 — 각자 다른 컴포넌트 + 다른 UX 의미.
7
+
8
+ ---
9
+
10
+ ## 1. 5 Micro-pattern 분리
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 |
19
+
20
+ > **핵심 차이** — 사용자 의식 여부.
21
+ > - Page / Section / Inline = 사용자가 보고 있음 → skeleton (예상 layout)
22
+ > - Action = 사용자가 trigger → button loading (응답 대기 명확)
23
+ > - Background = 사용자 모름 → indicator 없음 또는 작게
24
+
25
+ ---
26
+
27
+ ## 2. Error 상태
28
+
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 아래 |
37
+
38
+ > **mds 의 `ErrorState`** = `EmptyState variant="error"` 의 wrapper.
39
+ > `error` prop 으로 unknown 받아 자동 description 추출 (`getErrorDisplayMessage` from `@makitt/shared-utils`).
40
+
41
+ ---
42
+
43
+ ## 3. Empty 상태
44
+
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 아래 |
51
+
52
+ ### Empty 의 의도 명확화
53
+
54
+ - 첫 사용자 = **onboarding 의도** — primary action 강조
55
+ - 필터 결과 = **fix 의도** — 필터 초기화 강조
56
+ - 모두 삭제 = **상태 의도** — message 만
57
+
58
+ ---
59
+
60
+ ## 4. 결정표 (lookup)
61
+
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(...)` |
74
+
75
+ ---
76
+
77
+ ## 5. Mobile 변형
78
+
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 | 그대로 |
86
+
87
+ ---
88
+
89
+ ## 6. 안티 패턴
90
+
91
+ - ❌ **Spinner 사용** (`<Spinner>`) — skeleton 우선. Spinner 는 의도 모호 / placeholder X / 시간 불확실
92
+ - ❌ **Loading 중 화면 깜빡임** — skeleton 의 시간 = 최소 200ms (너무 빨리 사라지면 깜빡임)
93
+ - ❌ **Action button loading 외 indicator** (button + 별도 progress bar) — 중복
94
+ - ❌ **Background 의 modal loading** — 사용자 인터럽트
95
+ - ❌ **Empty 의 "다시 시도" 만** — empty != error. `EmptyState` 의 의도 다름
96
+ - ❌ **Error 시 toast 만** — page-level 은 ErrorState content area. toast 는 background / action
97
+ - ❌ **Skeleton 의 정확하지 않은 layout** — skeleton 가 실 content 와 다른 shape → 깜빡임 효과
98
+
99
+ ---
100
+
101
+ ## 7. Cross-cutting
102
+
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
+
110
+ ---
111
+
112
+ ## 8. 5 Pattern 의 실 사용 예 (apps/web 도메인)
113
+
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) |
119
+
120
+ ---
121
+
122
+ ## 9. TBD — 사용자 합의 대기
123
+
124
+ 1. **Spinner 사용 정책** — 완전 금지 vs 특수 case (예: button 안 inline, 또는 background corner indicator)
125
+ 2. **Skeleton 의 최소 표시 시간** — 200ms 깜빡임 방지. 정확 ms 결정
126
+ 3. **Background refetch indicator** — 완전 silent vs corner toast / progress bar
127
+ 4. **Empty 의 자동 onboarding 감지** — first-time user 판단 방법 (서버 flag / localStorage / row 0 시 모두 onboarding)
128
+ 5. **Polling interval** — 표준 ms (예: 30s) 또는 page 마다 다름
129
+
130
+ ---
131
+
132
+ ## Related Playbooks
133
+
134
+ - [form.md](./form.md) — Form 의 검증 / 저장 / submit 흐름 (Step 4.1)
135
+ - [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)
@@ -0,0 +1,192 @@
1
+ # MDS Playbook — DataGrid + Table
2
+
3
+ list / table 데이터 표시 의 약속.
4
+
5
+ > mds 의 list 컴포넌트 = **DataGrid** (full-feature) + **Table** (custom column).
6
+ > 99% list 페이지 = DataGrid. Table 은 특수 case.
7
+
8
+ ---
9
+
10
+ ## 1. DataGrid vs Table 선택
11
+
12
+ | 케이스 | 답 | 이유 |
13
+ |---|---|---|
14
+ | 셀러 admin 의 list 페이지 (customers / orders / products) | **DataGrid** | filter / search / sort / pagination / selection / bulk action / loading / empty / error 자동 |
15
+ | URL state 동기화 필요 (deep link) | **DataGrid** | controlled props (page / sort / filter / search) 모두 노출 |
16
+ | Server-side filter/sort | **DataGrid** `mode="server"` | totalItems / onPageChange / onSortChange caller 처리 |
17
+ | 단순 정보 표시 (정적 list, 보통 5-10 row) | **Table** | DataGrid 의 toolbar 과잉 |
18
+ | 페이지의 한 섹션 안 작은 list (예: 고객의 최근 주문 3개) | **Table** | 페이지의 다른 컨텍스트와 통합 |
19
+ | 매우 custom layout (column 별 다른 component) | **Table** + caller column render |
20
+ | Tree / nested rows | **Tree** (compounds) — Table 아님 |
21
+
22
+ > **Default = DataGrid**. Table 은 의식적 결정.
23
+
24
+ ---
25
+
26
+ ## 2. DataGrid 표준 패턴
27
+
28
+ ```tsx
29
+ <DataGrid
30
+ data={orders}
31
+ columns={columns}
32
+ rowId={(o) => o.id}
33
+ // ─ Toolbar ─
34
+ title="주문"
35
+ filters={[
36
+ { value: 'all', label: '전체', count: 248, predicate: () => true },
37
+ { value: 'pending', label: '결제 대기', count: 12, predicate: (o) => o.status === 'pending' },
38
+ ]}
39
+ defaultFilter="all"
40
+ searchableKeys={['id', 'customer']}
41
+ toolbarActions={<Button>+ 새 주문</Button>}
42
+ // ─ Selection ─
43
+ selectable
44
+ bulkActions={(selected, clear) => <Button>{selected.length}건 처리</Button>}
45
+ // ─ Pagination ─
46
+ pageSize={20}
47
+ // ─ States ─
48
+ loading={isLoading}
49
+ error={error}
50
+ emptyState={<EmptyState variant="onboarding" action={createOrder} />}
51
+ // ─ Behavior ─
52
+ onRowClick={(o) => navigate(`/orders/${o.id}`)}
53
+ density="default"
54
+ stickyHeader
55
+ storageKey="orders-grid" // localStorage column widths / hidden
56
+ />
57
+ ```
58
+
59
+ ### Column 정의
60
+
61
+ ```ts
62
+ const columns: ColumnDef<Order>[] = [
63
+ { key: 'id', label: 'ID', width: '100px', render: (o) => <Code size="xs">#{o.id}</Code> },
64
+ { key: 'customer', label: '고객', render: (o) => <PersonCell avatar={o.customer[0]} name={o.customer} sub={o.location} /> },
65
+ { key: 'status', label: '상태', render: (o) => <Pill tone={o.status.tone}>{o.status.label}</Pill> },
66
+ { key: 'amount', label: '금액', sortable: true, align: 'right', render: (o) => `₩${o.amount.toLocaleString()}` },
67
+ ];
68
+ ```
69
+
70
+ ### Column 패턴 (셀러 admin 표준)
71
+
72
+ | Column 종류 | 컴포넌트 | 예 |
73
+ |---|---|---|
74
+ | ID / 번호 | `Code size="xs"` | `#HE-2719` |
75
+ | 사람 (고객 / 사용자) | `PersonCell` | avatar + name + sub |
76
+ | 상태 | `Pill tone={...}` | success / warning / danger / info |
77
+ | 금액 | tabular-nums + right-align | `₩123,456` |
78
+ | 날짜 | `relativeDate` 또는 `formatDate` | `3일 전` / `2026-05-01` |
79
+ | 액션 | `IconButton` / `Menu` | 행 끝 |
80
+ | 진행률 | `ProgressRow` | bar + pct |
81
+ | 추세 | `Sparkline` | mini chart |
82
+
83
+ ---
84
+
85
+ ## 3. Mobile 변형
86
+
87
+ | Desktop | Mobile (< md) |
88
+ |---|---|
89
+ | DataGrid table 형 | **Card list** (각 row → card, primary column 1-2개만 표시) |
90
+ | Sticky header | `position: sticky` 그대로 (모바일도 동작) |
91
+ | Toolbar (filter pills + search + actions) | toolbar collapse — filter drawer / search expand |
92
+ | Pagination (page numbers) | infinite scroll 또는 numeric only |
93
+ | selection (checkbox column) | 일반적으로 숨김 — long-press 로 multi-select 도 가능 (TBD) |
94
+
95
+ > Mobile 변환 mechanism — DataGrid 의 `mobileView` prop 또는 자동 (`useIsBreakpointUp('md')`).
96
+ > 현재 mds 의 DataGrid = desktop only. Mobile 자동 변환 = **TBD** (Step 11 builder / web 마이그레이션 시).
97
+
98
+ ---
99
+
100
+ ## 4. Loading / Error / Empty state — async-states.md §1 참조
101
+
102
+ | state | 컴포넌트 |
103
+ |---|---|
104
+ | loading (초기 fetch) | `<SkeletonRows>` (DataGrid 자동) |
105
+ | loading 더 보기 (page 2) | 부분 skeleton + 기존 row 유지 |
106
+ | error | `<ErrorState error={err} onRetry={refetch}>` (DataGrid `errorState` prop) |
107
+ | empty (필터 결과 0) | `<EmptyState variant="search" action={resetFilters}>` |
108
+ | empty (한 번도 없음) | `<EmptyState variant="onboarding" action={createX}>` |
109
+
110
+ ---
111
+
112
+ ## 5. 결정 표 (lookup)
113
+
114
+ | 케이스 | 답 |
115
+ |---|---|
116
+ | "page list 표시" | DataGrid |
117
+ | "5개 정도 단순 table" | Table |
118
+ | "tree / nested" | Tree (compounds) |
119
+ | "row click → 상세" | DataGrid `onRowClick={navigate(...)}` |
120
+ | "row click → 옆 편집 (목록 유지)" | DataGrid `onRowClick={() => openDrawer(...)}` |
121
+ | "URL filter / page 동기화" | DataGrid controlled props (`page`, `onPageChange` etc.) |
122
+ | "Server-side" | DataGrid `mode="server" totalItems={N}` |
123
+ | "bulk action" | DataGrid `selectable bulkActions={(selected) => ...}` |
124
+ | "column resize / hide / reorder" | DataGrid `storageKey` + 자동 |
125
+ | "column custom component" | `column.render` prop |
126
+ | "특정 column 만 sortable" | `column.sortable: true` |
127
+ | "1 필드 inline 편집" | DataGrid column.render = controlled input + form.md §5 (Inline 편집 — Enter 명시 저장) |
128
+
129
+ ---
130
+
131
+ ## 6. 안티 패턴
132
+
133
+ - ❌ **DataGrid 안에 form (모든 row 의 input)** — 큰 form 은 별도 페이지 (page-form). DataGrid 는 list / inline 편집 1 필드만
134
+ - ❌ **Table 으로 DataGrid 흉내** — toolbar / filter / pagination 다 caller 구현 = 중복. DataGrid 사용
135
+ - ❌ **DataGrid 의 column 50+** — 사용자 스캔 불가. 일부 hidden + caller 가 선택
136
+ - ❌ **Loading 시 empty state 표시** — async-states.md §2 — 로딩 중 ErrorState/EmptyState 금지
137
+ - ❌ **row 클릭 + checkbox 동시 작동** — 한쪽만. selection 모드 시 row click 막힘 또는 다른 의미
138
+ - ❌ **pagination size 가 50+** — render 부담 + a11y. 최대 20-30 권장
139
+ - ❌ **column header 가 너무 길거나 wrap** — fixed height, ellipsis
140
+ - ❌ **selection state 가 page 이동 후 사라짐** — DataGrid 의 selectedIds Set 유지 (cross-page)
141
+
142
+ ---
143
+
144
+ ## 7. Cross-cutting
145
+
146
+ | Axis | 적용 |
147
+ |---|---|
148
+ | **Responsive** | DataGrid → card list mobile (TBD 자동 변환) |
149
+ | **A11y** | `<table role="table">` 자동 / column header `<th scope="col">` / sortable button aria-sort / selection checkbox aria-label |
150
+ | **i18n** | column label / filter label / empty message 모두 `t()` |
151
+ | **AI fill** | DataGrid 의 filter 가 AI 자동 제안 가능 (Step 4.8) |
152
+ | **Catalog** | DataGrid props (columns / filters / mode / state props) 자동 catalog (Step 5) |
153
+ | **Telemetry** | row click / sort / filter / search / bulk action 자동 log (Step 7) |
154
+
155
+ ---
156
+
157
+ ## 8. apps/web 의 실 사용 (Step 7 Web Playbook 의 baseline)
158
+
159
+ | Page | DataGrid 사용 |
160
+ |---|---|
161
+ | `/merchant/customers` | DataGrid (filter / search / row click → drawer / bulk action) |
162
+ | `/merchant/orders` | DataGrid (filter / search / row click → detail page / status update bulk) |
163
+ | `/merchant/payment/transactions` | DataGrid (server mode + URL state) |
164
+ | `/merchant/payment/refunds` | 동일 |
165
+ | `/merchant/content/blogs` (list) | DataGrid |
166
+ | `/merchant/content/announcements` | DataGrid |
167
+ | `/merchant/content/banners` | DataGrid |
168
+ | `/admin/dead-letters` | DataGrid (admin) |
169
+ | `/admin/exchange-rates` | DataGrid (admin) |
170
+
171
+ → 20+ 페이지가 DataGrid 사용. 균일성 보장 = Web Playbook (Step 7) 의 표준.
172
+
173
+ ---
174
+
175
+ ## 9. TBD
176
+
177
+ 1. **Mobile 자동 변환** — DataGrid → card list 의 자동 변환 컴포넌트 (CardList?)
178
+ 2. **Long-press multi-select** — 모바일 의 selection 패턴
179
+ 3. **Inline 편집 컴포넌트** — DataGrid cell 의 EditCell wrapper 추가 (Enter / 체크 명시 저장)
180
+ 4. **Sticky column** — first / last column 의 sticky (가로 scroll 시)
181
+ 5. **Virtual scroll** — row 1000+ 시 virtualization (현재 mds 미지원)
182
+ 6. **Group by** — row grouping (예: status 별 group)
183
+
184
+ ---
185
+
186
+ ## Related Playbooks
187
+
188
+ - [form.md](./form.md) — DataGrid 의 inline 편집 (Step 4.1 §5)
189
+ - [overlay.md](./overlay.md) — row click → drawer (Step 4.4)
190
+ - [async-states.md](./async-states.md) — loading / error / empty (Step 4.7)
191
+ - [page-layout.md](./page-layout.md) — DataGrid 가 page 안 위치 (Step 4.5)
192
+ - [array-input.md](./array-input.md) — row 의 sub-entity 편집 (Step 4.6)
@@ -0,0 +1,238 @@
1
+ # MDS Playbook — Feedback (Toast / Notification / Banner / Modal Dialog)
2
+
3
+ 사용자에게 메시지 / 알림 / 결정 강제 의 약속.
4
+
5
+ > 4 채널 명확 분리. **일시 vs 영속 / 인박스 vs 페이지 / 자유 vs 강제** 가 결정 기준.
6
+
7
+ ---
8
+
9
+ ## 1. 4 채널 의도 분리
10
+
11
+ | 채널 | 의도 | 사용자 액션 | mental model | 컴포넌트 |
12
+ |---|---|---|---|---|
13
+ | **Toast** | **일시적 알림** (1회 표시 후 사라짐) | 무시 가능 | 화면 corner 의 작은 알림 (자동 dismiss 4~6s) | `toast.success/error/info/warning(message, {description})` |
14
+ | **Notification** (영속) | **놓치면 안 되는 경고** (사용자가 명시적 닫을 때까지 유지) | 명시 닫기 또는 action | 화면 상단/사이드의 banner (Inbox 같은 느낌) | `<Notification tone="warning" persistent>` — TBD (mds 추가 예정) |
15
+ | **Banner** (inline) | **페이지 내 inline 안내** (해당 페이지의 상태) | 무시 가능 또는 dismiss | 페이지 내 box (정보/경고/에러) | `<Banner tone="info" variant="soft">` |
16
+ | **Modal Dialog** | **결정 강제** (사용자 응답 받아야 진행) | confirm / cancel 필수 | 작업 중단 + 짧은 결정 | `Modal.confirm / alert / prompt` (overlay.md §5) |
17
+
18
+ ### 핵심 차이
19
+
20
+ - **시간** — Toast (일시 4-6s) / Banner (페이지 동안) / Notification (영속) / Dialog (응답까지)
21
+ - **위치** — Toast (corner) / Banner (페이지 inline) / Notification (상단/사이드 inbox) / Dialog (centered overlay)
22
+ - **간섭** — Toast (무시 가능) / Banner (무시 가능) / Notification (명시 닫기) / Dialog (응답 필수)
23
+
24
+ ---
25
+
26
+ ## 2. 결정 표 (lookup)
27
+
28
+ | 케이스 | 답 |
29
+ |---|---|
30
+ | "저장됐어요" | `toast.success("저장됨")` |
31
+ | "서버 500 에러" | `toast.error("저장 실패", { description: getErrorDisplayMessage(error) })` |
32
+ | "백그라운드 동기화 중" | `toast.info("동기화 중")` 또는 silent (Step 4.7 async-states) |
33
+ | "비밀번호 강도 약함" (input 안 hint) | `Field.Hint` (form.md §3) |
34
+ | "form 422 검증 에러 요약" (상단) | `<Banner tone="danger">` |
35
+ | "라이선스 만료 7일 남음" (영속) | `<Notification tone="warning" persistent>` (mds 추가 예정) |
36
+ | "결제 카드 만료" (매번 페이지 진입 시) | `<Notification persistent>` 또는 dashboard 의 `<Banner>` |
37
+ | "이 페이지는 베타입니다" (페이지 inline) | `<Banner tone="info" variant="soft">` |
38
+ | "정말 삭제할까요?" | `Modal.confirm` (Dialog) |
39
+ | "환불 사유 입력해주세요" | `Modal.prompt` (Dialog with input) |
40
+ | "주문 완료" 페이지 진입 시 일회성 | `toast.success` (즉시 사라짐) |
41
+ | "주문 완료" 페이지에 항상 표시 | `<Banner tone="success">` (페이지 안 inline) |
42
+
43
+ ---
44
+
45
+ ## 3. Toast 표준
46
+
47
+ ### Toast tone
48
+
49
+ | Tone | 사용 | 색 |
50
+ |---|---|---|
51
+ | success | 저장 / 완료 / 성공 | green |
52
+ | error | 실패 / 5xx / network | red |
53
+ | warning | 주의 / 약한 에러 / 부분 성공 | amber |
54
+ | info | 정보 / 진행 / 백그라운드 알림 | blue |
55
+
56
+ ### Toast 패턴
57
+
58
+ ```ts
59
+ import { toast } from '@makitt/mds';
60
+
61
+ // 단순
62
+ toast.success('저장됨');
63
+
64
+ // title + description
65
+ toast.error('저장 실패', { description: getErrorDisplayMessage(error) });
66
+
67
+ // action (Undo, Retry)
68
+ toast.success('삭제됨', { action: { label: '되돌리기', onClick: undo } });
69
+
70
+ // duration (default 4000ms)
71
+ toast.info('동기화 중', { duration: 8000 });
72
+
73
+ // promise — async 진행 표시
74
+ toast.promise(saveOrder(), {
75
+ loading: '저장 중...',
76
+ success: '저장됨',
77
+ error: (err) => `실패: ${getErrorDisplayMessage(err)}`,
78
+ });
79
+ ```
80
+
81
+ ### Toast 위치
82
+
83
+ - mds 의 `<Toaster position="top-right">` (root layout 의 mount)
84
+ - position 변경 — `Toaster` 의 prop (top-left / top-right / top-center / bottom-* / etc.)
85
+ - mobile = top-center 권장 (TBD)
86
+
87
+ ---
88
+
89
+ ## 4. Banner 표준
90
+
91
+ ```tsx
92
+ import { Banner } from '@makitt/mds';
93
+
94
+ // inline 페이지 안 안내
95
+ <Banner tone="info" variant="soft">
96
+ 이 페이지는 베타입니다.
97
+ </Banner>
98
+
99
+ // dismissible
100
+ <Banner tone="warning" variant="soft" onClose={() => setVisible(false)}>
101
+ 3일 안에 카드를 갱신해주세요.
102
+ </Banner>
103
+
104
+ // action 포함
105
+ <Banner tone="danger">
106
+ 결제 카드가 만료되었습니다.
107
+ <Button onClick={renewCard}>갱신</Button>
108
+ </Banner>
109
+ ```
110
+
111
+ ### Banner tone + variant
112
+
113
+ | Tone | variant | 의도 |
114
+ |---|---|---|
115
+ | info | soft (default) | 일반 정보 안내 |
116
+ | success | soft | 페이지 단위 성공 (예: "주문 완료") |
117
+ | warning | soft | 주의 / 만료 임박 |
118
+ | danger | soft | 페이지 에러 / 결제 만료 |
119
+ | (각 tone) | solid | 강조 — 큰 announcement (시스템 점검 등) |
120
+ | (각 tone) | outline | 약한 정보 |
121
+
122
+ ---
123
+
124
+ ## 5. Notification (영속) — TBD (mds 추가 예정)
125
+
126
+ > mds 에 영속 Notification 컴포넌트 미존재. 현재는 `<Banner>` 의 long-lived 사용으로 대체.
127
+
128
+ ### 향후 추가 예정
129
+
130
+ ```tsx
131
+ // (예정)
132
+ <Notification tone="warning" persistent onDismiss={dismiss}>
133
+ 라이선스가 7일 후 만료됩니다.
134
+ <Button onClick={renew}>지금 갱신</Button>
135
+ </Notification>
136
+ ```
137
+
138
+ ### Banner 와 차이 — 일단 design
139
+
140
+ - **Banner** = 페이지 inline (그 페이지의 상태)
141
+ - **Notification** = global inbox (모든 페이지에서 표시)
142
+ - Persistent = sessionStorage / 서버 상태 — 명시 닫기 전 유지
143
+
144
+ ### TBD
145
+
146
+ - Notification 의 mount 위치 (top of shell? side inbox?)
147
+ - 다중 notification 의 stacking
148
+ - 닫기 후 다시 나타나는 조건 (서버 trigger / cookie 기반)
149
+
150
+ ---
151
+
152
+ ## 6. Modal Dialog — 결정 강제 (overlay.md §5 참조)
153
+
154
+ | Dialog | 사용 |
155
+ |---|---|
156
+ | `Modal.confirm` | 위험 액션 ("정말 삭제?"), 영향 큰 결정 |
157
+ | `Modal.alert` | 정보 알림 (응답 필수 — 그러나 OK 만) |
158
+ | `Modal.prompt` | 단일 input + 응답 |
159
+
160
+ ### Dialog vs Toast
161
+
162
+ | 의도 | 채널 |
163
+ |---|---|
164
+ | 사용자 결정 받아야 진행 | **Dialog** |
165
+ | 알림만 (사용자 응답 필요 X) | **Toast** |
166
+
167
+ → Toast 로 "OK" 응답 받지 말 것. Dialog 가 정답.
168
+
169
+ ---
170
+
171
+ ## 7. Mobile 변형
172
+
173
+ | 채널 | Mobile 변형 |
174
+ |---|---|
175
+ | Toast | position = top-center (default top-right 대신) |
176
+ | Banner | full-width, padding 줄임 |
177
+ | Notification | top of viewport (sidebar inbox X) |
178
+ | Dialog | 그대로 또는 bottom Sheet (Step 4.4 overlay) |
179
+
180
+ ---
181
+
182
+ ## 8. 안티 패턴
183
+
184
+ - ❌ **Toast 로 "정말 삭제?"** — Toast 는 dismissible. 응답 못 받음. **Dialog 가 정답**
185
+ - ❌ **Notification 으로 "저장됨"** — 영속 의도 아님. **Toast 가 정답**
186
+ - ❌ **Banner 와 Toast 동일 메시지 동시** — 한 곳만
187
+ - ❌ **Toast 5+ 동시** — 사용자 의식 한계. mds 의 stacking 제한 활용
188
+ - ❌ **Dialog 안 form (input multiple)** — `Modal.prompt` 또는 form Modal
189
+ - ❌ **Banner 의 너무 긴 텍스트** (2 줄 이상) — collapse 또는 Modal 로
190
+ - ❌ **영속 Notification 의 dismiss 없음** — 사용자 강제 닫기 가능해야 (a11y)
191
+
192
+ ---
193
+
194
+ ## 9. 일시 vs 영속 의사결정 매트릭스
195
+
196
+ | 시나리오 | 일시 (Toast) | 영속 (Banner / Notification) |
197
+ |---|---|---|
198
+ | 저장 success | ✓ | — |
199
+ | 서버 에러 (한번 발생) | ✓ | — |
200
+ | 백그라운드 동기화 | ✓ | — |
201
+ | 라이선스 만료 (전체 영향) | — | ✓ (Notification) |
202
+ | 결제 카드 만료 (모든 페이지) | — | ✓ (Notification) |
203
+ | 이 페이지 특정 상태 (예: 베타) | — | ✓ (Banner) |
204
+ | 페이지 안 form 의 422 에러 요약 | — | ✓ (Banner) |
205
+ | 시스템 점검 announce | — | ✓ (Banner solid) |
206
+
207
+ > 핵심 — **놓치면 안 되는가** 가 영속 결정. Toast 는 놓쳐도 OK.
208
+
209
+ ---
210
+
211
+ ## 10. Cross-cutting
212
+
213
+ | Axis | 적용 |
214
+ |---|---|
215
+ | **Responsive** | Toast position 모바일 변환 / Banner full-width |
216
+ | **A11y** | `role="status"` (success/info) / `role="alert"` (error/warning) / `aria-live="polite"` (info) / `assertive` (error) |
217
+ | **Telemetry** | error toast / banner click 추적 (Step 7) |
218
+ | **i18n** | 모든 메시지 `t()` 통과 |
219
+ | **AI fill** | AI 가 form 채운 후 `<FormFillApprovalBanner>` (Banner 형태) — Step 4.8 |
220
+
221
+ ---
222
+
223
+ ## 11. TBD
224
+
225
+ 1. **Notification 영속 컴포넌트 mds 추가** — design + mount 위치 + persistence (sessionStorage / 서버)
226
+ 2. **Toast position mobile** — top-center default 자동 변환
227
+ 3. **Toast stacking 최대** — 현재 5 권장? 정확 결정
228
+ 4. **Dialog 외 영구 alert** (예: 시스템 maintenance) — `<Banner solid>` 또는 별도
229
+ 5. **Notification 의 read/unread 상태** — inbox 패턴 (Slack 처럼) 필요 여부
230
+
231
+ ---
232
+
233
+ ## Related Playbooks
234
+
235
+ - [form.md](./form.md) — Form 의 검증 에러 (Banner + Field.Error) (Step 4.1)
236
+ - [overlay.md](./overlay.md) — Modal.confirm / alert / prompt (Step 4.4)
237
+ - [async-states.md](./async-states.md) — Background loading 의 toast vs silent (Step 4.7)
238
+ - [ai-fill.md](./ai-fill.md) — FormFillApprovalBanner (Step 4.8)