@lerx/promise-modal 0.10.4 → 0.11.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/docs/claude/commands/guide.md +760 -0
- package/docs/claude/skills/expert/SKILL.md +171 -0
- package/docs/claude/skills/expert/knowledge/advanced-patterns.md +294 -0
- package/docs/claude/skills/expert/knowledge/api-reference.md +175 -0
- package/docs/claude/skills/expert/knowledge/hooks-reference.md +207 -0
- package/docs/claude/skills/expert/knowledge/type-definitions.md +172 -0
- package/docs/en/SPECIFICATION.md +1185 -0
- package/docs/ko/SPECIFICATION.md +1193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
# @lerx/promise-modal 스펙 문서
|
|
2
|
+
|
|
3
|
+
> Promise 기반 API를 제공하는 범용 React 모달 유틸리티
|
|
4
|
+
|
|
5
|
+
## 개요
|
|
6
|
+
|
|
7
|
+
`@lerx/promise-modal`은 다음 기능을 제공하는 React 기반 범용 모달 유틸리티입니다:
|
|
8
|
+
|
|
9
|
+
- **Promise 기반 상호작용**: Alert, confirm, prompt 모달이 Promise를 반환
|
|
10
|
+
- **범용 사용**: React 컴포넌트 내부와 외부 모두에서 사용 가능
|
|
11
|
+
- **높은 커스터마이징성**: 모든 컴포넌트를 커스터마이즈 가능
|
|
12
|
+
- **자동 생명주기 관리**: 마운트/언마운트 및 애니메이션 자동 처리
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 목차
|
|
17
|
+
|
|
18
|
+
1. [설치](#설치)
|
|
19
|
+
2. [빠른 시작](#빠른-시작)
|
|
20
|
+
3. [아키텍처](#아키텍처)
|
|
21
|
+
4. [핵심 API](#핵심-api)
|
|
22
|
+
- [alert](#alert)
|
|
23
|
+
- [confirm](#confirm)
|
|
24
|
+
- [prompt](#prompt)
|
|
25
|
+
5. [Hooks](#hooks)
|
|
26
|
+
- [useModal](#usemodal)
|
|
27
|
+
- [useActiveModalCount](#useactivemodalcount)
|
|
28
|
+
- [useModalAnimation](#usemodalanimation)
|
|
29
|
+
- [useModalDuration](#usemodalduration)
|
|
30
|
+
- [useDestroyAfter](#usedestroyafter)
|
|
31
|
+
- [useSubscribeModal](#usesubscribemodal)
|
|
32
|
+
- [useInitializeModal](#useinitializemodal)
|
|
33
|
+
- [useModalOptions](#usemodaloptions)
|
|
34
|
+
- [useModalBackdrop](#usemodalbackdrop)
|
|
35
|
+
6. [컴포넌트](#컴포넌트)
|
|
36
|
+
- [ModalProvider](#modalprovider)
|
|
37
|
+
- [커스텀 컴포넌트](#커스텀-컴포넌트)
|
|
38
|
+
7. [타입 정의](#타입-정의)
|
|
39
|
+
8. [사용 패턴](#사용-패턴)
|
|
40
|
+
9. [고급 예제](#고급-예제)
|
|
41
|
+
10. [AbortSignal 지원](#abortsignal-지원)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 설치
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# yarn 사용
|
|
49
|
+
yarn add @lerx/promise-modal
|
|
50
|
+
|
|
51
|
+
# npm 사용
|
|
52
|
+
npm install @lerx/promise-modal
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 피어 의존성
|
|
56
|
+
|
|
57
|
+
- React 18-19
|
|
58
|
+
- React DOM 18-19
|
|
59
|
+
|
|
60
|
+
### 호환성
|
|
61
|
+
|
|
62
|
+
- Node.js 16.11.0 이상
|
|
63
|
+
- 최신 브라우저 (Chrome 94+, Firefox 93+, Safari 15+)
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 빠른 시작
|
|
68
|
+
|
|
69
|
+
### 1. Provider 설정
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { ModalProvider } from '@lerx/promise-modal';
|
|
73
|
+
|
|
74
|
+
function App() {
|
|
75
|
+
return (
|
|
76
|
+
<ModalProvider>
|
|
77
|
+
<YourApp />
|
|
78
|
+
</ModalProvider>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. 모달 함수 사용
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { alert, confirm, prompt } from '@lerx/promise-modal';
|
|
87
|
+
|
|
88
|
+
// 알림
|
|
89
|
+
await alert({
|
|
90
|
+
title: '알림',
|
|
91
|
+
content: '작업이 완료되었습니다.',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// 확인
|
|
95
|
+
const result = await confirm({
|
|
96
|
+
title: '확인',
|
|
97
|
+
content: '계속하시겠습니까?',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 입력
|
|
101
|
+
const name = await prompt<string>({
|
|
102
|
+
title: '이름 입력',
|
|
103
|
+
defaultValue: '',
|
|
104
|
+
Input: ({ value, onChange }) => (
|
|
105
|
+
<input value={value} onChange={(e) => onChange(e.target.value)} />
|
|
106
|
+
),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 아키텍처
|
|
113
|
+
|
|
114
|
+
### 계층 구조
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
118
|
+
│ 애플리케이션 │
|
|
119
|
+
├─────────────────────────────────────────────────────────────┤
|
|
120
|
+
│ Core API 계층 │
|
|
121
|
+
│ ├── alert() │
|
|
122
|
+
│ ├── confirm() │
|
|
123
|
+
│ └── prompt() │
|
|
124
|
+
├─────────────────────────────────────────────────────────────┤
|
|
125
|
+
│ Application 계층 │
|
|
126
|
+
│ └── ModalManager (싱글톤) │
|
|
127
|
+
│ ├── DOM 앵커링 │
|
|
128
|
+
│ ├── 스타일 주입 │
|
|
129
|
+
│ └── 모달 생명주기 │
|
|
130
|
+
├─────────────────────────────────────────────────────────────┤
|
|
131
|
+
│ Bootstrap 계층 │
|
|
132
|
+
│ └── ModalProvider │
|
|
133
|
+
│ ├── 초기화 │
|
|
134
|
+
│ └── 컴포넌트 설정 │
|
|
135
|
+
├─────────────────────────────────────────────────────────────┤
|
|
136
|
+
│ Provider 계층 │
|
|
137
|
+
│ ├── ModalManagerContext │
|
|
138
|
+
│ ├── ConfigurationContext │
|
|
139
|
+
│ └── UserDefinedContext │
|
|
140
|
+
├─────────────────────────────────────────────────────────────┤
|
|
141
|
+
│ Component 계층 │
|
|
142
|
+
│ ├── Anchor │
|
|
143
|
+
│ ├── Background │
|
|
144
|
+
│ ├── Foreground │
|
|
145
|
+
│ └── Fallback Components │
|
|
146
|
+
└─────────────────────────────────────────────────────────────┘
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 디자인 패턴
|
|
150
|
+
|
|
151
|
+
| 패턴 | 용도 |
|
|
152
|
+
|------|------|
|
|
153
|
+
| **Promise 기반 API** | 모달 함수가 Promise 반환 |
|
|
154
|
+
| **Provider 패턴** | 설정을 위한 Context providers |
|
|
155
|
+
| **Factory 패턴** | 모달 타입을 위한 Node factory |
|
|
156
|
+
| **Observer 패턴** | 상태를 위한 구독 시스템 |
|
|
157
|
+
| **Singleton 패턴** | 전역 상태를 위한 ModalManager |
|
|
158
|
+
|
|
159
|
+
### 모달 노드 시스템
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
AbstractNode (기본 클래스)
|
|
163
|
+
├── AlertNode → 간단한 알림
|
|
164
|
+
├── ConfirmNode → 예/아니오 확인
|
|
165
|
+
└── PromptNode → 입력 수집
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
각 노드가 제공하는 기능:
|
|
169
|
+
- 구독 기반 상태 관리
|
|
170
|
+
- Promise 해결 처리
|
|
171
|
+
- 생명주기 관리
|
|
172
|
+
|
|
173
|
+
### 설정 우선순위
|
|
174
|
+
|
|
175
|
+
설정은 계층적으로 적용되며, 하위 레벨이 상위 레벨을 덮어씁니다:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Provider 설정 (최하위) < Hook 설정 < Handler 설정 (최상위)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| 레벨 | 위치 | 설명 |
|
|
182
|
+
|------|------|------|
|
|
183
|
+
| **Provider** | `ModalProvider` props | 앱 전역 기본 설정 |
|
|
184
|
+
| **Hook** | `useModal(config)` | 컴포넌트 레벨 설정 |
|
|
185
|
+
| **Handler** | `alert/confirm/prompt(options)` | 개별 모달 설정 |
|
|
186
|
+
|
|
187
|
+
#### 예제
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// Provider 레벨: 전역 기본값
|
|
191
|
+
<ModalProvider options={{ duration: '500ms', closeOnBackdropClick: true }}>
|
|
192
|
+
<App />
|
|
193
|
+
</ModalProvider>
|
|
194
|
+
|
|
195
|
+
// Hook 레벨: 컴포넌트 기본값 (Provider 설정을 덮어씀)
|
|
196
|
+
const modal = useModal({
|
|
197
|
+
ForegroundComponent: CustomForeground,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Handler 레벨: 개별 모달 (Hook 설정을 덮어씀)
|
|
201
|
+
modal.alert({
|
|
202
|
+
title: '알림',
|
|
203
|
+
duration: 200, // 500ms → 200ms로 오버라이드
|
|
204
|
+
ForegroundComponent: SpecialForeground, // CustomForeground 오버라이드
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 핵심 API
|
|
211
|
+
|
|
212
|
+
### alert
|
|
213
|
+
|
|
214
|
+
간단한 알림 모달을 엽니다.
|
|
215
|
+
|
|
216
|
+
#### 시그니처
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
function alert<B = any>(options: AlertProps<B>): Promise<void>;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### 매개변수
|
|
223
|
+
|
|
224
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
225
|
+
|------|------|--------|------|
|
|
226
|
+
| `title` | `ReactNode` | - | 모달 제목 |
|
|
227
|
+
| `subtitle` | `ReactNode` | - | 제목 아래 부제목 |
|
|
228
|
+
| `content` | `ReactNode \| ComponentType<AlertContentProps>` | - | 모달 내용 |
|
|
229
|
+
| `subtype` | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | 모달 타입 |
|
|
230
|
+
| `dimmed` | `boolean` | `true` | 배경 어둡게 |
|
|
231
|
+
| `closeOnBackdropClick` | `boolean` | `true` | 배경 클릭 시 닫기 |
|
|
232
|
+
| `manualDestroy` | `boolean` | `false` | 수동 제거 모드 |
|
|
233
|
+
| `duration` | `number \| string` | - | 애니메이션 지속 시간 (Handler 레벨 오버라이드) |
|
|
234
|
+
| `background` | `ModalBackground<B>` | - | 배경 설정 |
|
|
235
|
+
| `footer` | `AlertFooterRender \| FooterOptions \| false` | - | 푸터 설정 |
|
|
236
|
+
| `ForegroundComponent` | `ComponentType<ModalFrameProps>` | - | 커스텀 전경 |
|
|
237
|
+
| `BackgroundComponent` | `ComponentType<ModalFrameProps>` | - | 커스텀 배경 |
|
|
238
|
+
| `signal` | `AbortSignal` | - | 모달 취소를 위한 AbortSignal |
|
|
239
|
+
|
|
240
|
+
#### 예제
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
await alert({
|
|
244
|
+
title: '성공',
|
|
245
|
+
content: '변경사항이 저장되었습니다.',
|
|
246
|
+
subtype: 'success',
|
|
247
|
+
footer: { confirm: '확인' },
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### confirm
|
|
254
|
+
|
|
255
|
+
사용자 결정을 위한 확인 모달을 엽니다.
|
|
256
|
+
|
|
257
|
+
#### 시그니처
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
function confirm<B = any>(options: ConfirmProps<B>): Promise<boolean>;
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### 매개변수
|
|
264
|
+
|
|
265
|
+
`alert`의 모든 옵션 포함, 추가:
|
|
266
|
+
|
|
267
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
268
|
+
|------|------|--------|------|
|
|
269
|
+
| `footer` | `ConfirmFooterRender \| FooterOptions \| false` | - | 푸터 설정 |
|
|
270
|
+
|
|
271
|
+
#### confirm용 FooterOptions
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
interface FooterOptions {
|
|
275
|
+
confirm?: string; // 확인 버튼 텍스트
|
|
276
|
+
cancel?: string; // 취소 버튼 텍스트
|
|
277
|
+
hideConfirm?: boolean; // 확인 버튼 숨김
|
|
278
|
+
hideCancel?: boolean; // 취소 버튼 숨김
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### 반환값
|
|
283
|
+
|
|
284
|
+
- `true` - 사용자가 확인 버튼 클릭
|
|
285
|
+
- `false` - 사용자가 취소 또는 배경 클릭
|
|
286
|
+
|
|
287
|
+
#### 예제
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const shouldDelete = await confirm({
|
|
291
|
+
title: '항목 삭제',
|
|
292
|
+
content: '이 작업은 되돌릴 수 없습니다.',
|
|
293
|
+
subtype: 'warning',
|
|
294
|
+
footer: {
|
|
295
|
+
confirm: '삭제',
|
|
296
|
+
cancel: '취소',
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (shouldDelete) {
|
|
301
|
+
await deleteItem();
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### prompt
|
|
308
|
+
|
|
309
|
+
사용자 입력을 수집하는 프롬프트 모달을 엽니다.
|
|
310
|
+
|
|
311
|
+
#### 시그니처
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
function prompt<T, B = any>(options: PromptProps<T, B>): Promise<T>;
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### 매개변수
|
|
318
|
+
|
|
319
|
+
`alert`의 모든 옵션 포함, 추가:
|
|
320
|
+
|
|
321
|
+
| 옵션 | 타입 | 필수 | 설명 |
|
|
322
|
+
|------|------|------|------|
|
|
323
|
+
| `Input` | `(props: PromptInputProps<T>) => ReactNode` | 예 | 입력 컴포넌트 |
|
|
324
|
+
| `defaultValue` | `T` | 아니오 | 기본값 |
|
|
325
|
+
| `disabled` | `(value: T) => boolean` | 아니오 | 확인 버튼 비활성화 |
|
|
326
|
+
| `returnOnCancel` | `boolean` | 아니오 | 취소 시 기본값 반환 |
|
|
327
|
+
|
|
328
|
+
#### PromptInputProps
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
interface PromptInputProps<T> {
|
|
332
|
+
value?: T; // 현재 값
|
|
333
|
+
defaultValue?: T; // 기본값
|
|
334
|
+
onChange: (value: T | undefined) => void; // 값 변경 핸들러
|
|
335
|
+
onConfirm: () => void; // 확인 핸들러
|
|
336
|
+
onCancel: () => void; // 취소 핸들러
|
|
337
|
+
context: any; // 사용자 정의 context
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### 예제
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// 간단한 입력
|
|
345
|
+
const email = await prompt<string>({
|
|
346
|
+
title: '이메일 입력',
|
|
347
|
+
defaultValue: '',
|
|
348
|
+
Input: ({ value, onChange }) => (
|
|
349
|
+
<input
|
|
350
|
+
type="email"
|
|
351
|
+
value={value}
|
|
352
|
+
onChange={(e) => onChange(e.target.value)}
|
|
353
|
+
/>
|
|
354
|
+
),
|
|
355
|
+
disabled: (value) => !value?.includes('@'),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// 복잡한 객체
|
|
359
|
+
interface UserData {
|
|
360
|
+
name: string;
|
|
361
|
+
age: number;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const userData = await prompt<UserData>({
|
|
365
|
+
title: '사용자 정보',
|
|
366
|
+
defaultValue: { name: '', age: 0 },
|
|
367
|
+
Input: ({ value, onChange }) => (
|
|
368
|
+
<form>
|
|
369
|
+
<input
|
|
370
|
+
value={value.name}
|
|
371
|
+
onChange={(e) => onChange({ ...value, name: e.target.value })}
|
|
372
|
+
placeholder="이름"
|
|
373
|
+
/>
|
|
374
|
+
<input
|
|
375
|
+
type="number"
|
|
376
|
+
value={value.age}
|
|
377
|
+
onChange={(e) => onChange({ ...value, age: Number(e.target.value) })}
|
|
378
|
+
placeholder="나이"
|
|
379
|
+
/>
|
|
380
|
+
</form>
|
|
381
|
+
),
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Hooks
|
|
388
|
+
|
|
389
|
+
### useModal
|
|
390
|
+
|
|
391
|
+
컴포넌트 생명주기에 연결된 모달 핸들러를 반환합니다.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
function useModal(config?: Partial<ModalOptions>): {
|
|
395
|
+
alert: typeof alert;
|
|
396
|
+
confirm: typeof confirm;
|
|
397
|
+
prompt: typeof prompt;
|
|
398
|
+
};
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### 핵심 기능
|
|
402
|
+
|
|
403
|
+
컴포넌트가 언마운트될 때 모달이 자동으로 정리됩니다.
|
|
404
|
+
|
|
405
|
+
#### 비교
|
|
406
|
+
|
|
407
|
+
| 기능 | 정적 함수 | useModal 훅 |
|
|
408
|
+
|------|----------|------------|
|
|
409
|
+
| 생명주기 | 독립적 | 컴포넌트에 연결 |
|
|
410
|
+
| 정리 | 수동 | 자동 |
|
|
411
|
+
| 사용 위치 | 어디서나 | 컴포넌트 내부 |
|
|
412
|
+
|
|
413
|
+
#### 예제
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
function DeleteButton({ id }) {
|
|
417
|
+
const modal = useModal();
|
|
418
|
+
|
|
419
|
+
const handleDelete = async () => {
|
|
420
|
+
if (await modal.confirm({ content: '삭제하시겠습니까?' })) {
|
|
421
|
+
await deleteItem(id);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return <button onClick={handleDelete}>삭제</button>;
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### useActiveModalCount
|
|
432
|
+
|
|
433
|
+
활성 모달의 개수를 반환합니다.
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
function useActiveModalCount(
|
|
437
|
+
validate?: (modal?: ModalNode) => boolean,
|
|
438
|
+
refreshKey?: string | number
|
|
439
|
+
): number;
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
#### 매개변수
|
|
443
|
+
|
|
444
|
+
| 매개변수 | 타입 | 설명 |
|
|
445
|
+
|----------|------|------|
|
|
446
|
+
| `validate` | `(modal) => boolean` | 필터 함수 |
|
|
447
|
+
| `refreshKey` | `string \| number` | 강제 새로고침 키 |
|
|
448
|
+
|
|
449
|
+
#### 예제
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
function ModalCounter() {
|
|
453
|
+
const total = useActiveModalCount();
|
|
454
|
+
const alerts = useActiveModalCount((m) => m?.type === 'alert');
|
|
455
|
+
|
|
456
|
+
return <div>전체: {total}, 알림: {alerts}</div>;
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
### useModalAnimation
|
|
463
|
+
|
|
464
|
+
애니메이션 상태 콜백을 제공합니다.
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
function useModalAnimation(
|
|
468
|
+
visible: boolean,
|
|
469
|
+
options: {
|
|
470
|
+
onVisible?: () => void;
|
|
471
|
+
onHidden?: () => void;
|
|
472
|
+
}
|
|
473
|
+
): void;
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### 특징
|
|
477
|
+
|
|
478
|
+
- 최적의 타이밍을 위해 `requestAnimationFrame` 사용
|
|
479
|
+
- 진입/퇴장 애니메이션 분리
|
|
480
|
+
- CSS 트랜지션과 함께 작동
|
|
481
|
+
|
|
482
|
+
#### 예제
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
function AnimatedModal({ visible, children }) {
|
|
486
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
487
|
+
|
|
488
|
+
useModalAnimation(visible, {
|
|
489
|
+
onVisible: () => {
|
|
490
|
+
ref.current?.classList.add('fade-in');
|
|
491
|
+
},
|
|
492
|
+
onHidden: () => {
|
|
493
|
+
ref.current?.classList.remove('fade-in');
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
return <div ref={ref}>{children}</div>;
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
### useModalDuration
|
|
504
|
+
|
|
505
|
+
모달 애니메이션 지속 시간을 반환합니다.
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
function useModalDuration(modalId?: number): {
|
|
509
|
+
duration: string; // 예: '300ms'
|
|
510
|
+
milliseconds: number; // 예: 300
|
|
511
|
+
};
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### useDestroyAfter
|
|
517
|
+
|
|
518
|
+
지정된 시간 후 모달을 자동으로 제거합니다.
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
function useDestroyAfter(
|
|
522
|
+
modalId: number,
|
|
523
|
+
duration: string | number
|
|
524
|
+
): void;
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
#### 동작
|
|
528
|
+
|
|
529
|
+
- 모달이 숨겨지면 타이머 시작
|
|
530
|
+
- 모달이 다시 보이면 타이머 취소
|
|
531
|
+
- 지속 시간 후 DOM에서 모달 제거
|
|
532
|
+
|
|
533
|
+
#### 예제
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
function ToastMessage({ id }) {
|
|
537
|
+
useDestroyAfter(id, 300); // 숨겨진 후 300ms 후 제거
|
|
538
|
+
return <div>토스트</div>;
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### useSubscribeModal
|
|
545
|
+
|
|
546
|
+
모달 상태 변경을 구독합니다.
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
function useSubscribeModal(modal?: ModalNode): number;
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
#### 반환값
|
|
553
|
+
|
|
554
|
+
각 상태 변경 시 증가하는 버전 번호.
|
|
555
|
+
|
|
556
|
+
#### 예제
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
function ModalDebugger({ modal }) {
|
|
560
|
+
const version = useSubscribeModal(modal);
|
|
561
|
+
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
console.log('상태 변경:', modal?.visible);
|
|
564
|
+
}, [version]);
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
### useInitializeModal
|
|
571
|
+
|
|
572
|
+
모달 서비스를 수동으로 초기화합니다.
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
function useInitializeModal(options?: {
|
|
576
|
+
mode?: 'auto' | 'manual';
|
|
577
|
+
}): {
|
|
578
|
+
initialize: (anchor?: HTMLElement) => void;
|
|
579
|
+
portal: ReactPortal | null;
|
|
580
|
+
};
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### 모드
|
|
584
|
+
|
|
585
|
+
| 모드 | 설명 |
|
|
586
|
+
|------|------|
|
|
587
|
+
| `auto` | 자동 초기화 |
|
|
588
|
+
| `manual` | `initialize()` 호출 필요 |
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
### useModalOptions
|
|
593
|
+
|
|
594
|
+
모달 옵션 설정을 반환합니다.
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
function useModalOptions(): ModalOptions;
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
#### 반환값
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
interface ModalOptions {
|
|
604
|
+
duration?: number | string; // 애니메이션 지속 시간
|
|
605
|
+
backdrop?: string; // 배경 오버레이 색상
|
|
606
|
+
manualDestroy?: boolean; // 수동 제거 모드
|
|
607
|
+
closeOnBackdropClick?: boolean; // 배경 클릭 시 닫기
|
|
608
|
+
zIndex?: number; // CSS z-index
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
#### 예제
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
function ModalDebugInfo() {
|
|
616
|
+
const options = useModalOptions();
|
|
617
|
+
|
|
618
|
+
return (
|
|
619
|
+
<div>
|
|
620
|
+
<p>Duration: {options.duration}</p>
|
|
621
|
+
<p>Backdrop: {options.backdrop}</p>
|
|
622
|
+
</div>
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
### useModalBackdrop
|
|
630
|
+
|
|
631
|
+
모달 배경 설정만 반환합니다.
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
function useModalBackdrop(): string | CSSProperties;
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
#### 예제
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
function BackdropInfo() {
|
|
641
|
+
const backdrop = useModalBackdrop();
|
|
642
|
+
|
|
643
|
+
return <p>현재 배경: {backdrop}</p>;
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## 컴포넌트
|
|
650
|
+
|
|
651
|
+
### ModalProvider
|
|
652
|
+
|
|
653
|
+
초기화를 위한 메인 Provider 컴포넌트입니다.
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
interface ModalProviderProps {
|
|
657
|
+
children: ReactNode;
|
|
658
|
+
ForegroundComponent?: ComponentType<ModalFrameProps>;
|
|
659
|
+
BackgroundComponent?: ComponentType<ModalFrameProps>;
|
|
660
|
+
TitleComponent?: ComponentType<WrapperComponentProps>;
|
|
661
|
+
SubtitleComponent?: ComponentType<WrapperComponentProps>;
|
|
662
|
+
ContentComponent?: ComponentType<WrapperComponentProps>;
|
|
663
|
+
FooterComponent?: ComponentType<FooterComponentProps>;
|
|
664
|
+
options?: ModalOptions;
|
|
665
|
+
context?: Record<string, any>;
|
|
666
|
+
usePathname?: () => { pathname: string }; // 라우터 통합
|
|
667
|
+
root?: HTMLElement; // 커스텀 루트 엘리먼트
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
#### ModalOptions
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
interface ModalOptions {
|
|
675
|
+
duration?: number | string; // 애니메이션 지속 시간
|
|
676
|
+
backdrop?: string; // 배경 오버레이 색상
|
|
677
|
+
manualDestroy?: boolean; // 수동 제거 모드
|
|
678
|
+
closeOnBackdropClick?: boolean; // 배경 클릭 시 닫기
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
#### usePathname (라우터 통합)
|
|
683
|
+
|
|
684
|
+
`usePathname` prop을 사용하면 라우터와 통합할 수 있습니다. 경로가 변경되면 모달이 자동으로 닫힙니다.
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
import { useLocation } from 'react-router-dom';
|
|
688
|
+
|
|
689
|
+
<ModalProvider
|
|
690
|
+
usePathname={useLocation} // react-router-dom 통합
|
|
691
|
+
// ...
|
|
692
|
+
>
|
|
693
|
+
<App />
|
|
694
|
+
</ModalProvider>
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
#### 예제
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
import { useLocation } from 'react-router-dom';
|
|
701
|
+
|
|
702
|
+
<ModalProvider
|
|
703
|
+
usePathname={useLocation}
|
|
704
|
+
ForegroundComponent={CustomForeground}
|
|
705
|
+
TitleComponent={CustomTitle}
|
|
706
|
+
SubtitleComponent={CustomSubtitle}
|
|
707
|
+
ContentComponent={CustomContent}
|
|
708
|
+
FooterComponent={CustomFooter}
|
|
709
|
+
options={{
|
|
710
|
+
duration: '200ms',
|
|
711
|
+
backdrop: 'rgba(0, 0, 0, 0.35)',
|
|
712
|
+
manualDestroy: true,
|
|
713
|
+
closeOnBackdropClick: true,
|
|
714
|
+
}}
|
|
715
|
+
context={{
|
|
716
|
+
theme: 'dark',
|
|
717
|
+
locale: 'ko-KR',
|
|
718
|
+
}}
|
|
719
|
+
>
|
|
720
|
+
<App />
|
|
721
|
+
</ModalProvider>
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
### 커스텀 컴포넌트
|
|
727
|
+
|
|
728
|
+
#### ModalFrameProps
|
|
729
|
+
|
|
730
|
+
Foreground/Background 컴포넌트에 전달되는 Props입니다.
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
interface ModalFrameProps<Context = any, B = any> {
|
|
734
|
+
id: number; // 모달 ID
|
|
735
|
+
type: 'alert' | 'confirm' | 'prompt'; // 모달 타입
|
|
736
|
+
alive: boolean; // 활성 상태
|
|
737
|
+
visible: boolean; // 표시 상태
|
|
738
|
+
initiator: string; // 초기화 출처
|
|
739
|
+
manualDestroy: boolean; // 수동 제거 모드
|
|
740
|
+
closeOnBackdropClick: boolean; // 배경 클릭 닫기
|
|
741
|
+
background?: ModalBackground<B>; // 배경 데이터
|
|
742
|
+
onConfirm: () => void; // 확인 핸들러
|
|
743
|
+
onClose: () => void; // 닫기 핸들러
|
|
744
|
+
onChange: (value: any) => void; // 값 변경 핸들러
|
|
745
|
+
onDestroy: () => void; // 제거 핸들러
|
|
746
|
+
onChangeOrder: Function; // 순서 변경 핸들러
|
|
747
|
+
context: Context; // 사용자 정의 context
|
|
748
|
+
children: ReactNode; // 자식 요소
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
#### FooterComponentProps
|
|
753
|
+
|
|
754
|
+
푸터 컴포넌트용 Props입니다.
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
interface FooterComponentProps {
|
|
758
|
+
type: 'alert' | 'confirm' | 'prompt'; // 모달 타입
|
|
759
|
+
onConfirm: (value?: any) => void; // 확인 핸들러
|
|
760
|
+
onClose: () => void; // 닫기 핸들러
|
|
761
|
+
onCancel?: () => void; // 취소 핸들러
|
|
762
|
+
disabled?: boolean; // 비활성화 상태
|
|
763
|
+
footer?: FooterOptions; // 푸터 옵션
|
|
764
|
+
context: any; // 사용자 정의 context
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
#### WrapperComponentProps
|
|
769
|
+
|
|
770
|
+
title/subtitle/content 컴포넌트용 Props입니다.
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
interface WrapperComponentProps {
|
|
774
|
+
children: ReactNode; // 자식 요소
|
|
775
|
+
context: any; // 사용자 정의 context
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
#### 커스텀 컴포넌트 예제
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
const CustomForeground: FC<ModalFrameProps> = ({
|
|
783
|
+
id,
|
|
784
|
+
visible,
|
|
785
|
+
children,
|
|
786
|
+
onClose,
|
|
787
|
+
}) => {
|
|
788
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
789
|
+
const { duration } = useModalDuration();
|
|
790
|
+
|
|
791
|
+
useModalAnimation(visible, {
|
|
792
|
+
onVisible: () => ref.current?.classList.add('visible'),
|
|
793
|
+
onHidden: () => ref.current?.classList.remove('visible'),
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
useDestroyAfter(id, duration);
|
|
797
|
+
|
|
798
|
+
return (
|
|
799
|
+
<div
|
|
800
|
+
ref={ref}
|
|
801
|
+
style={{
|
|
802
|
+
background: 'white',
|
|
803
|
+
borderRadius: 12,
|
|
804
|
+
padding: 24,
|
|
805
|
+
opacity: 0,
|
|
806
|
+
transition: `opacity ${duration}ms`,
|
|
807
|
+
}}
|
|
808
|
+
>
|
|
809
|
+
{children}
|
|
810
|
+
</div>
|
|
811
|
+
);
|
|
812
|
+
};
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## 타입 정의
|
|
818
|
+
|
|
819
|
+
### 모달 타입
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
type ModalType = 'alert' | 'confirm' | 'prompt';
|
|
823
|
+
type ModalSubtype = 'info' | 'success' | 'warning' | 'error';
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### ModalBackground
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
interface ModalBackground<T = any> {
|
|
830
|
+
data?: T;
|
|
831
|
+
[key: string]: any;
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Content Props
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
// Alert 컨텐츠 Props
|
|
839
|
+
interface AlertContentProps {
|
|
840
|
+
onConfirm: () => void;
|
|
841
|
+
context: any;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Confirm 컨텐츠 Props
|
|
845
|
+
interface ConfirmContentProps {
|
|
846
|
+
onConfirm: () => void;
|
|
847
|
+
onCancel: () => void;
|
|
848
|
+
context: any;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Prompt 컨텐츠 Props
|
|
852
|
+
interface PromptContentProps<T> {
|
|
853
|
+
value?: T;
|
|
854
|
+
onChange: (value: T) => void;
|
|
855
|
+
onConfirm: () => void;
|
|
856
|
+
onCancel: () => void;
|
|
857
|
+
context: any;
|
|
858
|
+
}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
## 사용 패턴
|
|
864
|
+
|
|
865
|
+
### 패턴 1: 정적 API (가장 간단)
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
import { alert, confirm, prompt } from '@lerx/promise-modal';
|
|
869
|
+
|
|
870
|
+
// React 외부에서도 사용 가능
|
|
871
|
+
async function handleSubmit() {
|
|
872
|
+
if (await confirm({ content: '변경사항을 저장하시겠습니까?' })) {
|
|
873
|
+
await saveData();
|
|
874
|
+
await alert({ content: '저장되었습니다!' });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### 패턴 2: useModal 훅 (컴포넌트에 권장)
|
|
880
|
+
|
|
881
|
+
```typescript
|
|
882
|
+
function EditForm() {
|
|
883
|
+
const modal = useModal();
|
|
884
|
+
|
|
885
|
+
const handleSave = async () => {
|
|
886
|
+
if (await modal.confirm({ content: '저장하시겠습니까?' })) {
|
|
887
|
+
// 컴포넌트 언마운트 시 모달 자동 정리
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### 패턴 3: 전체 커스터마이징
|
|
894
|
+
|
|
895
|
+
```typescript
|
|
896
|
+
<ModalProvider
|
|
897
|
+
ForegroundComponent={CustomForeground}
|
|
898
|
+
FooterComponent={CustomFooter}
|
|
899
|
+
options={{ duration: 400 }}
|
|
900
|
+
context={{ theme: 'dark' }}
|
|
901
|
+
>
|
|
902
|
+
<App />
|
|
903
|
+
</ModalProvider>
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
### 패턴 4: 개별 모달 오버라이드
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
await alert({
|
|
910
|
+
content: '특별한 모달',
|
|
911
|
+
ForegroundComponent: SpecialForeground,
|
|
912
|
+
background: { variant: 'special' },
|
|
913
|
+
});
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
---
|
|
917
|
+
|
|
918
|
+
## 고급 예제
|
|
919
|
+
|
|
920
|
+
### 토스트 알림
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
const ToastForeground: FC<ModalFrameProps> = ({
|
|
924
|
+
id,
|
|
925
|
+
visible,
|
|
926
|
+
children,
|
|
927
|
+
onClose,
|
|
928
|
+
}) => {
|
|
929
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
930
|
+
const { duration } = useModalDuration();
|
|
931
|
+
|
|
932
|
+
useEffect(() => {
|
|
933
|
+
const timer = setTimeout(onClose, 3000);
|
|
934
|
+
return () => clearTimeout(timer);
|
|
935
|
+
}, [onClose]);
|
|
936
|
+
|
|
937
|
+
useModalAnimation(visible, {
|
|
938
|
+
onVisible: () => ref.current?.classList.add('visible'),
|
|
939
|
+
onHidden: () => ref.current?.classList.remove('visible'),
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
useDestroyAfter(id, duration);
|
|
943
|
+
|
|
944
|
+
return (
|
|
945
|
+
<div
|
|
946
|
+
ref={ref}
|
|
947
|
+
style={{
|
|
948
|
+
position: 'fixed',
|
|
949
|
+
bottom: 20,
|
|
950
|
+
left: '50%',
|
|
951
|
+
transform: 'translateX(-50%) translateY(100px)',
|
|
952
|
+
opacity: 0,
|
|
953
|
+
transition: `all ${duration}ms`,
|
|
954
|
+
}}
|
|
955
|
+
>
|
|
956
|
+
{children}
|
|
957
|
+
</div>
|
|
958
|
+
);
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
export const toast = (message: ReactNode) => {
|
|
962
|
+
return alert({
|
|
963
|
+
content: message,
|
|
964
|
+
ForegroundComponent: ToastForeground,
|
|
965
|
+
footer: false,
|
|
966
|
+
dimmed: false,
|
|
967
|
+
closeOnBackdropClick: false,
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
// 사용
|
|
972
|
+
toast('작업이 완료되었습니다!');
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
### 다단계 마법사
|
|
976
|
+
|
|
977
|
+
```typescript
|
|
978
|
+
async function registrationWizard() {
|
|
979
|
+
// 1단계: 약관 동의
|
|
980
|
+
const accepted = await confirm({
|
|
981
|
+
title: '이용약관',
|
|
982
|
+
content: <TermsContent />,
|
|
983
|
+
footer: { confirm: '동의합니다', cancel: '거부' },
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
if (!accepted) return null;
|
|
987
|
+
|
|
988
|
+
// 2단계: 사용자 정보
|
|
989
|
+
const userInfo = await prompt<{
|
|
990
|
+
name: string;
|
|
991
|
+
email: string;
|
|
992
|
+
}>({
|
|
993
|
+
title: '사용자 정보',
|
|
994
|
+
defaultValue: { name: '', email: '' },
|
|
995
|
+
Input: RegistrationForm,
|
|
996
|
+
disabled: (v) => !v.name || !v.email?.includes('@'),
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
if (!userInfo) return null;
|
|
1000
|
+
|
|
1001
|
+
// 3단계: 완료
|
|
1002
|
+
await alert({
|
|
1003
|
+
title: '환영합니다!',
|
|
1004
|
+
content: `${userInfo.name}님의 계정이 생성되었습니다`,
|
|
1005
|
+
subtype: 'success',
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
return userInfo;
|
|
1009
|
+
}
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
### 커스텀 앵커 (Iframe/Portal)
|
|
1013
|
+
|
|
1014
|
+
```typescript
|
|
1015
|
+
function IframedModals() {
|
|
1016
|
+
const { initialize } = useInitializeModal({ mode: 'manual' });
|
|
1017
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
1018
|
+
|
|
1019
|
+
useEffect(() => {
|
|
1020
|
+
if (containerRef.current) {
|
|
1021
|
+
initialize(containerRef.current);
|
|
1022
|
+
}
|
|
1023
|
+
}, [initialize]);
|
|
1024
|
+
|
|
1025
|
+
return (
|
|
1026
|
+
<div style={{ position: 'relative', height: 600 }}>
|
|
1027
|
+
<div ref={containerRef} style={{ height: '100%' }} />
|
|
1028
|
+
<ModalTriggerButtons />
|
|
1029
|
+
</div>
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
---
|
|
1035
|
+
|
|
1036
|
+
## 생명주기
|
|
1037
|
+
|
|
1038
|
+
```
|
|
1039
|
+
생성 → 표시 → 숨김 → 제거
|
|
1040
|
+
↓ ↓ ↓ ↓
|
|
1041
|
+
open() visible onHide onDestroy
|
|
1042
|
+
↓ true ↓ ↓
|
|
1043
|
+
nodeFactory ↓ visible alive
|
|
1044
|
+
↓ ↓ false false
|
|
1045
|
+
Promise animation ↓ ↓
|
|
1046
|
+
↓ starts duration DOM에서
|
|
1047
|
+
... ↓ passes 제거됨
|
|
1048
|
+
↓ ↓
|
|
1049
|
+
interaction destroy
|
|
1050
|
+
↓
|
|
1051
|
+
resolve
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
## 모범 사례
|
|
1057
|
+
|
|
1058
|
+
1. **앱 루트에 ModalProvider 배치** - 최상위 레벨에 Provider 래핑
|
|
1059
|
+
2. **컴포넌트 내에서는 useModal 사용** - 자동 정리를 위해
|
|
1060
|
+
3. **유틸리티에는 정적 API 사용** - React 외부 코드에서
|
|
1061
|
+
4. **Provider 레벨에서 커스터마이징** - 일관된 스타일링을 위해
|
|
1062
|
+
5. **의미론적으로 subtype 사용** - info, success, warning, error
|
|
1063
|
+
6. **항상 await 또는 처리** - Promise 거부 처리
|
|
1064
|
+
7. **모달 내용은 간단하게** - 복잡한 상태 피하기
|
|
1065
|
+
8. **접근성 테스트** - 키보드 네비게이션 확인
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## AbortSignal 지원
|
|
1070
|
+
|
|
1071
|
+
모달을 프로그래밍 방식으로 취소할 수 있는 `AbortSignal` 지원을 제공합니다.
|
|
1072
|
+
|
|
1073
|
+
### 기본 사용법
|
|
1074
|
+
|
|
1075
|
+
```typescript
|
|
1076
|
+
const controller = new AbortController();
|
|
1077
|
+
|
|
1078
|
+
alert({
|
|
1079
|
+
title: '취소 가능한 모달',
|
|
1080
|
+
content: '3초 후 자동으로 닫힙니다.',
|
|
1081
|
+
signal: controller.signal,
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// 3초 후 모달 취소
|
|
1085
|
+
setTimeout(() => {
|
|
1086
|
+
controller.abort();
|
|
1087
|
+
}, 3000);
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### 수동 취소 제어
|
|
1091
|
+
|
|
1092
|
+
```typescript
|
|
1093
|
+
function ManualAbortControl() {
|
|
1094
|
+
const [controller, setController] = useState<AbortController | null>(null);
|
|
1095
|
+
|
|
1096
|
+
const handleOpen = () => {
|
|
1097
|
+
const newController = new AbortController();
|
|
1098
|
+
setController(newController);
|
|
1099
|
+
|
|
1100
|
+
alert({
|
|
1101
|
+
title: '수동 취소 가능',
|
|
1102
|
+
content: '"취소" 버튼을 클릭하면 모달이 닫힙니다.',
|
|
1103
|
+
signal: newController.signal,
|
|
1104
|
+
closeOnBackdropClick: false,
|
|
1105
|
+
}).then(() => {
|
|
1106
|
+
setController(null);
|
|
1107
|
+
});
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
const handleAbort = () => {
|
|
1111
|
+
if (controller) {
|
|
1112
|
+
controller.abort();
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
return (
|
|
1117
|
+
<div>
|
|
1118
|
+
<button onClick={handleOpen} disabled={!!controller}>
|
|
1119
|
+
모달 열기
|
|
1120
|
+
</button>
|
|
1121
|
+
<button onClick={handleAbort} disabled={!controller}>
|
|
1122
|
+
모달 취소
|
|
1123
|
+
</button>
|
|
1124
|
+
</div>
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
### 여러 모달 일괄 취소
|
|
1130
|
+
|
|
1131
|
+
```typescript
|
|
1132
|
+
function MultipleModalsAbort() {
|
|
1133
|
+
const [controllers, setControllers] = useState<AbortController[]>([]);
|
|
1134
|
+
|
|
1135
|
+
const handleOpenMultiple = () => {
|
|
1136
|
+
const newControllers: AbortController[] = [];
|
|
1137
|
+
|
|
1138
|
+
for (let i = 0; i < 3; i++) {
|
|
1139
|
+
const controller = new AbortController();
|
|
1140
|
+
newControllers.push(controller);
|
|
1141
|
+
|
|
1142
|
+
alert({
|
|
1143
|
+
title: `모달 ${i + 1}`,
|
|
1144
|
+
content: `이것은 ${i + 1}번째 모달입니다.`,
|
|
1145
|
+
signal: controller.signal,
|
|
1146
|
+
closeOnBackdropClick: false,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
setControllers(newControllers);
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
const handleAbortAll = () => {
|
|
1154
|
+
controllers.forEach((controller) => controller.abort());
|
|
1155
|
+
setControllers([]);
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
return (
|
|
1159
|
+
<div>
|
|
1160
|
+
<button onClick={handleOpenMultiple}>3개 모달 열기</button>
|
|
1161
|
+
<button onClick={handleAbortAll}>모든 모달 취소</button>
|
|
1162
|
+
</div>
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
### 이미 취소된 Signal 처리
|
|
1168
|
+
|
|
1169
|
+
```typescript
|
|
1170
|
+
// Signal이 이미 취소된 상태로 전달되면 모달이 즉시 닫힙니다
|
|
1171
|
+
const controller = new AbortController();
|
|
1172
|
+
controller.abort(); // 먼저 취소
|
|
1173
|
+
|
|
1174
|
+
alert({
|
|
1175
|
+
title: '즉시 닫힘',
|
|
1176
|
+
content: 'Signal이 이미 취소되어 즉시 닫힙니다.',
|
|
1177
|
+
signal: controller.signal,
|
|
1178
|
+
}).then(() => {
|
|
1179
|
+
console.log('모달이 즉시 닫혔습니다.');
|
|
1180
|
+
});
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
---
|
|
1184
|
+
|
|
1185
|
+
## 라이선스
|
|
1186
|
+
|
|
1187
|
+
MIT License
|
|
1188
|
+
|
|
1189
|
+
---
|
|
1190
|
+
|
|
1191
|
+
## 버전
|
|
1192
|
+
|
|
1193
|
+
현재: package.json 참조
|