docs: 메일 15단계(메일 · 메일함 다루기)를 계약 · 디자인 README · 스킬에 적는다

계약 2절에 메일함 옮기기 · 하위까지 지우기 · 받은편지함 정리 · 지금 보내기(결과를 모르는 실패와 알려진 한계까지),
디자인 README 에 15단계 절과 구현하며 정한 보완, nestjs · vue3 스킬 표에 새 파일과 잠금(여러 대로 늘릴 때 옮길 것).
14단계 "미룬 것"에서 15단계에 한 것(대화째 끌기 · 모두 펼치기 · 줄 머리 답장)을 뺐다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-09 18:38:11 +09:00
parent d02a983d57
commit 7f39aa4eb2
4 changed files with 296 additions and 24 deletions
+174 -2
View File
@@ -1088,8 +1088,8 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
### 처음 판에서 미룬 것
본문 강조의 다음 · 이전 이동, 검색어 줄임말(from: 같은 꼴), 대화째 끌어다 놓기, 첨부 파일 안의 글 찾기, 끝이 바뀐 말 찾기(형태소 분석).
같은 대화 칸 머리의 [모두 펼치기]와 펼친 줄 머리의 [답장]도 다음 후보로 둔다(2026-11-07 사용자 관점 점검에서 나온 제안 — 사용자가 미루기로 정함). 지금은 줄마다 펼치고, [답장]은 펼친 본문 아래에만 있다.
본문 강조의 다음 · 이전 이동, 검색어 줄임말(from: 같은 꼴), 첨부 파일 안의 글 찾기, 끝이 바뀐 말 찾기(형태소 분석).
대화째 끌어다 놓기와 같은 대화 칸 머리의 [모두 펼치기] · 펼친 줄 머리의 [답장](2026-11-07 사용자 관점 점검에서 나와 미뤘던 것)은 15단계에서 했다(아래 "15단계").
## 휴대폰 앱 받기 (2026-11-08 확정 — `SettingsMobileApp`)
@@ -1164,6 +1164,114 @@ PC 에서는 설정 › 계정 › 휴대폰 앱(A)이 QR 코드로 휴대폰에
- 서명 키가 다른 판(시험판)이 깔린 휴대폰은 새 판을 덮어 설치할 수 없다 — 그때는 앱을 지운 뒤 설치하고 로그인을 다시 한다.
- 미룬 것: 앱 안에서 받아 바로 설치, 최소 판이 아닌 새 판 알림, 아이폰(Ad Hoc · TestFlight), Google 개발자 인증(2027 전 세계 — 그 전에 등록할지 정한다).
## 15단계 — 메일 · 메일함 다루기 (2026-11-09 확정 — `Reader15` · `ListDrag15` · `Mailbox15`)
메일과 메일함을 옮기고 정리하는 일을 더한다 — 읽기 화면 [이동 ▾] · 끌어다 놓기 · 메일함 옮기기 · 하위 메일함까지 지우기 · 차단할 때 받은 메일 옮기기 · 예약 메일 [지금 보내기].
서버 약속은 계약 2절("메일함 관리" · "보낸 사람 차단" · "보내기 취소 · 예약 발송")이다. 글의 원본은 화면 상수(`shared/constants/mailbox-manage.ts` · `mail-block.ts` · `mail-send-later.ts` · `mail-filter.ts`)이고 아래는 옮겨 적은 것이다.
새 크기 토큰 · 밀도 예외는 없다 — 옮기기 창의 줄 40 은 메일함 패널의 줄(`--size-row-min`), [지금 보내기] 36 은 안내 줄 단추(`--size-notice-action`)를 다시 쓴다. 문구는 위 「화면 문구 — 사용자에게 말하듯」을 따른다.
### 읽기 화면 · 목록 · 상단 바 (`Reader15` 1~3 · 6 · `ListDrag15`)
- **읽기 화면 [이동 ▾]**: 도구 줄 답장 · 전체 답장 · 전달 | **이동 ▾** · 스팸 신고 · 삭제 · ⋯ — 메일이 어디로 가는지를 한 묶음에 둔다(목록 보기는 지금처럼 구분선 없이 [전달] 뒤).
메뉴는 목록 도구 줄의 [이동 ▾]와 같은 부품(항목 40) · 같은 옮길 곳이다. 그 메일이 든 메일함(검색 결과에서 연 메일도 그 메일이 든 메일함) · 임시보관함 · 휴지통은 없다 — 휴지통으로는 [삭제]로 옮긴다.
보내기를 기다리는 메일(보내는 중 · 예약)에는 [스팸 신고]처럼 없다. 키보드는 목록의 [이동 ▾]와 같다(Enter · Space · 아래 화살표로 열면 첫 항목에 초점, Esc 는 메뉴만 닫고 [이동 ▾]으로).
- **옮긴 뒤**: [삭제]와 같은 길 — 분할 보기는 목록의 다음 메일을 열고 목록 보기는 목록으로 돌아간다. 알림 "메일 1통을 거래처 › 2025로 옮겼습니다." + [되돌리기](10초 — 되돌리면 "메일 1통을 받은편지함으로 되돌렸습니다.").
- **옮길 메일함은 메일함 패널처럼**: 같은 차례로 깊이만큼 들여 쓰고(10 · 28 · 44 · 56, 3단계까지) 앞에 패널과 같은 아이콘, 기본 메일함과 내 메일함 사이에 가는 선.
들여 쓴 메일함은 화면 낭독이 위 메일함부터 읽는다("거래처 안의 2025"). 목록의 [이동 ▾]도 같은 모양이다.
- **그 메일이 든 메일함에 하위 메일함이 있으면** 빼지 않고 고를 수 없는 흐린 줄(aria-disabled)로 남기고 이름 아래에 "이 메일이 있는 메일함" — 하위 메일함이 누구 밑에 있는지 보이게.
하위 메일함이 없으면 지금처럼 뺀다. 목록의 [이동 ▾]는 같은 규칙에 글만 "고른 메일이 있는 메일함"이다.
- **알림의 메일함 이름**: 같은 이름의 메일함이 다른 곳에도 있을 때만 위 메일함 이름을 붙인다("거래처 › 2025로" — 정하는 규칙은 아래 "15단계 구현하며 정한 보완"의 메일함 나무 도우미).
목록의 [이동 ▾] · 끌어다 놓기 · 되돌리기 알림도 같다.
- **메뉴 자리**: 화면(보이는 영역) 밖으로 넘치면 반대 끝에 맞춘다 — 단추 왼쪽 끝에 맞춘 메뉴가 오른쪽 밖으로 넘치면 메뉴 오른쪽 끝을 단추 오른쪽 끝에. 위아래만 바꾸던 메뉴 자리 정하기에 좌우를 더해 같은 부품의 모든 메뉴에 걸린다.
- **좁은 읽기 칸**: 도구 줄의 이름은 화면 폭이 아니라 읽기 칸 폭(컨테이너 쿼리)으로 숨긴다 — 이름을 한 줄에 다 보일 폭 570(도구 줄 좌우 여백 포함 · 휴지통은 [영구 삭제] 때문에 595)보다 좁으면 아이콘만 40×44([이동 ▾]는 ▾ 까지).
이름은 마우스를 올리면 뜨는 글(title)과 화면 낭독(aria-label)으로 읽힌다. 분할 보기 1144~1233px 에서 두 줄로 꺾이던 일이 없고, 360 에서도 두 줄로 끝난다.
- **끌어다 놓기**(목록 → 메일함 패널): 고른 줄을 끌면 고른 것 모두, 고르지 않은 줄을 끌면 그 줄만(고른 것은 그대로). 대화로 묶은 목록의 줄은 그 대화에서 지금 메일함에 든 메일(보내기를 기다리는 메일은 빼고 — [이동 ▾]와 같다),
보내기를 기다리는 메일의 줄 · 그런 메일뿐인 대화 줄은 끌 수 없다.
- 끄는 동안 마우스 오른쪽 아래에 "메일 N통" · "대화 N개" 표(알림과 같은 어두운 바탕 · 12.5 굵은 흰 글자 · 봉투 · 말풍선 아이콘). 끈 줄은 흐리게 하지 않는다.
- 놓을 수 있는 메일함 위에서는 그 줄에 2px 청록 점선 · 옅은 청록 바탕(쓰기 화면 파일 놓기와 같은 모양 — 초점 선과 헷갈리지 않게). 놓을 수 없는 곳(지금 메일함 · 임시보관함 · 「예약됨」)은 줄이 그대로이고 마우스 커서만 금지 모양이다.
- 놓을 곳은 [이동 ▾]의 옮길 곳 + 휴지통. 휴지통에 놓으면 [삭제], 스팸메일함에 놓으면 [스팸 신고]와 같고 그 밖은 [이동 ▾]와 같다(알림 · [되돌리기]도 같다).
- 마우스로만 한다 — 키보드 · 터치는 [이동 ▾]. 메일함 패널은 쓰기 화면에도 보이므로 파일 끌기는 받지 않는다(첨부 놓기와 겹치지 않게). 줄의 링크가 끌어 가던 메일 주소(URL)는 싣지 않는다 — 다른 창 · 바깥 프로그램에 놓아도 아무 일 없다.
- **스팸메일함으로 옮기는 세 길 같게**: [스팸 신고] · [이동 ▾]에서 스팸메일함 · 스팸메일함에 끌어다 놓기는 모두 스팸 표시를 붙여 옮긴다. 스팸메일함에서 휴지통이 아닌 메일함으로 옮기면(메뉴 · 끌기)
[스팸 아님]과 같이 스팸 아님 표시를 붙인다 — "메일 1통을 스팸이 아닌 메일로 표시해 거래처로 옮겼습니다." 휴지통으로는 표시를 바꾸지 않는다. [되돌리기]는 표시도 되돌린다.
- **메일 화면에 파일을 놓으면**: 지금은 브라우저가 그 파일을 열어 웹메일을 떠났다 — 메일 화면에서는 무시한다(놓는 동안 금지 커서). 쓰기 화면은 지금처럼 어디에 놓아도 첨부로 받는다.
- **상단 바 좁은 폭**(360 — 바 안쪽 256): [로그아웃]은 줄지도 꺾이지도 않는다. 이름 · 주소는 넘치면 끝을 "…"로 줄이고 마우스를 올리면 다 보인다(title "김아라 (ara.kim@arasystem.it.kr)").
화면 폭 480 이하에서는 주소를 감추고 이름만 둔다. 줄 수와 차례(메일 · 검색 칸 · 계정)는 지금과 같다 — 계정 줄을 첫 줄로 올리지 않는다(Tab 차례).
- **같은 대화 [모두 펼치기]**: 칸 머리 오른쪽의 글자 단추(36 — 펼친 줄의 [답장]과 같은 크기, 안내 줄 단추 `--size-notice-action` 재사용). 불러온 줄(지금 보는 메일 빼고)을 모두 펼치고 [모두 접기]로 바뀐다 —
줄을 하나씩 다 펼쳐도 [모두 접기]이고, 하나라도 접혀 있으면 [모두 펼치기]. [더 보기]로 더 받은 줄은 접힌 채로 온다.
본문은 한꺼번에 받지 않는다 — 화면에 들어오는 줄부터 하나씩(요청 수 한도와 화면 무게), 아직 화면 밖인 줄은 받는 중과 같은 자리만 둔다.
읽음은 지금 규칙 그대로 — 본문이 화면에 보인 메일만 읽음이 되고 펼쳤어도 화면 밖이면 그대로다.
- **펼친 줄 머리 [답장]**: 펼친 줄 머리 오른쪽(36) — 긴 메일을 끝까지 내리지 않고 답장한다. 줄 머리 전체가 펼침 단추라 머리를 감싸는 틀을 두고 펼침 단추와 [답장]을 나란히 놓는다(마우스를 올린 바탕은 틀 전체).
아래쪽 [답장] · [이 메일만 열기]는 그대로이고, 임시 저장본 · 예약한 메일의 줄에는 없다(지금과 같다).
### 메일함 ⋯ 「옮기기」 · 옮기기 창 (`Mailbox15` 1~4)
- **⋯ 메뉴**: 내 메일함(역할 없는 메일함 — 받은편지함 아래에 만든 메일함 포함)에만 「옮기기」. 차례는 모두 읽음으로 표시 · 하위 메일함 만들기 · 이름 바꾸기 · **옮기기** · 지우기.
창을 여는 다른 항목처럼 이름 뒤에 …를 붙이지 않는다. 메일함을 지우는 동안에는 지금처럼 모든 항목을 누를 수 없다.
- **옮기기 창**(모달 400): 제목 "‘2025’ 메일함을 어디로 옮길까요?" · 글 "하위 메일함과 안의 메일도 함께 옮깁니다."(하위가 없으면 "안의 메일도 함께 옮깁니다.") → 옮길 곳 목록 → [옮기기] [취소].
- 목록(테두리 칸 · 줄 40 · 메일함 패널과 같은 차례 · 들여쓰기 10 · 28 · 44 · 56 · 패널 아이콘): 「맨 위」(위 화살표 — "다른 메일함 안에 두지 않습니다") · 받은편지함(과 그 아래) · "내 메일함" 묶음.
보낸편지함 · 임시보관함 · 스팸메일함 · 휴지통은 없다(하위 메일함을 만들 수 있는 곳과 같다). 메일함이 많으면 목록 칸 안에서 스크롤한다.
- 처음에는 아무 곳도 고르지 않은 채 열리고 [옮기기]는 꺼져 있다. 고른 줄은 패널의 지금 메일함처럼 청록 바탕 · 굵은 글자 · 오른쪽 체크.
- 고를 수 없는 줄은 이름 · 아이콘만 흐리게(까닭 글은 그대로): 옮기는 메일함과 그 하위(까닭 없음) · 지금 있는 곳("지금 있는 곳" — 맨 위의 메일함이면 맨 위 줄) ·
바로 아래에 같은 이름의 메일함이 있는 곳("같은 이름의 메일함이 있습니다" — 대소문자를 가리지 않는다, 서버에 묻기 전에 화면이 하는 검사).
- 줄은 radiogroup 의 radio 다. 화살표로 줄을 옮기면 고를 수 있는 줄은 바로 고르고, Enter 는 [옮기기], Esc 는 [취소]와 같다. 들여 쓴 줄은 위 메일함부터 읽는다("거래처 안의 2025").
- **그사이 같은 이름이 생겼을 때**(드묾 — 409 `MAIL_009`): 목록 아래 빨간 사전 문구 "같은 이름의 메일함이 이미 있습니다." — 알림 없이 창을 둔다. 다른 곳을 고르면 사라진다.
그사이 메일함이 사라졌으면(`RES_001`) 알림을 띄우고 창을 닫은 뒤 메일함 목록을 다시 받는다(이름 바꾸기 창과 같다). 옮기는 동안에는 [옮기기]를 다시 누를 수 없다.
- **옮긴 뒤**: 메일함 패널에 바로 나타나고(하위 메일함과 안의 메일이 함께 간다 · 그 메일함으로 옮기는 필터도 그대로 돈다), 보던 메일함이면 그 메일함을 계속 본다.
알림 "‘2025’ 메일함을 ‘프로젝트’ 안으로 옮겼습니다."(받은편지함은 따옴표 없이 "…받은편지함 안으로 …") · 맨 위로는 "‘2025’ 메일함을 다른 메일함 밖으로 옮겼습니다." + [되돌리기](원래 있던 곳으로 —
"‘2025’ 메일함을 ‘거래처’ 안으로 되돌렸습니다.").
### 하위 메일함까지 지우기 (`Mailbox15` 5)
- 하위 메일함이 있는 메일함의 「지우기」는 지금까지의 "지울 수 없습니다" 창 대신 확인 창(처음 초점 [취소]): "‘거래처’ 메일함과 하위 메일함 2개를 지울까요?" ·
"하위 메일함에 든 메일까지 모두 40통을 휴지통으로 옮깁니다. 휴지통에 들어간 메일은 30일 뒤 자동으로 지워집니다. 지운 메일함은 되돌릴 수 없습니다." [지우기] [취소].
메일이 하나도 없으면 둘째 글은 "하위 메일함까지 모두 비어 있습니다. 지운 메일함은 되돌릴 수 없습니다." 하위 메일함 수 · 메일 수는 모든 깊이를 센다.
- 확인하면 같은 지우기를 하위까지(`?withChildren=true`) 보낸다 — 진행 · 결과는 지금 메일함 지우기 그대로("‘거래처’ 메일함을 지우는 중 · 12 / 40통" → "‘거래처’ 메일함을 지웠습니다. 안의 메일 40통은 휴지통에 있습니다.").
휴지통을 비우는 동안에는 [지우기]를 누를 수 없다(지금과 같다).
### 보낸 사람 차단 창 — 받은편지함의 메일도 옮기기 (`Mailbox15` 6~7)
- 「이 메일도 스팸메일함으로 옮기기」 아래에 「받은편지함에 있는 이 보낸 사람의 메일도 스팸메일함으로 옮기기」 + "옮긴 메일은 한꺼번에 되돌릴 수 없습니다." — 창을 열 때마다 **꺼진 채** 시작한다.
「이 도메인 전체」를 고르면 "…이 도메인의 메일도 …"(그 도메인만 — 하위 도메인은 보지 않는다). 두 칸은 붙여 두고, 칸 글이 두 줄이면 고르기 상자를 첫 줄에 맞춘다. 칸 아래 글은 글 선(28)에 맞춘다.
- 새 칸을 켰는데 이 메일이 받은편지함에 있으면 첫 칸은 켜진 채 잠기고(aria-disabled) 아래에 "받은편지함의 메일을 옮길 때 함께 옮깁니다." — 새 칸을 끄면 첫 칸은 원래 값으로 돌아온다.
이 메일이 다른 메일함에 있으면 잠그지 않는다. 스팸메일함의 메일이면 첫 칸은 지금처럼 없고 새 칸은 남는다.
- 이 탭에서 받은편지함을 정리하는 동안(필터 적용 · 다른 차단 정리)은 새 칸을 끄고(aria-disabled) 칸 아래 글을 "받은편지함을 정리하는 중이라 지금은 고를 수 없습니다."로 바꾼다.
이 탭에서 스팸메일함을 비우는 동안도 끄고 "스팸메일함을 비우는 중이라 지금은 고를 수 없습니다."로 바꾼다(보드에 없다 — 아래 "15단계 구현하며 정한 보완").
- **차단한 뒤**: 새 칸을 켜지 않았으면 지금 알림 그대로(+ [되돌리기]). 켰으면 차단 알림을 따로 띄우지 않고 알림 하나가 진행을 보이다 같은 자리에서 결과로 바뀐다 — [되돌리기]는 없다(차단은 설정에서 푼다):
"‘news@example.com’을(를) 차단했습니다. 받은편지함의 메일을 스팸메일함으로 옮기는 중 · 1,200 / 4,800통 확인"(확인한 메일 / 받은편지함의 메일 수 — 첫 응답 전에는 받은편지함의 메일 수) →
"‘news@example.com’을(를) 차단하고 받은편지함의 메일 37통을 스팸메일함으로 옮겼습니다." + [스팸메일함 보기] · 옮길 메일이 없으면 "…차단했습니다. 받은편지함에는 옮길 메일이 없습니다."(단추 없음).
- 멈춤: "…받은편지함은 4,800통 가운데 1,200통까지 확인해 12통을 옮긴 뒤 멈췄습니다. 다시 시도해 주세요." + [다시 시도](멈춘 자리부터 · 수는 이어 센다)
- 처음부터 실패: "…차단했지만 받은편지함의 메일은 옮기지 못했습니다. 다시 시도해 주세요." + [다시 시도](처음부터)
- 다른 창에서 하는 중: "…차단했습니다. 다른 창에서 받은편지함이나 스팸메일함을 정리하는 중입니다. 끝난 뒤 다시 시도해 주세요." + [다시 시도] — 차단한 뒤에는 ⋯ 메뉴가 「차단 풀기」라 다시 고를 길이 이것뿐이다.
서버의 차단 정리는 받은편지함 정리(필터 적용)뿐 아니라 스팸메일함 비우기와도 잠금을 나누어 두 곳을 함께 말한다(2026-11-09 재리뷰).
필터 적용의 429 글은 "다른 창에서 받은편지함을 정리하는 중입니다. 끝난 뒤 다시 시도해 주세요." 그대로다(7단계의 "다른 창에서 필터로 …"를 이 글로 바꿨다).
- 그동안 다른 화면으로 가도 이어 하고, 탭을 닫거나 로그아웃하면 멈추고 알림을 닫는다. 필터 적용과 이 일은 한 직원에 한 번에 하나다. 잘못 옮긴 메일은 스팸메일함에서 [스팸 아님]으로 돌려놓는다.
### 예약한 메일 [지금 보내기] (`Reader15` 4~5)
- 예약한 메일의 청록 안내 줄 단추가 [지금 보내기] [고치기] [예약 취소] 차례다(같은 흰 단추 36 — 누르는 동안 세 단추를 함께 막는다). 예약됨 목록 줄 · 휴대폰 앱에는 두지 않는다.
- 누르면 [보내기]를 누른 것과 같다 — 보내기 취소 설정의 초만큼 기다렸다 보내고, 그동안 "메일을 보냈습니다." + [보내기 취소] + 남은 시간 막대(9단계 마감 알림 그대로). 확인 창은 두지 않는다.
끔으로 두었거나 예약 시각이 막 지나 이미 보낸 메일이면 오류가 아니라 "메일을 보냈습니다." + [보낸편지함 보기]. 받는 사람 일부가 받지 못하면 쓰기 화면과 같은 문장이다.
- 알림의 [보내기 취소]는 지금의 [보내기 취소]와 같다(그 메일을 쓰기 화면으로 — 다시 예약하려면 [보내기 ▾] → [예약 발송…]).
- 보낸 메일은 예약됨에서 빠지므로 분할 보기는 다음 예약 메일을 열고 목록 보기는 예약됨 목록으로 간다([예약 취소]와 같다). 예약됨 수를 다시 맞춘다. 보낸편지함에서 연 메일이면 그 자리에 두고 안내 줄만 사라진다(날짜 칸은 보내는 동안 "보내는 중").
- 예약 시각보다 먼저 보내므로 대용량 첨부 링크의 기간은 실제로 보낸 날부터 센다(서버).
- **보내지 못했거나 결과를 모를 때**(서버가 503 `MAIL_001` 로 답함 — 결과를 모르는 실패와 엔진이 거절해 임시보관함으로 되돌린 실패가 같은 꼴이다.
앞단의 502 · 504 · 시간 초과 · 연결 끊김(`SYS_001`)도 결과를 모르는 실패라 같은 갈래다): 사전 문구 알림은 끄고, 화면이 그 메일을 다시 받아 붙잡은 발송과 메일함으로 가른다(2026-11-09 15단계 리뷰 · 재리뷰).
- 임시보관함: "지금 보내지 못해 임시보관함으로 옮겼습니다." + [임시보관함 보기] — 예약됨을 떠났으니 안내 줄을 내리고 이동은 [예약 취소]와 같다.
- 예약이 그대로(예약을 거두기 전에 멈췄다 — 예약한 시각에 나간다): "지금 보내지 못했습니다. 다시 시도해 주세요." + [다시 시도](같은 요청 — 같은 메일은 요청 하나로 묶는다). 안내 줄은 그대로 둔다.
- 예약만 풀렸다(보낸편지함에 있고 기다리는 발송이 없다 — 예약한 시각에 따로 나가지는 않는다): "메일을 보냈는지 확인하지 못했습니다. [다시 시도]를 누르면 아직 보내지 않았을 때만 보냅니다." + [다시 시도].
이미 나갔을 수 있어(보내기 취소 0초로 바로 나감 · 응답만 잃음 · 그사이 예약 시각이 됨) 못 보냈다고 단정하지 않는다 — 그 글을 믿고 손으로 다시 보내면 두 통이 간다(2026-11-09 마지막 확인).
[다시 시도]는 같은 요청이라 안전하다(이미 보낸 메일이면 서버가 200 으로 답한다). 예약이 아닌 것은 확인했으니 "…에 보낼 메일입니다." 안내 줄은 내린다 — 예약됨 · 보낸편지함 · 검색 어디서 열었든.
예약됨에서는 빠지므로 이동은 [예약 취소]와 같고, 보낸편지함 · 검색에서 연 메일은 그 자리에 둔다.
- 다시 낸 발송이 보내기 취소를 기다린다: 보낸 것과 같다("메일을 보냈습니다." + [보내기 취소]).
- 그 메일을 다시 받지 못했다: 위와 같은 "메일을 보냈는지 확인하지 못했습니다. …" + [다시 시도] — 보냈다 · 못 보냈다 · 되돌렸다고 말하지 않는다(닫힌 쪽).
안내 줄은 마지막으로 안 상태 그대로 두고, 다음 다시 맞추기에서 바로잡힌다.
- 예약됨 수와 목록은 어느 쪽이든 다시 맞춘다. 받는 사람이 모두 받지 않으면(400 `MAIL_010` · 503 `MAIL_011` — 그 메일은 임시보관함에) 사전 문구 그대로다.
- **알려진 한계**: 드물게 [다시 시도]를 누르지 않고 알림을 닫으면 그 메일은 보내지 않은 채 보낸편지함에 남는다 — 9단계 예약 취소와 같은 한계다.
### 처음 판에서 미룬 것
메일함을 끌어 옮기기 · 순서 바꾸기(옮기기 창으로), 차단할 때 옮긴 메일의 한꺼번에 되돌리기, 예약됨 목록 줄의 단추, 옮기기 단축키, 휴대폰 앱의 [지금 보내기].
## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다)
- 찾지 못한 첨부(보관 시간이 지난 업로드·지워진 원본)는 실패로 표시하고, 전달 원본에는 "빼기"만 둔다(다시 올릴 수 없다).
@@ -1574,3 +1682,67 @@ PC 에서는 설정 › 계정 › 휴대폰 앱(A)이 QR 코드로 휴대폰에
- **앱 그림**의 모서리는 한 변의 1/4 이다(48 은 12, 52 는 13 — 보드 값).
- **폭 기준**: 설정의 QR 줄을 빼는 좁은 폭은 480 이하(공개 내려받기 화면의 좁은 폭과 같은 값), `/app` 의 넓은 화면은 그 반대(481 이상)다.
- 글자 크기 17(`/app` 의 앱 이름 · 받을 앱이 없을 때의 제목)은 토큰 `--font-size-title-md` 를 더했다(크기 · 밀도 예외 토큰은 아니다).
### 15단계 구현하며 정한 보완 (2026-11-09 — 캔버스에는 아직 없다)
메일함 · 차단 · 예약 쪽을 먼저, 읽기 화면 · 목록 · 상단 바 쪽을 뒤에 적는다.
- **메일함 나무 도우미**(`shared/utils/mailbox.ts`): 패널 차례로 편 나무 `mailboxTree`(깊이 · 위 메일함 · 하위가 있는지 · 묶음), 화면 낭독 이름 `mailboxSpokenName`("거래처 안의 2025" —
더 깊으면 "거래처 안의 2025 안의 정산"), 알림 이름 `mailboxDisplayName`(같은 이름이 다른 곳에도 있을 때만 "거래처 › 2025" — 위 메일함까지 같으면 가를 수 있을 때까지 올라간다, 맨 위의 메일함은 이름 그대로).
[이동 ▾] · 끌어다 놓기 · 옮기기 창이 함께 쓴다. 알림의 이름은 글자 모양으로 견주고(NFC — 대소문자는 가린다), 같은 자리 겹침은 대소문자를 가리지 않는다(엔진과 같다).
- **옮기기 창**
- 옮길 곳은 ⋯ 「하위 메일함 만들기」가 있는 메일함(받은편지함 · 내 메일함)과 맨 위다. 하위를 둘 수 없는 메일함(보낸편지함 등 · 보관함) 아래의 메일함은 그 위까지 모두 둘 수 있는 곳일 때만 넣는다 —
다른 메일 프로그램이 보낸편지함 아래에 만든 메일함은 옮길 곳에 없다.
- 화살표는 꺼진 줄에도 초점을 옮긴다(까닭을 읽게 — 고른 것은 그대로). 처음과 끝에서 돌고, Home · End 도 된다. 처음 초점은 고를 수 있는 첫 줄이다(로빙 tabindex).
- 맨 위에서 옮긴 것을 되돌린 글은 "‘2025’ 메일함을 다른 메일함 밖으로 되돌렸습니다."(보드에 없다 — 맨 위로 옮긴 글과 같은 결). [되돌리기]가 실패하면(그사이 원래 자리에 같은 이름 · 원래 상위가 지워짐) 사전 문구 알림만.
- 옮길 수 없는 자리였으면(400 `VAL_001` — 그사이 나무가 바뀜) 알림을 본 뒤 메일함 목록을 다시 받고 창은 둔다(다시 고를 수 있게). `REQ_001`(그 메일함을 정리하는 중) 등도 알림만 · 창은 그대로.
- 옮기는 동안 [옮기기]는 `aria-disabled` + `aria-busy`(누른 단추가 초점을 잃지 않게), 고르기 전에는 `disabled`.
- **하위까지 지우기**: 지우는 동안 나무의 모든 메일함(하위 포함)을 "지우는 중"으로 둔다 — 그 메뉴들이 꺼지고, 나무 안의 메일함을 따로 지우기 시작하지 않는다.
하위 메일함을 보고 있었어도 끝나면 받은편지함으로 간다. 하위가 없는 메일함은 지금처럼 `withChildren` 없이 보낸다.
- **차단할 때 받은 메일 옮기기**
- 「이 메일도 …」가 켜져 있으면 그 메일을 먼저 옮긴 뒤(지금 길 그대로 — 읽기 화면은 삭제처럼 바로 다음 메일로) 받은편지함 정리를 시작한다. 그 메일이 받은편지함에 있었으면 옮긴 수에 넣는다.
- 이 탭에서 받은편지함을 정리하는 중에 [다시 시도]를 누르면 "‘…’을(를) 차단했습니다. 받은편지함을 정리하는 중입니다. 끝난 뒤 다시 시도해 주세요." + 같은 [다시 시도](보드에 없다 — 필터 적용의 busy 와 같은 결).
- 그사이 차단을 풀었으면(404 `RES_001`) 진행 알림을 닫고 사전 문구만 보인다.
- 끝났거나 나아간 뒤 멈췄으면 개수를 다시 받고, 받은편지함 · 스팸메일함 목록을 보고 있으면 그 목록도 맞춘다(필터 적용과 같다).
- 되풀이(cursor · 25초 · 나아가지 못한 응답 3번)는 필터 적용과 함께 쓴다(`stores/inbox-cleanup-run.ts`).
- 이 탭에서 받은편지함 정리(필터 적용 · 차단 정리)가 하나라도 도는 동안은 필터 고치기 창의 「받은편지함에 이미 있는 메일도 정리하기」도 끈다 — 차단 창의 새 칸과 같은 규칙(`inboxBusy`)이다.
두 칸의 까닭 글은 같은 공용 글 "받은편지함을 정리하는 중이라 지금은 고를 수 없습니다."(Mailbox15 보드의 차단 칸 글)이고, 7단계 칸 글 "다른 필터로 받은편지함을 정리하는 중입니다. 끝난 뒤에 고를 수 있습니다."도
이 글로 바꿨다 — 칸을 끈 까닭이 필터가 아니라 차단 정리일 수도 있어서다(메인이 정함).
같은 까닭으로 필터 적용 알림의 [다시 시도]도 이 탭의 차단 정리를 보고(`inboxBusy`), 그때 글은 공용 글 "받은편지함을 정리하는 중입니다. 끝난 뒤 다시 시도해 주세요."다(7단계 "다른 필터로 …"에서 바꿨다).
- 이 탭에서 차단 정리가 도는 동안은 스팸메일함 비우기를 시작할 수 없다(메뉴 · 목록 머리 [비우기] · 확인 단추가 같은 판정 — 휴지통 비우기와 메일함 지우기를 함께 돌리지 않는 규칙과 같은 결).
옮겨 온 메일이 들어오자마자 영구 삭제되지 않게다. 반대로 이 탭에서 스팸메일함을 비우는 동안은 차단 창의 새 칸을 끄고(까닭 "스팸메일함을 비우는 중이라 지금은 고를 수 없습니다.") 차단 정리를 시작하지 않는다 —
알림의 [다시 시도]를 누르면 "‘…’을(를) 차단했습니다. 스팸메일함을 비우는 중입니다. 끝난 뒤 다시 시도해 주세요." + 같은 [다시 시도](2026-11-09 재리뷰).
필터 적용은 스팸메일함에 넣지 않으므로 막지 않는다(`inboxBusy` 는 그대로이고 차단 정리만 `mail-block-run.store` 의 `startBlock` 을 본다).
다른 창과는 서버가 요청마다 스팸메일함 처리 잠금을 잡아 겹친 요청을 429 로 막지만, 잠금은 요청 하나씩이라 두 창의 비우기와 차단 정리가 요청 사이에 번갈아 돌 수 있다 —
차단 정리가 옮겨 넣은 메일을 다른 창의 다음 비우기 요청이 지울 수 있다(알려진 한계 — 다른 창끼리의 휴지통 비우기와 메일함 지우기와 같다, 계약 2절 "보낸 사람 차단").
- **지금 보내기**
- 서버가 혹시 409 `MAIL_014`(이미 보냄)로 답해도 보낸 것으로 다룬다("메일을 보냈습니다." — 약속은 200 이다).
- 503 `MAIL_001` · `SYS_001` 의 갈래(임시보관함 · 예약 그대로 · 예약만 풀림 · 다시 낸 발송이 기다림 · 다시 받지 못함)는 위 "예약한 메일 [지금 보내기]"에 적었다 — 사전 문구 알림은 끄고 화면 글 하나로 알린다.
판정은 `shared/utils/pending-send.ts` 의 `unsentReleaseState` 하나다(임시보관함을 먼저 보고, 다음으로 기다리는 발송의 종류). 다시 낸 발송이 보내기 취소를 기다리는 갈래는 리뷰에 없던 것이라 구현하며 정했다 —
같은 요청을 다시 보내도 서버가 그 상태로 200 을 주므로 보낸 것과 같게 알린다(받는 사람별 결과는 모르니 일부가 받지 못한 문장은 없다).
받는 사람이 모두 받지 않았거나(400 `MAIL_010` · 503 `MAIL_011`) 예약이 그사이 사라졌으면(404) 사전 문구 알림만 보이고 그 자리에 두며, 예약 표시 · 예약됨 수 · 보이는 목록을 서버 기준으로 다시 맞춘다.
- 보내기 취소 알림의 마감은 쓰기 화면과 같은 규칙이다(설정한 초를 알면 "지금 + 초 − 1초", 모르면 보낼 시각까지 · 30초를 넘지 않게 — `undoToastDeadline`).
- 안내 줄이 좁으면(글이 12rem 보다 좁아질 때) 단추 셋을 묶음째 다음 줄 오른쪽으로 보낸다 — 단추가 셋이 되며 360 폭에서 글이 한 글자 폭으로 눌리지 않게(보드는 넓은 폭만 그렸다).
**읽기 화면 · 목록 · 상단 바**
- **좁은 읽기 칸의 기준(보드에 없는 것)**: 컨테이너는 도구 줄 띠(`.reader__toolbar-bar` — 칸 전체 폭)다. 목록 보기는 앞에 [목록으로] · 이전/다음이 함께 놓여 이름이 다 보이는 줄이 800(콘텐츠 열)보다 조금 넓다 —
그래서 목록 보기만 단추 줄의 최대 폭을 풀고 접는 폭을 850 · 휴지통 875 로 두었다(570 실측에 [목록으로] · 이전/다음 · 위치 글을 더한 어림 — 브라우저에서 다시 잰다). 화면 폭 기준은 tokens.css 머리에 한 줄씩 적었다.
- **메뉴 폭**: 옮길 메일함 메뉴는 이름 길이만큼(200 부터 320 까지 — 좁은 화면은 화면 폭 − 80 까지) 넓어지고 그보다 긴 이름은 줄을 바꾼다(보드는 줄을 바꾸지 않는 짧은 이름만 그렸다).
- **좌우 바꾸기**: 오른쪽 끝에 맞춘 메뉴(⋯)가 왼쪽 밖으로 넘칠 때도 같은 규칙으로 단추 왼쪽 끝에 맞춘다(360 에서 ⋯ 가 둘째 줄 맨 앞으로 내려간다). 양쪽 다 넘치면 덜 넘치는 쪽이다(위아래와 같은 결).
- **흐린 줄 더**: 하위 메일함이 있는 휴지통 · 임시보관함(다른 메일 프로그램이 지운 폴더를 휴지통 아래에 둔다)도 까닭 글 없이 흐린 줄로 남겨 그 하위 메일함을 고를 수 있게 한다.
흐린 줄 아래에 보일 하위 메일함이 하나도 없으면 그 줄도 뺀다.
- **전체 메일함 검색에서 [이동 ▾] · 끌기**: 고른 메일이 모두 든 메일함만 "메일이 있는 메일함"으로 친다(메일마다 다르면 아무 데나 옮길 수 있다 — 지금과 같다).
- **끌어다 놓기**
- 줄 전체가 끌린다(링크만이 아니라) — 목록 보기 표의 글은 마우스로 끌어 고를 수 없게 된다(두 번 눌러 고르기는 된다).
- 끄는 표는 메일 화면에 늘 그려 두는 화면 밖 요소를 브라우저가 찍은 것이다(setDragImage) — 운영체제 · 브라우저마다 투명도 · 커서 모양이 조금 다르다.
- 이 탭에서 시작한 끌기만 받는다(다른 탭 · 바깥에서 끌어 온 같은 꼴의 자료는 놓을 수 없다).
- 열어 둔 메일을 끌어 옮겼으면 목록 도구 줄로 옮길 때처럼 목록 주소로 바꿔 끼운다.
- 패널의 메일함이 많아 아래가 가려져 있으면 패널 끝에서 저절로 스크롤되는지는 브라우저가 한다 — 브라우저에서 확인한다.
- **메일 화면 파일 놓기 무시**: 쓰기 화면도 메일 화면 안이라 두 창 처리기가 함께 걸린다 — 파일을 받는 화면(쓰기 화면 · 서명 설정)이 떠 있으면 무시하는 쪽이 아무것도 하지 않는다(어느 쪽이 먼저 걸렸는지와 상관없이 쓰기 화면이 받는다).
- **상단 바**: 줄어든 이름 · 주소의 title 은 이름 · 주소 칸 전체에 둔다(넓은 폭에서 주소가 줄어도 같은 글).
- **같은 대화 [모두 펼치기]**
- 손으로 펼친 줄은 지금처럼 누른 그 자리에서 본문을 받고 받는 대로 읽음이다. [모두 펼치기]로 펼친 줄만 화면에 들어올 때 차례로 받는다 — 차례가 오기 전에 화면 밖으로 나간 줄은 건너뛰고 다시 들어오면 다시 줄을 선다.
- 받는 사이 화면 밖으로 나갔으면 읽음으로 바꾸지 않고, 다시 들어올 때 읽음이다(읽음이 실패해도 드나들 때마다 다시 묻지 않는다).
- 화면에 들어왔는지 알 수 없는 브라우저(IntersectionObserver 없음)는 펼친 줄이 생기면 들어온 것으로 본다 — 그래도 하나씩 받는다.
- 펼칠 줄이 없으면(지금 메일만 받고 나머지를 받는 중) 단추가 없다. 그사이 지워진 메일(본문이 404)의 줄에는 머리 [답장]도 없다(아래 [답장]과 같다).