# dialog

화면 가운데 뜨는 확인 대화상자와 아래에서 올라오는 메뉴를 함수 호출 하나로 여는 도구다. 결과(눌린 버튼)를 약속(Promise)으로 돌려준다.

- 분류: 알림과 안내
- 가져오기: `import { DialogHost, DialogCard } from '@/ui/dialog'`
- 코드 위치: `apps/mobile/src/ui/dialog.tsx:73` (Mordy 저장소 60df633)
- 웹 페이지: https://developers.moring.co/tds/components/dialog

## 미리보기

실제 컴포넌트를 그린 화면은 https://developers.moring.co/tds/components/dialog 에서 볼 수 있다. 서비스 색 선택기로 Mordy, Morit 색을 바꿔 볼 수 있다.

## 언제 쓰는가

### 쓴다
- 알려야 하고 꼭 확인받아야 하는 일에 `dialog.alert`를 쓴다.
- "삭제할까요?" 같은 확인에는 `dialog.confirm`을 쓴다. 되돌릴 수 없는 일은 `tone: "danger"`로 한다.
- 선택지 여러 개를 세로로 늘어놓으려면 `dialog.choose`, 행동 목록(이름 바꾸기, 삭제)은 `dialog.menu`를 쓴다.

### 쓰지 않는다
- 사용자를 막지 않아도 되는 알림에는 쓰지 않는다. `toast`나 `Notice`를 쓴다.
- 대화상자 위에 대화상자를 연속으로 띄우지 않는다. 연속으로 부르면 하나씩 차례로 뜬다.
- 긴 입력이나 양식을 넣지 않는다. `Sheet`를 쓴다.

## 종류

| 이름 | 설명 |
| --- | --- |
| dialog.alert | 제목, 본문, 버튼 하나("확인"). 닫으면 끝. |
| dialog.confirm | 취소와 확인 버튼 두 개가 가로로 놓인다. 확인을 누르면 `true`. |
| dialog.choose | 선택지가 세로로 쌓이고 맨 아래에 취소가 붙는다. 고른 번호를 돌려준다. |
| dialog.menu | 아래에서 올라오는 메뉴 시트. 행마다 아이콘, 이름, 보조 글. |

## 크기

| 이름 | 값 | 설명 |
| --- | --- | --- |
| 대화상자 | 폭 최대 420, 좌우 28 여백 | 안쪽 여백 22(위 24). 버튼 높이 48. |
| 메뉴 | 행 최소 높이 56 |  |

## 상태

| 상태 | 모습과 동작 |
| --- | --- |
| 열림 | 대화상자는 180ms 동안 0.96배에서 1배로 커지며 나타난다. 메뉴는 220ms 동안 올라온다. |
| 닫힘 | 130ms. 바깥을 누르거나 뒤로 가기를 누르면 닫히고 취소(`false`/`null`)로 처리된다. |
| 여러 개 호출 | 순서대로 하나씩 뜬다(대기열). |

## 사용 방법

### 앱 맨 위에 한 번 놓기

```tsx
import { DialogHost } from '@/ui/dialog';

// 루트 레이아웃에서 한 번만 렌더링한다.
<DialogHost />
```

### 확인받기

```tsx
import { dialog } from '@/ui/dialog';

const ok = await dialog.confirm({
  title: '자료를 삭제할까요?',
  body: '문제와 풀이 기록이 모두 사라져요. 되돌릴 수 없어요.',
  confirmLabel: '삭제',
  tone: 'danger',
  icon: Trash2,
});
if (ok) await remove();
```

### 알림과 메뉴

```tsx
await dialog.alert('저장했어요', '오답 노트에서 다시 볼 수 있어요.');

const i = await dialog.menu({
  title: '자료 관리',
  options: [
    { label: '이름 바꾸기', icon: Pencil },
    { label: '삭제', icon: Trash2, tone: 'danger' },
  ],
});
if (i === 1) await remove();
```

### 속성(props)

| 함수 | 형태 | 설명 |
| --- | --- | --- |
| `dialog.alert` | `(title: string, body?: string, label = '확인'): Promise<void>` | 알림. 닫힐 때 끝나는 약속을 돌려준다. |
| `dialog.confirm` | `({ title, body, confirmLabel = '확인', cancelLabel = '취소', tone = 'primary', icon }: { title: string; body?: string; confirmLabel?: string; cancelLabel?: string; tone?: 'primary' \| 'danger'; icon?: Icon; }): Promise<boolean>` | 확인. 확인을 눌렀으면 `true`, 취소하거나 바깥을 눌렀으면 `false`. |
| `dialog.choose` | `({ title, body, options, cancelLabel = '취소', icon, tone }: { title: string; body?: string; options: Option[]; cancelLabel?: string \| null; icon?: Icon; tone?: Tone; }): Promise<number \| null>` | 선택지 목록. 고른 번호(0부터), 취소면 `null`. |
| `dialog.menu` | `({ title, body, options }: { title: string; body?: string; options: Option[] }): Promise<number \| null>` | 메뉴 시트. 고른 번호, 닫으면 `null`. |

#### DialogHost

속성이 없다.

#### DialogCard

| 이름 | 타입 | 기본값 | 필수 | 설명 |
| --- | --- | --- | --- | --- |
| `children` | `ReactNode` | — | 필수 | 카드 안의 내용. 대화상자와 같은 면(`surface`, 모서리 24, `shadow(3)`)을 직접 그릴 때 쓴다. |

`DialogHost`는 속성이 없다. 앱 루트에 한 번 두면 `dialog.*` 호출을 받아 그린다. 선택지 항목(`Option`)은 `{ label, tone?: "primary" | "danger" | "neutral", icon?, detail? }`이다.

## 치수와 토큰

| 항목 | 값 |
| --- | --- |
| 대화상자 | `surface`, 모서리 `radius.sheet`(24), 안쪽 22(위 24), `shadow(3)`, 좌우 28 여백, 최대 폭 420 |
| 아이콘 타일 | 44 × 44, 모서리 14, 톤 색 12% 투명도, 아이콘 22 선 2.2 |
| 제목과 본문 | 제목 `headline`, 본문 `body` `textSecondary`(제목 아래 6) |
| 버튼 | 높이 48, 모서리 14, 둘이면 가로로 같은 폭, 그 밖에는 세로로 쌓기(사이 8). neutral은 `segmentTrack` 바탕, primary는 `primary`, danger는 `danger` |
| 메뉴 | `card` 바탕 목록, 행 최소 56, 아이콘 타일 34(색 10%) |
| 어둠막 | `scrim` |

## 접근성

- 대화상자의 역할은 `alert`이고 모달이다(`accessibilityViewIsModal`).
- 버튼에는 이름(글자)이 있고 글자가 길면 80%까지 줄어든다.
- 위험한 확인(`danger`)은 색, 아이콘, 제목·본문 글자로 함께 알린다.
- Android 뒤로 가기는 닫기와 같다.

### 글자 색 대비

| 쓰임 | 글자색 | 바탕 | Mordy 라이트 | Mordy 다크 | Morit 라이트 | Morit 다크 | 기준 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| primary 버튼 글자 (15px 굵게) | `onPrimary` | `primary` | 3.93:1 미달 | 3.31:1 미달 | 4.60:1 통과 | 7.25:1 통과 | 4.5:1 |
| danger 버튼 글자 (15px 굵게) | `onDanger` | `danger` | 4.55:1 통과 | 3.02:1 미달 | 5.15:1 통과 | 7.36:1 통과 | 4.5:1 |
| neutral 버튼 글자 (15px 굵게) | `text` | `segmentTrack` | 15.54:1 통과 | 14.08:1 통과 | 14.95:1 통과 | 9.62:1 통과 | 4.5:1 |
| 본문 (15px) | `textSecondary` | `surface` | 4.23:1 미달 | 6.95:1 통과 | 4.88:1 통과 | 7.11:1 통과 | 4.5:1 |

## 잘한 예와 잘못한 예

- 잘한 예: 되돌릴 수 없는 일은 무엇이 사라지는지 쓰고 `danger`로 한다.
- 잘못한 예: 사용자를 막지 않아도 되는 알림("저장했어요")은 `toast`로 충분하다.

## 함께 보는 컴포넌트

- [Sheet](https://developers.moring.co/tds/components/sheet.md): 화면 아래에서 올라오는 시트다. 위쪽 두 모서리가 둥글고, 손잡이와 제목 줄, 닫기 버튼이 있다.
- [toast](https://developers.moring.co/tds/components/toast.md): 화면 아래에 잠깐 떴다 사라지는 짧은 확인 메시지다. 함수 한 번 호출로 띄운다. 되돌리기 같은 행동 하나를 붙일 수 있다.
- [Notice](https://developers.moring.co/tds/components/notice.md): 화면 안에 끼워 넣는 한 줄 안내 상자다. 안내(파랑), 주의(주황), 오류(빨강) 세 가지 톤이 있다.
- [PrimaryButton](https://developers.moring.co/tds/components/primary-button.md): 서비스 주색 그라데이션으로 채운 알약 모양 버튼이다. 한 화면에서 가장 중요한 행동 하나에 쓴다.
