generated from ttipo/project-base
chore: 14단계 검색 색인을 앱 로그인 브랜치에 합친다
앱 새 메일 알림(푸시)의 서버 일을 앱 세션과 14단계 위에서 하려고 합친다. 겹친 곳은 두 곳이다. auth.module.ts 는 import 둘을 함께 두고, AuthService.login() 은 enrollAfterLogin(14단계)을 남긴 뒤 recordSignIn(앱 로그인)으로 기록한다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+108
-10
@@ -297,7 +297,8 @@ API 와 서버 규칙은 계약 2절 "새 메일 확인" · "검색" · "여러
|
||||
- **알림**(오른쪽 아래 · 되돌리기가 있으면 10초): "메일 N통을 보낸편지함으로 옮겼습니다." [되돌리기] · "메일 N통을 휴지통으로 옮겼습니다." [되돌리기] ·
|
||||
"메일 N통을 영구 삭제했습니다." · 일부만 처리했으면 뒤에 "1통은 그사이 옮겨졌거나 지워져 그대로 두었습니다."
|
||||
(메일함 이름의 받침에 맞춰 '으로/로'). 읽음 · 중요 표시는 목록이 바로 바뀌므로 알리지 않는다.
|
||||
- **영구 삭제 확인**: 휴지통에서만. 제목 "메일 N통을 영구 삭제할까요?" · 글 "휴지통에서 지운 메일은 되돌릴 수 없습니다." ·
|
||||
[되돌리기] 뒤 알림은 한 메일함으로 되돌렸으면 "메일 N통을 받은편지함으로 되돌렸습니다.", 여러 곳이면 "메일 N통을 되돌렸습니다."(2026-11-07 — 잇달아 되돌린 알림을 가를 수 있게).
|
||||
- **영구 삭제 확인**: 휴지통에서만. 제목 "메일 N통을 영구 삭제할까요?"(대화로 묶은 휴지통에서 줄을 골랐으면 "휴지통에 있는 메일 N통을 영구 삭제할까요?" — 14단계) · 글 "휴지통에서 지운 메일은 되돌릴 수 없습니다." ·
|
||||
[영구 삭제](#B42318 채움) [취소] — 열리면 「취소」에 초점이 간다.
|
||||
- 메일함을 바꾸거나 검색하면 선택을 푼다.
|
||||
|
||||
@@ -539,10 +540,10 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- **보낸 뒤 알림**(오른쪽 아래): "메일을 보냈습니다." + [보내기 취소] + 아래 남은 시간 막대(설정한 초 — 마우스를 올려도 멈추지 않는다) → 시간이 지나면 저절로 닫힌다.
|
||||
일부가 받지 못하면 빨간 알림 "보냈지만 1명은 받지 못합니다. 주소 (까닭)" + [보내기 취소](주소를 고쳐 다시 보낼 수 있게).
|
||||
[보내기 취소] → "보내기를 취소했습니다. 쓰던 메일로 돌아왔습니다." + 그 메일로 쓰기 화면(받는 사람 · 제목 · 본문 · 첨부 그대로). 늦게 누르면 "이미 보낸 메일이라 취소할 수 없습니다."
|
||||
예약 → "10월 30일 (금) 오전 9:00 에 보냅니다." + [예약됨 보기]. 로그아웃 · 다른 계정 로그인 때 알림은 닫는다 — 메일은 그대로 보내진다.
|
||||
예약 → "10월 30일 (금) 오전 9:00에 보냅니다." + [예약됨 보기]. 로그아웃 · 다른 계정 로그인 때 알림은 닫는다 — 메일은 그대로 보내진다.
|
||||
- **「예약됨」**: 메일함 패널의 보낸편지함 아래(예약한 메일이 있을 때만, 시계 아이콘, 오른쪽에 통 수). 목록은 받는 사람 · 제목 · 보낼 시각(청록 글 — "10월 30일 (금) 오전 9:00") 차례,
|
||||
고르는 칸 · 여러 통 처리는 없다. 보낸 메일은 저절로 빠지고 보낸편지함에 남는다.
|
||||
- **예약한 메일을 열면**: 머리 아래 청록 안내 줄 "10월 30일 (금) 오전 9:00 에 보낼 메일입니다." + [고치기](예약을 취소하고 쓰기 화면으로) [예약 취소](보내지 않고 임시보관함으로 —
|
||||
- **예약한 메일을 열면**: 머리 아래 청록 안내 줄 "10월 30일 (금) 오전 9:00에 보낼 메일입니다." + [고치기](예약을 취소하고 쓰기 화면으로) [예약 취소](보내지 않고 임시보관함으로 —
|
||||
알림 "예약을 취소하고 임시보관함으로 옮겼습니다." + [고치기]). 답장 · 전달 · 스팸 신고 단추는 없다. [삭제]는 예약을 취소한 뒤 휴지통으로(확인 창).
|
||||
- **보낸편지함**: 아직 보내지 않은 메일은 날짜 칸이 "예약 · 10월 30일 오전 9:00"(청록) — 보내기 취소를 기다리는 동안은 "보내는 중".
|
||||
|
||||
@@ -571,7 +572,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
### 쓰기 화면 — 첨부 칸 (`ComposeLargeFiles`)
|
||||
|
||||
- **링크로 바뀌는 때**: 파일 하나가 20MB 를 넘을 때, 메일에 담은 파일의 합계를 25MB 넘게 만들 때(고른 차례대로 그 파일부터), 메일에 담지 못하는 형식(mp4 · mov · dwg · dxf · psd · ai · 7z)일 때 저절로 링크로 보낸다.
|
||||
묻는 창은 없다 — 합계 때문에 바뀌면 칸 아래 ⓘ 한 줄 "‘회의자료_전체.pptx’(9.4 MB)는 메일에 담으면 합계가 25MB 를 넘어 링크로 보냅니다."(role=status). 받는 사람이 사내뿐이어도 같은 규칙이다.
|
||||
묻는 창은 없다 — 합계 때문에 바뀌면 칸 아래 ⓘ 한 줄 "‘회의자료_전체.pptx’(9.4 MB)는 메일에 담으면 합계가 25MB를 넘어 링크로 보냅니다."(role=status). 받는 사람이 사내뿐이어도 같은 규칙이다.
|
||||
메일에 담은 파일도 줄의 링크 단추(40 · 「링크로 보내기」 — 마우스를 올리면 같은 글)로 바꿀 수 있다 — **이 쓰기 화면에서 고른 파일에만** 있다(전달 원본 · 저장본에서 이어 온 첨부에는 없다 — 처음 판).
|
||||
링크 → 메일에 담기 단추는 없다(빼고 다시 고르면 한도 안에서 담긴다 — 아래 "처음 판에서 미룬 것").
|
||||
- **안내문**: "파일당 최대 20MB · 합계 25MB · 최대 20개 · 더 큰 파일은 파일당 2GB, 10개까지 링크로 보냅니다".
|
||||
@@ -590,7 +591,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
| 위험 | "위험한 내용이 발견되어 보낼 수 없습니다. 이 파일을 빼 주세요."(경고 삼각형) | 빨간 줄 · ✕ |
|
||||
|
||||
- **올리기 전 검사**(지금 첨부 사전 검사와 같은 결 — 넘으면 올리지 않고 빨간 줄, 그 파일만 빠지고 보내기는 막지 않는다):
|
||||
"파일 하나는 2GB 까지 보낼 수 있습니다." · "링크로 보내는 파일은 메일 한 통에 10개까지입니다." ·
|
||||
"파일 하나는 2GB까지 보낼 수 있습니다." · "링크로 보내는 파일은 메일 한 통에 10개까지입니다." ·
|
||||
"대용량 첨부는 10GB까지 보관할 수 있어 더 올릴 수 없습니다. 보내지 않은 파일을 지우거나 링크를 끊으면 바로 공간이 생깁니다. [대용량 첨부 보기]" ·
|
||||
"지금은 서버에 공간이 모자라 올릴 수 없습니다. 잠시 후 다시 시도해 주세요." · 형식은 `FILE_001` 사전 문구.
|
||||
링크로 보낼 수 있는 형식 = 일반 첨부 목록 + mp4 · mov · dwg · dxf · psd · ai · 7z. 실행 파일(exe · msi · bat · cmd · js · vbs · scr 등)과 디스크 이미지(iso · img · vhd)는 막는다. 파일 고르기 창은 두 목록을 합친 형식만.
|
||||
@@ -651,7 +652,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
"한국 시간으로 2026년 11월 29일(일)까지 내려받을 수 있습니다. 30일 남았습니다."(날짜만 굵게) → 파일 목록(줄마다 아이콘 36 · 이름 · "1.2 GB · 보안 검사 결과 이상 없음" · [내려받기](채운 단추 44)) →
|
||||
"내려받다 끊기면 브라우저의 다운로드 목록에서 다시 시도해 주세요. 받은 곳부터 이어서 받습니다." →
|
||||
청록 상자 "이 화면은 로그인이나 비밀번호를 묻지 않습니다. 묻는 화면이 나오면 닫고 보낸 사람에게 확인해 주세요." →
|
||||
"받을 이유가 없는 파일이면 postmaster@arasystem.it.kr 로 알려 주세요."(운영 설정 — 처음 값 postmaster@메일 도메인) → 영어 한 줄.
|
||||
"받을 이유가 없는 파일이면 postmaster@arasystem.it.kr(으)로 알려 주세요."(조사는 주소의 끝 글자로 고르고 영문이면 "(으)로" — 주소는 운영 설정이라 바뀔 수 있다, 2026-11-07)(운영 설정 — 처음 값 postmaster@메일 도메인) → 영어 한 줄.
|
||||
- 메일 제목 · 본문 · 받는 사람, 내려받은 수 · 남은 수는 보이지 않는다. [내려받기]는 형식과 상관없이 늘 저장이고, 누르기 직전에 상태를 한 번 더 묻는다. noindex · no-referrer.
|
||||
- **상태**(모든 상태에서 머리 · 영어 한 줄은 그대로. 신고 주소 줄은 링크 정보를 받은 상태에서만 — 없는 링크 · 요청이 너무 많음 · 오류 응답에는 그 값이 없다):
|
||||
- 검사 중 — 모두 검사 중이면 제목 "보안 검사 중입니다" / "파일을 검사하고 있습니다. 몇 분 뒤 이 화면을 다시 열어 주세요." + [다시 확인]. 일부면 그 줄만 "보안 검사 중" + [내려받기] `aria-disabled`.
|
||||
@@ -685,7 +686,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- 최근에 보낸 차례 · 20개씩 [더 보기].
|
||||
- **[링크 복사]는 두지 않는다** — 서버는 링크를 해시로만 알아 원래 주소를 모른다. 주소는 보낸 메일의 읽기 화면(보낸 사람 자신)에서 복사한다.
|
||||
- [링크 끊기] → 읽기 화면과 같은 확인 창 · 같은 알림(창 글에 메일 제목). 끊는 동안 그 덩이의 [링크 끊기]를 막고, 끝나면 초점을 덩이 머리로.
|
||||
- **빈 상태**: 사용량 "0 / 10 GB" + "링크로 보낸 파일이 없습니다" / "메일 쓰기에서 20MB 가 넘는 파일을 고르면 링크로 보냅니다. 받는 사람은 보낸 날부터 30일 동안 내려받을 수 있습니다."
|
||||
- **빈 상태**: 사용량 "0 / 10 GB" + "링크로 보낸 파일이 없습니다" / "메일 쓰기에서 20MB가 넘는 파일을 고르면 링크로 보냅니다. 받는 사람은 보낸 날부터 30일 동안 내려받을 수 있습니다."
|
||||
보내지 않은 파일이 있으면 그 묶음은 위에 그대로 있다. 목록을 받지 못하면 지금의 목록 오류와 같은 모양.
|
||||
- **위험이 나오면** 빨간 알림 "올린 대용량 파일 ‘견적_요청서.zip’에서 위험한 내용이 발견되어 막았습니다." + [대용량 첨부 보기] —
|
||||
웹메일을 열어 둔 동안 실시간 알림(`large-file`)으로 바로 띄운다. 그때 열려 있지 않았으면 알림은 없고 목록 · 첨부 칸의 상태(위험)로만 안다 —
|
||||
@@ -1034,7 +1035,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
같은 대화(답장 · 전달로 이어진 메일)를 읽기 화면에 늘 보이고, 목록은 사용자가 켜면 대화로 묶는다.
|
||||
까닭은 엔진 실측(2026-11-03)이다 — 엔진 스레드는 `회신:` · `답장:` · `전달:` 을 떼지 않아 한국어 Outlook 의 답장이 따로 떨어지고, 엔진 검색은 어절이 통째로 같을 때만 맞는다.
|
||||
데이터 · API 규칙(색인 · 대화 · 조건)은 구현 때 계약 2절에 더한다. 구현 · 배포는 전환 뒤 안정화(1~2주) 다음이다.
|
||||
새 토큰은 없다 — 조건 칸 40 은 필터 조건 칸의 밀도 예외(`--size-filter-control`), [대화로 묶기] 36 은 안내 줄 단추의 밀도 예외(`--size-notice-action`)를 다시 쓴다.
|
||||
새 크기 토큰은 없다 — 조건 칸 40 은 필터 조건 칸의 밀도 예외(`--size-filter-control`), [대화로 묶기] 36 은 안내 줄 단추의 밀도 예외(`--size-notice-action`)를 다시 쓴다(구현 때 [조건] 32 도 — 아래 "14단계 구현하며 정한 보완"). 강조 노랑만 색 토큰(`--color-search-mark`)을 하나 둔다.
|
||||
문구는 위 「화면 문구 — 사용자에게 말하듯」을 따른다.
|
||||
|
||||
### 검색 · 조건 검색 · 강조 (`Search14`)
|
||||
@@ -1050,8 +1051,8 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- **결과 줄**(높이 64, 세 줄): 보낸 사람 · 시각 / 제목(+ 첨부 표시) / 미리 보기 한 줄(맞은 말 둘레). 제목 · 미리 보기의 맞은 말은 `<mark>`(노랑 `#FCE7A4` · 글자색은 그대로).
|
||||
- **읽기 화면 강조**: 제목 · 본문 · 첨부 이름의 맞은 말. 본문은 서버가 정화한 HTML 의 글 조각에만 `<mark>` 를 넣는다(찾기와 같은 정규화 · 부분 일치). 맞은 곳 사이 이동(다음 · 이전)은 이번에 넣지 않는다.
|
||||
- **검색 준비 중**: 지난 메일의 색인을 만드는 동안 그 직원의 검색은 지금 방식(엔진) 그대로이고, 결과 머리 아래 안내 줄(옅은 청록 바탕 · role=status)
|
||||
"지난 메일을 검색할 수 있게 준비하고 있습니다. 준비가 끝날 때까지는 낱말이 통째로 같은 메일만 찾습니다."
|
||||
- **결과 없음**: "‘계약’에 맞는 메일이 없습니다" / "다른 말로 찾거나 조건을 줄여 보세요." + [전체 메일함에서 찾기](그 메일함에서 찾을 때만). 지금의 한계 안내("낱말 단위로 찾습니다 …")는 색인이 준비된 뒤에는 뺀다.
|
||||
"지난 메일을 검색할 수 있게 밤사이에 준비하고 있습니다. 메일이 많으면 며칠 걸릴 수 있고, 그동안은 낱말이 통째로 같은 메일만 찾습니다."(2026-11-07 사용자 관점 점검 — 색인은 밤에만 채워, 낮 내내 같은 글이면 멈춘 줄 안다)
|
||||
- **결과 없음**: "‘계약’에 맞는 메일이 없습니다" / "다른 말로 찾거나 조건을 줄여 보세요." + [전체 메일함에서 찾기](그 메일함에서 찾을 때만). 지금의 한계 안내는 색인이 준비된 뒤에는 뺀다 — 준비 중에는 위 안내 줄과 겹치지 않게 "준비가 끝나기 전에는 ‘서버이전’의 ‘서버’처럼 붙여 쓴 말의 일부나, 제목 맨 앞의 [공지]나 Re: 같은 말머리로는 찾지 못합니다."(2026-11-07).
|
||||
- 검색 결과는 대화로 묶지 않는다(한 통씩).
|
||||
|
||||
### 읽기 화면 — 같은 대화 (`Thread14`)
|
||||
@@ -1073,6 +1074,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
### 처음 판에서 미룬 것
|
||||
|
||||
본문 강조의 다음 · 이전 이동, 검색어 줄임말(from: 같은 꼴), 대화째 끌어다 놓기, 첨부 파일 안의 글 찾기, 끝이 바뀐 말 찾기(형태소 분석).
|
||||
같은 대화 칸 머리의 [모두 펼치기]와 펼친 줄 머리의 [답장]도 다음 후보로 둔다(2026-11-07 사용자 관점 점검에서 나온 제안 — 사용자가 미루기로 정함). 지금은 줄마다 펼치고, [답장]은 펼친 본문 아래에만 있다.
|
||||
|
||||
## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다)
|
||||
|
||||
@@ -1374,3 +1376,99 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- **설정 › 대용량 첨부**: 보낸 링크 머리의 메일 제목이 없으면(지웠거나 읽지 못함 — `subject: null`) 글자만, 빈 제목은 메일이 있는 것이라 링크다.
|
||||
[지우기]가 `FILE_003`(그사이 보내져 링크에 실림)이면 목록 · 사용량을 다시 받고 사전 문구로 알린다.
|
||||
- **내려받기 화면**: 보낸 사람 이름이 비어 있으면 주소를 쓴다. 불러오는 동안은 `role="status"` "불러오는 중"(자리 막대는 `aria-hidden`).
|
||||
|
||||
### 14단계 구현하며 정한 보완 — 검색 (2026-11-06 — 캔버스에는 아직 없다)
|
||||
|
||||
주소 · 이어 찾기의 값은 계약 2절 "검색 — 14단계"의 클라이언트 줄이 같은 내용이다.
|
||||
|
||||
- **크기**: [조건] 단추는 높이 32 — 목록 표 별 · 보기 전환의 밀도 예외(`--size-compact-control`)를 다시 쓴다. 패널의 칸 · 단추 40(`--size-filter-control`), 조건 줄 40 · 줄 안 [조건 지우기] 36(`--size-notice-action`). 새 크기 토큰은 없다.
|
||||
- **[조건] 패널**
|
||||
- 단추의 title "조건으로 찾기", 조건이 걸려 있으면 화면 낭독에 ", 조건이 걸려 있음"을 붙인다.
|
||||
- 칸 안내는 "이름이나 메일 주소"(보낸 사람 · 받는 사람) · "제목에 든 말" · "내용에 든 말"(칸 이름 "내용"과 같게 — 2026-11-07)이고, 날짜 칸은 "시작 날짜" · "끝 날짜"다.
|
||||
- 날짜가 거꾸로면 알림 대신 칸 아래 "끝 날짜를 시작 날짜와 같거나 그 뒤로 골라 주세요.", 고를 수 있는 날짜 밖이면 "날짜는 1970년 1월 1일부터 9999년 12월 30일 사이에서 골라 주세요."(날짜 고르기는 이미 막고, 해를 손으로 칠 때만 보인다).
|
||||
- 패널의 [조건 지우기]는 패널 안의 칸만 비운다 — 보낸 사람 · 받는 사람 · 제목 · 내용 · 기간(전체 기간으로) · 날짜 · 첨부 체크와 칸 아래 알림까지(2026-11-07 사용자 확정 — 보드와 다르다).
|
||||
패널은 열린 채이고 찾지 않으며 초점은 그 단추에 남는다. 걸린 조건 · 조건 줄 · 결과는 [찾기]를 누를 때까지 그대로이고, [찾기] 없이 닫았다 다시 열면 걸린 조건으로 다시 채운다.
|
||||
비운 채 [찾기]를 누르면 검색어가 있으면 조건만 빼고 다시 찾고, 검색어도 없으면(조건만 걸린 검색) 검색을 닫는다. 걸린 조건을 바로 빼는 것은 결과의 조건 줄 [조건 지우기]다(조건만 걸린 검색이면 검색을 닫는다).
|
||||
- 패널이 열린 채 검색 칸에서 Esc 를 누르면 패널만 닫고 초점은 검색 칸에 남는다.
|
||||
- **기간**: "최근 N"은 고른 순간 날짜로 바꾼다 — 그만큼 앞의 같은 날부터 오늘까지(1주는 7일 전, 달은 말일을 넘지 않는다).
|
||||
- **조건 줄 글**: "조건: " 뒤에 쉼표로 잇는다 — "{이름} 님이 보낸 메일" · "{이름} 님이 받은 메일" · "제목에 ‘{말}’이/가 든 메일" · "내용에 ‘{말}’이/가 든 메일" · "최근 1달" 또는 "2026. 10. 1.부터 11. 5.까지"(같은 해면 끝 날짜의 해를 뺀다) · "…부터" · "…까지" · "첨부 있음". 시작과 끝이 같은 날이면 "2026. 11. 6. 하루", 보낸 사람 · 받는 사람 값이 내 이름이나 주소와 통째로 같으면(앞뒤 공백 · 대소문자 무시) "내가 보낸 메일" · "내가 받은 메일"(2026-11-07).
|
||||
- **결과 줄**: 분할 보기는 보드대로 세 줄 64 다. **목록 보기(표)는 행 38 그대로 두고 제목만 강조한다**(미리 보기 줄 없음).
|
||||
결과 줄에도 별 · 고르기 칸을 남겼다 — 3단계부터 검색 결과에서도 여러 통을 고르고 처리한다(보드의 결과 줄에는 없다).
|
||||
- **글**
|
||||
- 조건만으로 찾았을 때 머리 글은 "N통" · "찾는 중", 결과 없음은 "조건에 맞는 메일이 없습니다" / "조건을 바꾸거나 줄여 보세요.".
|
||||
결과 없음의 도움말은 검색어 + 조건이면 "다른 말로 찾거나 조건을 줄여 보세요.", 검색어만(걸린 조건 없음)이면 "다른 말로 찾아 보세요."(2026-11-07 — 조건이 없는데 "조건을 줄여"라고 했다).
|
||||
- 옛 한계 안내("낱말 단위로 찾습니다 …")는 색인을 채우는 동안(`building`)만 결과 없음 아래에 더한다.
|
||||
- 한 글자 안내: "한 글자로만 찾을 때는 본문은 빼고 제목, 보낸 사람과 받는 사람, 첨부 파일 이름에서 찾습니다. 본문까지 찾으려면 두 글자 이상으로 찾아 주세요." — 색인이 준비됐고 본문을 찾을 말(검색어나 내용 조건)이 있을 때만.
|
||||
- 준비 상태를 받지 못하면(요청 실패) 안내 줄 · 옛 한계 안내 · 한 글자 안내를 모두 보이지 않는다.
|
||||
- **[더 찾기]**: 한 번에 다 찾지 못하면 "아직 맞는 메일을 찾지 못했습니다" / "메일이 많아 한 번에 다 찾지 못했습니다. [더 찾기]를 누르면 이어서 찾습니다." + [더 찾기](찾는 동안 "찾는 중…").
|
||||
하나도 못 찾았으면 "찾는 중"을 이어 보이고, 하나라도 찾으면 먼저 보이면서 뒤에서 잇는다. 목록 보기의 [다음]은 이어 받는 동안 기다렸다가 넘기고, 그동안 꺼짐 · `aria-busy` 다.
|
||||
- **낱말이 너무 많은 주소**(손으로 고친 주소): "찾을 말이 너무 많습니다" / "검색어와 조건에 넣은 낱말은 모두 합쳐 8개까지 찾을 수 있습니다. 검색 칸이나 [조건]에서 낱말을 줄여 주세요." — [전체 메일함에서 찾기] · 알림 없이, 조건 줄의 [조건 지우기]는 남긴다.
|
||||
- **강조 색**: 노랑 `#FCE7A4` 는 색 토큰 `--color-search-mark` 하나로 두고, 본문 틀 안 CSS 는 같은 값을 직접 적는다(짝). 보낸 사람이 본문에 쓴 `<mark>` 도 같은 노랑으로 보인다(드물어 가르지 않는다).
|
||||
미리 보기 줄(회색 `--color-text-muted`)의 맞은 말만 글자를 `--color-text-secondary`(#4A5363)로 진하게 한다 — 회색 그대로면 노랑 위 대비가 4.05:1 로 4.5:1 에 못 미친다(2026-11-07 사용자 확정 — 보드와 다르다, 앱도 같다). 제목 · 읽기 화면 · 본문 틀의 강조는 진한 글 위라 글자색 그대로다.
|
||||
- 긴 안내 글(결과 없음 · [더 찾기] · 안내 줄)은 낱말 가운데서 줄을 바꾸지 않는다(`word-break: keep-all`).
|
||||
|
||||
### 14단계 구현하며 정한 보완 — 같은 대화 (2026-11-06 — 캔버스에는 아직 없다)
|
||||
|
||||
받기 · 다시 받기의 값은 계약 2절 "대화 — 14단계"의 클라이언트 줄이 같은 내용이다.
|
||||
|
||||
- **크기**: 줄 48 · 머리글자 28, 펼친 줄의 [답장] · [이 메일만 열기] 36(안내 줄 단추의 밀도 예외 `--size-notice-action` 재사용), [더 보기] · [다시 시도] 44. 새 크기 토큰은 없다.
|
||||
- **보드와 다르게 한 것**
|
||||
- 지금 보는 메일 줄의 바탕은 가장 가까운 기존 토큰 `--color-brand-tint`(#F3F8F8 — 보드 #F2F8F8, 시각 글자 대비 4.6:1).
|
||||
- 칸 제목은 h3 다 — 읽기 화면 제목이 h2 · 첨부 칸이 h3 이라 같은 층으로(보드는 제목을 h1 로 써서 h2 였다).
|
||||
- 펼친 본문은 읽기 화면과 같은 본문 틀(14px — 보드 13.5px)이다.
|
||||
- 펼친 줄에는 외부 이미지 안내 · 첨부 목록을 두지 않는다 — 본문을 줄였거나 HTML 을 보이지 못하면 읽기 화면과 같은 노란 안내 줄만 두고, 나머지는 [이 메일만 열기]로 연 읽기 화면에서 본다.
|
||||
다만 첨부가 있으면 본문 아래 · [답장] 위에 클립과 "첨부 파일 N개는 [이 메일만 열기]를 누르면 볼 수 있습니다."(12.5 회색 — 수는 읽기 화면과 같은 셈: 본문 그림 빼고 대용량 첨부 더해)를 둔다(2026-11-07 — 본문은 첨부했다는데 표시가 없었다).
|
||||
- **보드에 없어서 정한 것**
|
||||
- 칸 머리의 통 수는 다 셀 수 있을 때만 "같은 대화 N통"이고, 아직 다 받지 못해 수를 모르면 "같은 대화"다.
|
||||
- 남은 메일이 있으면 상자 아래 [더 보기](받는 동안 "불러오는 중…" · `aria-busy`).
|
||||
- 받지 못하면 칸 자리에 "같은 대화를 불러오지 못했습니다." + [다시 시도] + 문의 코드. 이 경로가 없는 옛 서버(404)면 칸이 없다.
|
||||
- 시각은 "오늘 오전 9:30" · "어제 …" · "10월 29일 (목) 오전 9:30" · 다른 해는 "2025. 10. 29. (수) …" 꼴이다.
|
||||
- 내가 보낸 메일 · 임시 저장본은 이름 자리에 "나" + 회색 메일함 이름("보낸편지함" · "임시보관함"), 다른 메일함에 든 내 메일은 "나"만. 지금 보는 메일 줄에는 회색 글을 두지 않는다(보드 그대로).
|
||||
- 미리 보기가 비면 제목을 보인다. 우리 웹 · 앱의 원문 인용 머리 "> 원본 메일"(끝에서 잘린 "> 원…" 포함)부터 뒤는 잘라 내고 남는 글이 없으면 제목이다 — 다른 메일 프로그램의 인용 꼴은 그대로(2026-11-07).
|
||||
- 줄 차례는 서버 차례(받은 시각 — 내 메일은 저장한 시각) 그대로이고, 보내기를 기다리는 메일(보내기 취소 · 예약)만 받은 줄의 맨 끝에 보낼 시각 차례로 둔다(2026-11-07 — 예약 줄이 시각 차례를 거슬렀다).
|
||||
- 임시 저장본 줄은 [답장]이 없고 [이 메일만 열기]가 쓰기 화면으로 연다. 예약한 메일은 [답장]이 없다(보내기 취소를 기다리는 메일은 읽기 화면처럼 있다).
|
||||
- 붙잡힌 메일의 시각 자리는 목록과 같은 보내기 취소 · 예약 표시(청록 굵게)다.
|
||||
- **펼치기**: 안 읽은 메일을 펼치면 읽음이 된다(읽기 화면에서 연 것과 같다). 접힌 줄의 받아 둔 본문은 최근 10통까지만 기억한다. 펼친 본문 안에서 Ctrl+P 는 그 펼친 메일의 인쇄 화면을 연다.
|
||||
- **안 읽은 줄**(2026-11-07 사용자 확정 — 보드와 다르다): 목록 줄과 같은 굵기 · 색이다 — 이름 650, 미리 보기 · 시각 600(붙잡힌 메일의 시각은 청록 그대로). 화면 낭독은 이름 앞에 목록과 같은 "안 읽음, ". 임시 저장본은 목록처럼 읽음 여부를 보이지 않는다.
|
||||
이 탭에서 바꾼 읽음(줄을 펼침 · 읽기 화면에서 연 메일 · 목록과 여러 통 처리의 읽음 · 안 읽음 표시 · 메일함 ⋯ › 모두 읽음)은 줄에 바로 보이고, 바꾸지 못해 되돌리면 줄도 되돌린다. 받는 동안 바꾼 것과 받기를 시작할 때 보내던 것은 받은 값보다 이 탭의 값을 믿는다.
|
||||
다른 탭 · 기기에서 바꾼 읽음은 다음에 칸을 다시 받을 때 보인다(그 줄을 펼치면 받은 본문대로 바로 풀린다). 읽은 줄의 이름(600)과 안 읽은 줄(650)의 차이는 작아 눈에 띄는 차이는 미리 보기 · 시각이다 — 읽은 줄 이름을 낮추는 것은 보드를 바꾸는 일이라 두었다.
|
||||
- **[이 메일만 열기]**: 분할 보기는 방문 기록을 쌓고(목록 줄을 누른 것과 같다), 목록 보기는 바꿔 끼운다(이전 · 다음과 같다 — [목록으로]가 기록을 쌓지 않게).
|
||||
지금 목록에 든 메일은 그 목록 안에서, 검색 결과 · 예약됨은 받아 둔 줄에 있을 때만 그 목록 안에서, 그 밖은 그 메일이 든 메일함에서 연다.
|
||||
- **초점**: 누른 단추가 사라지면(칸의 [다시 시도]가 성공 · [더 보기]로 끝까지 받음 · 다시 받아 줄이 빠짐) 그 줄의 펼침 단추 → 새 [다시 시도] → 칸 제목 차례로 옮기고, 칸이 통째로 사라지면 본문으로 옮긴다.
|
||||
[이 메일만 열기] 뒤에는 삭제 뒤 다음 메일로 갈 때처럼 따로 옮기지 않는다(두 길을 함께 고칠 거리).
|
||||
- **다시 받기**: 새 메일 · 지움 · 옮김으로 메일함 통 수가 바뀔 때와 쓰기 화면에서 돌아올 때만 다시 받는다 — 읽음 표시만 바뀐 소식으로는 받지 않는다(받으면 메일을 열 때마다 대화를 한 번 더 받는다). 칸의 읽음은 위 "안 읽은 줄"처럼 이 탭에서 바꾼 것만 바로 적는다.
|
||||
|
||||
### 14단계 구현하며 정한 보완 — 목록 대화로 묶어 보기 (2026-11-06 — 캔버스에는 아직 없다)
|
||||
|
||||
받기 · 맞추기 · 줄에 하는 일의 규칙은 계약 2절 "대화 — 14단계"의 클라이언트 줄(목록을 대화로 묶어 보기)이 같은 내용이다.
|
||||
|
||||
- **크기 · 모양**
|
||||
- [대화로 묶기]는 36 — 안내 줄 단추의 밀도 예외(`--size-notice-action`)를 다시 쓴다. 새 크기 토큰은 없다.
|
||||
분할 보기는 아이콘만(36×36 — 휴지통 [비우기]와 같은 결, 이름은 title · 화면 낭독), 목록 보기는 이름까지 보인다.
|
||||
분할 보기에 이름까지 넣으면(99) 1440 에서 개수 글 자리가 25px 남짓이라 아이콘으로 두고, 설정 설명이 말풍선 모양이라고 알린다(2026-11-07 잼).
|
||||
- 켠 모양은 옅은 청록 바탕 + 진한 청록 글자이고 테두리는 그대로다(보드의 #9FD3D7 테두리는 새 색 토큰이라 두지 않았다).
|
||||
- 설정 › 메일 › 보내기 · 읽기의 한 줄은 외부 이미지 아래 가는 선 뒤 — 끔 · 켬 전환(`.segmented--outline` — 보내기 취소와 같은 모양)과
|
||||
"답장과 전달로 이어진 메일을 메일함 목록에서 한 줄로 묶어 보여 줍니다. 메일함 목록 위의 말풍선 모양 [대화로 묶기] 단추로도 켜고 끌 수 있습니다." — 줄 이름은 단추와 같은 "대화로 묶기"(2026-11-07).
|
||||
- **줄**
|
||||
- 고르기 칸 · 별을 남겼다(보드의 줄에는 없다 — 검색 결과 줄과 같은 판단, 3단계부터 목록에서 여러 통을 고른다).
|
||||
메일이 모두 보내기를 기다리는 줄은 고르기 칸 · 별이 꺼짐(aria-disabled) + 까닭이고, 목록 보기에서 Ctrl · Shift + 클릭으로도 고르지 않는다.
|
||||
- 참여자: 다른 사람은 받은 차례대로, 나는 끝에 "나"(보드 "이도윤, 나" · "인사팀, 김민수, 나"). 보낸편지함 · 임시보관함에서 참여자가 나뿐이면 메일 목록처럼 "받는 사람: …"이다.
|
||||
임시보관함의 제목 없는 줄은 흐린 "(제목 없음)"이다(메일 줄과 같다).
|
||||
- 목록 보기(표)는 고르기 · 별 · 참여자(+ 회색 "N통" — 머리 글은 모든 메일함에서 "참여자", 2026-11-07) · 제목 · 날짜다 — **크기 칸은 뺐다**(대화의 크기는 마지막 메일의 크기가 아니다).
|
||||
표 제목은 "{메일함} 대화 목록", 머리 고르기는 "불러온 대화 모두 고르기", 고른 수는 "대화 N개 선택됨"이다(분할 보기의 도구 줄은 "N개 선택됨" — 화면 낭독은 "대화 N개 선택됨", 단추 일곱과 한 줄 53 이 되게, 2026-11-07).
|
||||
- 목록 머리가 좁으면 개수가 먼저 줄고 메일함 이름은 남는다(개수에 마우스를 올리면 다 보인다). **묶지 않았을 때의 머리 글도 보드 글로 바꿨다**("전체 9 · 안 읽음 2" → "메일 9통, 안 읽은 메일 2통").
|
||||
분할 보기(좁은 머리)는 안 읽은 메일이 있으면 "안 읽은 메일 N통"만, 없으면 "메일 N통" · "대화 N개"를 보이고 전체 글은 title · 화면 낭독에 둔다 — 머리의 칸 사이는 이름↔개수 6 · 머리↔단추 4 · 단추끼리 4(2026-11-07 — 1440 에서 안 읽은 수가 늘 잘렸다).
|
||||
- 줄에 한 옮기기 · 삭제 · 스팸 신고 · 스팸 아님 알림은 "{메일함}에 있는 메일 N통을 {갈 곳} 옮겼습니다." 꼴이다(줄의 통 수는 대화 전체라 "4통" 줄에서 "메일 2통"이 헷갈렸다 — 2026-11-07). 휴지통의 영구 삭제도 같은 꼴이다 — 확인 창 "휴지통에 있는 메일 N통을 영구 삭제할까요?" · 알림 "휴지통에 있는 메일 N통을 영구 삭제했습니다."(일부만 지웠으면 뒤의 "N통은 그사이 …"는 그대로). 묶지 않은 목록 · 읽기 화면은 그대로다.
|
||||
- 메일 화면은 탭이 다시 보일 때 설정을 마지막으로 받은 지 60초가 지났으면 조용히 다시 받는다 — 다른 탭 · PC 에서 바꾼 묶기면 목록을 다시 열고, 보내기 취소 초 · 외부 이미지도 받은 값을 쓴다(받는 동안 단추를 숨기지 않고, 세션이 끝났으면 받지 않는다 — 2026-11-07).
|
||||
- "새 메일 N통 · 맨 위로"는 맨 앞에 새로 생긴 줄을 1통, 새 메일이 든 기존 줄을 새 메일 수로 센다.
|
||||
- **줄의 별**: 중요 표시가 하나라도 있으면 모두 풀고, 없으면 모두 표시한다. 보내기를 기다리는 메일만 중요 표시가 남으면
|
||||
"보내기를 기다리는 메일은 목록에서 중요 표시를 바꿀 수 없습니다. 메일을 열어 바꿔 주세요."
|
||||
- **읽기 화면**
|
||||
- [이전] · [다음]은 대화 사이를 오간다(앞뒤 줄의 마지막 메일). 위치는 "n / 대화 수"이고, 같은 대화 칸의 [이 메일만 열기]로 연 더 오래된 메일도 그 메일이 든 줄을 기준으로 한다. [목록으로]는 그 줄로 초점을 돌려준다.
|
||||
- [삭제](한 통) 뒤 열 메일(분할 보기 — 목록 보기는 지금처럼 목록으로): 그 줄에서 지운 메일의 다음 칸(더 오래된 메일), 없으면 앞 칸 →
|
||||
줄이 비면 다음 줄의 마지막 메일, 없으면 앞 줄의 마지막 메일. 아직 다시 받기 전이라 줄에 남아 있는 옛 마지막 메일이면 남은 메일의 첫 칸이다(메일 목록의 삭제 뒤와 같은 결).
|
||||
- **첫 진입**: 설정을 받기 전에는 이 브라우저가 그 직원에게서 마지막으로 안 값(처음이면 끔)으로 연다 — 서버 값이 다르면 받는 즉시 바꾼다.
|
||||
설정을 받지 못했으면 그 값대로 두고 [대화로 묶기]도 누를 수 있다(받는 동안에는 단추를 숨긴다). 로그아웃해도 그 값은 남고, 다른 직원이 로그인하면 그 직원의 값이다.
|
||||
- **알려진 한계**: 여러 통인 줄의 마지막 메일만 읽으면 다음 맞추기까지 줄이 굵은 채다. 마지막 메일이 아닌, 보내기를 기다리는 메일은 풀린 뒤에도 다음 맞추기까지 고를 수 없다(계약 2절).
|
||||
분할 보기 머리는 [비우기] 단추가 있는 휴지통 · 스팸메일함, 세 자리 이상 안 읽은 수, "받은편지함"보다 긴 메일함 이름, 1120~1220 폭에서는 1440 에서도 개수 글이 잘린다(title 로 다 보인다).
|
||||
분할 보기 도구 줄의 "N개 선택됨"은 세 자리부터 두 줄로 넘친다(메일 목록의 "123통 선택됨"도 같다).
|
||||
|
||||
+178
-5
@@ -264,8 +264,11 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙
|
||||
| `GET /api/mail-quota` | — | 200 `MailQuota` | `MAIL_001` |
|
||||
| `GET /api/mail-state` | — | 200 `MailState` (아래 "새 메일 확인") | 503 `MAIL_001` |
|
||||
| `GET /api/mail-events` | — | `text/event-stream` (표준 래퍼 없음 — 아래 "실시간 알림", 5단계) | 401 `AUTH_001`, 429 `REQ_001`(한 직원의 연결 5개 초과), 503 `MAIL_001` |
|
||||
| `GET /api/messages` | `mailboxId` · `cursor` · `limit`(기본 20, 최대 100) · `q` — `mailboxId` 와 `q` 중 하나는 있어야 한다(`q` 만 있으면 휴지통·스팸메일함을 뺀 모든 메일함에서 찾는다 — 아래 "검색") | 200 `{ items: MessageSummary[], pageInfo }` — 받은 시각 내림차순, `total` 포함 | 400 `VAL_001`, `MAIL_001` |
|
||||
| `GET /api/messages/{id}` | `images`(선택 — `show` 면 외부 이미지를 보이는 본문, 9단계 "외부 이미지 보기") | 200 `MessageDetail` — **조회만으로 읽음 처리하지 않는다** | 400 `VAL_001`(모르는 `images` · 모르는 쿼리 — 9단계부터, 전에는 무시했다), 404 `RES_001` |
|
||||
| `GET /api/messages` | `mailboxId` · `cursor` · `limit`(기본 20, 최대 100) · `q` · 조건 `from` · `to` · `subject` · `body` · `after` · `before` · `hasAttachment`(14단계) — `mailboxId` 와 검색(`q` 또는 조건) 중 하나는 있어야 한다(검색만 있으면 휴지통·스팸메일함을 뺀 모든 메일함에서 찾는다 — 아래 "검색" · "검색 — 14단계") | 200 `{ items: MessageSummary[], pageInfo }` — 받은 시각 내림차순, `total` 포함(14단계 색인 방식은 첫 쪽만 · 1,000까지 — 넘으면 `null`) | 400 `VAL_001`, `MAIL_001` |
|
||||
| `GET /api/messages/{id}` | `images`(선택 — `show` 면 외부 이미지를 보이는 본문, 9단계 "외부 이미지 보기") · 검색 쿼리 `q` · `from` · `to` · `subject` · `body`(선택 — 맞은 말 강조, 14단계) | 200 `MessageDetail` — **조회만으로 읽음 처리하지 않는다** | 400 `VAL_001`(모르는 `images` · 모르는 쿼리 — 9단계부터, 전에는 무시했다), 404 `RES_001` |
|
||||
| `GET /api/messages/{id}/conversation` | `cursor` · `limit`(기본 100, 최대 100) | 200 `{ items: MessageSummary[], pageInfo, basis }` — 그 메일의 같은 대화, 오래된 차례(아래 "대화 — 14단계") | 400 `VAL_001`, 404 `RES_001`, 503 `MAIL_001` |
|
||||
| `GET /api/conversations` | `mailboxId`(필수) · `cursor` · `limit`(기본 20, 최대 100) — 검색 쿼리는 받지 않는다 | 200 `{ items: ConversationSummary[], pageInfo, basis }` — 그 메일함을 대화로 묶은 목록(아래 "대화 — 14단계") | 400 `VAL_001`, 404 `RES_001`(없는 메일함), 503 `MAIL_001` |
|
||||
| `GET /api/search-index` | — | 200 `{ status, done, total }` — 그 직원의 검색 색인 준비 상태(아래 "검색 색인", 14단계) | — |
|
||||
| `PATCH /api/messages/{id}` | `{ isRead?, isFlagged?, mailboxId?, isJunk? }` (하나 이상 — `isJunk` 는 4단계 "스팸 · 원문") | 200 `MessageSummary` | 404 `RES_001`, 400 `VAL_001`, 409 `MAIL_015`(붙잡힌 메일의 `mailboxId` · `isJunk` — 9단계, 아래 "보내기 취소 · 예약 발송"), 503 `MAIL_001`(붙잡혔는지 확인하지 못함 — 바꾸지 않았다) |
|
||||
| `PATCH /api/messages` | `{ ids, isRead?, isFlagged?, mailboxId?, isJunk? }` — 여러 통 (아래 "여러 통 한꺼번에") | 200 `MessagesPatched` — 붙잡힌 메일(9단계)의 `mailboxId` · `isJunk` 는 바꾸지 않고 `failedIds` 로 | 400 `VAL_001`, 404 `RES_001`(없는 `mailboxId` — 아무것도 바꾸지 않음), 503 `MAIL_001` |
|
||||
| `DELETE /api/messages/{id}` | — | 204 — 휴지통이 아니면 휴지통으로 이동, 휴지통에만 있으면 영구 삭제(확인한 상태 그대로일 때만 — 아래). 휴지통과 다른 메일함에 함께 있으면 휴지통에서만 뺀다. **웹 클라이언트는 쓰지 않는다**(3단계 — 아래 "여러 통 한꺼번에") | 404 `RES_001`, 409 `MAIL_015`(붙잡힌 메일 — 9단계), 503 `MAIL_001`(확인하는 사이 계속 바뀜 — 지우지 않았다) |
|
||||
@@ -662,7 +665,7 @@ interface MailState {
|
||||
영문 · 숫자로 끝나는 낱말(ERP · 2026)은 받침을 알 수 없어 두 가지를 다 붙인다. 따옴표 구절에는 붙이지 않는다.
|
||||
조사 목록은 서버 상수 한 곳(`jmap/search-terms.ts`)에 둔다 — 한글로 끝나는 낱말은 형태 44개, 영문 · 숫자로 끝나면 59개가 되고,
|
||||
개발 엔진에서 낱말 8개(조건 472개)를 전체 메일함에서 찾는 데 0.16초였다.
|
||||
- **알려진 한계** (엔진 색인 때문 — 자체 검색 색인을 두기 전까지): 붙여 쓴 말의 일부('회의실'의 '회의', '서버이전'의 '서버'), 앞부분만 친 말('견적' → '견적서'),
|
||||
- **알려진 한계** (엔진 색인 때문 — 14단계부터는 그 직원의 자체 색인을 채우는 동안(`building`)의 검색에만 남는다, 아래 "검색 — 14단계"): 붙여 쓴 말의 일부('회의실'의 '회의', '서버이전'의 '서버'), 앞부분만 친 말('견적' → '견적서'),
|
||||
끝이 바뀐 말('보냈습니다'의 '보내'), **제목 맨 앞의 말머리**('[공지]' · '[긴급]' · 'Re:' · 'Fwd:' — 엔진이 제목을 정규화하며 색인에서 뺀다. 제목 가운데의 '[결재]'는 찾는다).
|
||||
화면은 결과가 없을 때 이 한계를 한 줄로 알린다.
|
||||
- 결과는 받은 시각 내림차순, `total` 포함. 커서는 그 검색어 · 범위로만 쓴다(다르면 400 `VAL_001`). 발송 중인 내부 초안은 지금처럼 빠진다.
|
||||
@@ -676,6 +679,172 @@ interface MailState {
|
||||
범위의 기본값은 검색을 시작한 메일함이고 결과 머리에서 "그 메일함 | 전체 메일함"으로 바꾼다(휴지통 · 스팸메일함에서 시작하면 그 안에서 찾는다).
|
||||
✕ · Esc · "검색 닫기"는 검색 전 메일함으로 돌아간다. 임시보관함의 결과를 누르면 지금처럼 쓰기 화면에서 이어 쓴다.
|
||||
|
||||
### 검색 색인 (`GET /api/search-index`) — 14단계 (색인 바탕 — 검색은 아래 "검색 — 14단계", 대화는 "대화 — 14단계")
|
||||
|
||||
14단계는 엔진 검색 대신 **서버의 자체 색인**(지금 DB — 디자인 README 「14단계 — 검색 · 대화 묶기」)으로 찾는다. 이 절은 그 바탕 — 색인을 만들고 맞추는 규칙과 준비 상태다.
|
||||
|
||||
- **색인에 드는 것**: 직원 주소 + 엔진 메일 id 마다 한 줄 — 메일함 · 키워드 · 받은 시각 · 답장 관계(Message-ID · In-Reply-To · References) · 제목 · 보낸 사람 · 받는 사람 · 참조 · 첨부 이름 · 본문 글(앞 2만 자 — 잰 뒤 정한다).
|
||||
메일 글이 DB 와 매일 백업에 한 벌 더 들어간다(사용자가 받아들였다). 퇴사 마무리는 이 표도 지운다. 언제나 로그인한 직원 주소로만 읽고 쓴다.
|
||||
- **만드는 때**: 직원이 웹메일에 처음 로그인할 때(비밀번호가 만료된 로그인은 빼고 — 새 비밀번호를 정하면 만든다) 또는 아래 경로를 처음 물을 때 그 직원의 상태 줄을 `building` 으로 만든다. 로그인한 적 없는 계정은 색인하지 않는다.
|
||||
- **지난 메일 채우기**: 밤에만 한 사람씩(KST 22:00 ~ 02:45 · 05:00 ~ 07:30 — 03:00 · 04:00 · 04:30 엔진 정리 · 03:30 백업을 피한다). 다 채우면 `ready`.
|
||||
- **새로 온 · 바뀐 · 지운 메일**은 엔진의 변경(Email/changes)으로 바로 맞춘다 — 실시간 연결이 알릴 때 · 5분마다 · 검색 바로 전. 엔진이 변경을 셀 수 없다고 하면 그 직원 전체를 대조한다.
|
||||
비밀번호가 만료됐거나 잠긴 계정은 대리 접근이 막혀 건너뛰고, 다음 로그인 · 아래 경로를 물을 때 다시 한다.
|
||||
- `GET /api/search-index` → `{ status, done, total }`
|
||||
- `status`: `building`(지난 메일을 채우는 중 — 그동안 그 직원의 검색 · 대화는 지금 방식(엔진)이고, 화면은 검색 결과 머리 아래에 "검색 준비 중" 줄을 보인다) · `ready`.
|
||||
- `done`: 채운 메일 수(`total` 을 넘지 않는다) · `total`: 채우기를 시작할 때의 메일 수(시작 전이면 `null`, `ready` 면 `done` 과 같다).
|
||||
- **조회지만 상태 줄을 만들고 멈춤을 푼다** — 줄이 없던 직원은 이때 `building` 으로 만들고, 대리 접근이 막혀 멈췄던 줄은 다시 해 보게 한다(로그인한 직원이 물었으니 다시 될 것이다). 같은 직원이 여러 번 물어도 결과는 같다(멱등).
|
||||
- 로그인 필요(401 `AUTH_001`) · 제한된 세션은 403 `AUTH_010`(6절).
|
||||
- 로그 가림: 검색 조건의 이름 `from` · `to` · `subject` · `body` 도 `q` · `cursor` 처럼 길이만 남긴다(`maskUrlForLog`). 개발 환경도 DB 쿼리 로그(매개변수)를 남기지 않는다 — 메일 글이 DB 에 있다.
|
||||
|
||||
### 검색 — 14단계 (자체 색인 · 조건 검색 · 강조)
|
||||
|
||||
- **방식**: 그 직원의 색인이 `ready` 면 색인으로, 아니면(`building`) 엔진으로 찾는다(위 3단계 절의 규칙 · 한계 그대로 — 조건은 엔진 필터로, 낱말이 통째로 같을 때만).
|
||||
방식은 첫 쪽에서 정해지고 커서가 그 방식을 잇는다 — 쪽을 넘기는 중에 준비가 끝나도 그 검색은 처음 방식대로다. 색인 방식은 찾기 바로 전에 그 직원의 색인을 한 번 맞춘다(1.5초 안 — 방금 온 메일도 찾아지게).
|
||||
- **쿼리** (`GET /api/messages` — 3단계의 `mailboxId` · `q` · `cursor` · `limit` 에 더한다)
|
||||
|
||||
| 이름 | 찾는 곳 | 검사 |
|
||||
| --- | --- | --- |
|
||||
| `q` | 보낸 사람 · 받는 사람 · 참조 · 제목 · 본문 · 첨부 이름 | 3단계와 같다 |
|
||||
| `from` | 보낸 사람(이름 · 주소) | 넷 모두 `q` 와 같은 검사 — 앞뒤 공백을 지우고 공백뿐이면 없는 것, 1~200자(UTF-16), 낱말 8개, 길이를 먼저 본다 |
|
||||
| `to` | 받는 사람과 참조 | |
|
||||
| `subject` | 제목 | |
|
||||
| `body` | 본문 | |
|
||||
| `after` · `before` | 받은 시각이 `after` 이상 · `before` 미만 | ISO 8601 UTC(`Z` · 소수 초 3자리까지 · 있는 날짜) |
|
||||
| `hasAttachment` | 진짜 첨부가 있는 메일만(목록의 `hasAttachment` 와 같은 판정) | `true` 만 받는다 |
|
||||
|
||||
`q` 나 조건 하나라도 있으면 검색이다(검색 칸이 비어도 조건만으로). 메일함 범위는 3단계와 같고(`mailboxId` 가 없으면 휴지통 · 스팸메일함을 뺀 모두), `mailboxId` 도 검색도 없으면 400 `VAL_001`.
|
||||
**`q` 와 조건 칸 넷(`from` · `to` · `subject` · `body`)을 합친 낱말은 8개까지**다(넘으면 400 `VAL_001` — 색인을 채우는 동안의 엔진 방식이 낱말마다 조사 형태를 붙여 묻기 때문에, 3단계에서 잰 비용(낱말 8개 · 조건 472개 · 0.16초) 안에 두려고. 화면은 보내기 전에 같은 수를 본다).
|
||||
검색 글의 NUL(U+0000)은 색인과 같이 U+FFFD 로 바꿔 찾는다.
|
||||
화면은 고른 날짜를 브라우저 시간대의 시작일 0시 · 끝나는 날 다음 날 0시로 바꿔 보낸다(부재중과 같은 방식).
|
||||
- **찾는 규칙 (색인 방식)**
|
||||
- 부분 일치 — 낱말이 그 칸의 글 어디에 있어도 맞는다('계약' → 계약서 · 계약은 · 계약직, 제목 맨 앞의 [공지] · Re: 도). 영문 대소문자 · 전각 영숫자는 가리지 않는다(NFC).
|
||||
- 여러 낱말은 모두 맞아야 하고 큰따옴표는 구절이다(3단계와 같다). 구절은 줄바꿈 · NBSP 로 갈린 글에도 맞는다 — 띄어쓰기 두 칸에는 맞지 않는다.
|
||||
- **한 글자 낱말**: 두 글자 넘는 낱말과 함께면 모두 찾는다. 한 글자 낱말뿐이면 제목 · 사람(보낸 사람 · 받는 사람 · 참조) · 첨부 이름에서만 찾고 본문은 찾지 않는다(2026-11-05 사용자 확정 — 본문을 다 훑으면 메일이 많은 직원은 수 초. 화면은 결과 머리에 한 줄 알린다).
|
||||
- **본문 강조**(목록 미리 보기 · 읽기 화면)는 본문을 찾은 두 글자 이상 낱말만이다. 한 글자 낱말뿐이면 본문에 강조하지 않고(제목 · 첨부 이름은 그대로), 두 글자 이상 낱말과 섞이면 그 낱말만 강조한다 —
|
||||
함께 찾은 한 글자 낱말은 본문에서 맞았어도 강조하지 않는다(본문 곳곳에 흩어진다 — 2026-11-06 실사용 시험). 두 글자 이상 낱말이 보낸 사람 · 받는 사람 조건에만 있어도 섞인 검색이다.
|
||||
- 본문은 앞 2만 자까지 찾는다. 첨부 파일 안의 글 · 끝이 바뀐 말('보내' → '보냈습니다')은 찾지 않는다(첨부 이름은 찾는다). 받는 사람 칸은 주소들을 이어 보므로 구절이 이웃한 두 주소에 걸쳐 맞을 수 있다(작은 한계).
|
||||
- **응답 (목록)**
|
||||
- 항목(MessageSummary)에 `searchHighlight: { subject: [start, end][], preview: { text, ranges: [start, end][] } } | null` — 색인 방식 검색에서만 차고, 메일함 목록 · 엔진 방식은 `null`.
|
||||
위치는 NFC 글의 UTF-16 [시작, 끝)이고, 강조가 있으면 `subject` 도 NFC 로 싣는다. `preview.text` 는 본문에서 맞았으면 맞은 말 둘레 한 줄(잘린 쪽에 "…"), 아니면 엔진 미리 보기다.
|
||||
본문의 맞은 곳은 이 차례로 고른다(2026-11-06 실사용 시험 — 답장은 처음 맞은 곳이 거의 늘 원문 인용 안이었다).
|
||||
① 인용 줄(앞 공백 뒤 `>`) 밖의 첫 맞은 곳 — 둘레는 인용 줄 전까지.
|
||||
② 인용 안에서만 맞았고, 본문에서 맞은 말이 모두 검색어(`q`)의 낱말로 제목 · 보낸 사람 · 받는 사람과 참조 · 첨부 이름에서도 맞았으면 인용 밖의 첫 글 — 맞은 곳이 없으면 `ranges` 는 빈 배열, 인용 밖 글이 없으면 ③.
|
||||
③ 그 밖에는 줄 앞 `>` 표시(겹친 것 · 사이 공백)를 뺀 인용 조각을 그 인용 안에서만 — `ranges` 는 보이는 글 기준.
|
||||
내용 조건(`body`)의 낱말은 ②에 들지 않는다. `>` 없는 인용 꼴("-----Original Message-----" 등)과 인용 머리 줄("…님이 작성:")은 알아보지 않는다(알려진 한계).
|
||||
- **`hasNext: true` 인데 항목이 `limit` 보다 적거나 0개일 수 있다**(색인 방식) — 한 요청이 확인하는 메일 수에 상한이 있어(3,000 — 1초 안에 답하려고) 그때까지 찾은 것과 이어 찾을 커서를 준다. 화면은 커서가 있으면 이어 묻는다(빈 쪽이 이어지면 몇 번까지만 — 화면 상수).
|
||||
- `total`: 첫 쪽에서 셀 수 있으면 0~1,000, 1,000 을 넘거나 이 요청에서 다 세지 못했으면 `null`, 다음 쪽은 늘 `null`(엔진 방식은 3단계와 같다).
|
||||
**화면은 `null` 이면 통 수를 보이지 않는다** — "1,000통 넘게"라고 쓰지 않는다(다 세지 못한 것일 수도 있다).
|
||||
- 커서는 불투명한 문자열이다. 방식 · 메일함 · `q` · 모든 조건이 지문에 들어가 다른 검색에 쓰면 400 `VAL_001`. 3단계의 옛 커서는 그대로 통한다.
|
||||
- **읽기 화면** (`GET /api/messages/{id}`)
|
||||
- `images` 에 더해 `q` · `from` · `to` · `subject` · `body` 를 받는다(검사는 목록과 같다 — `from` · `to` 는 받기만 하고 강조하지 않는다). `after` · `before` · `hasAttachment` · `cursor` · `limit` 은 받지 않는다(400 `VAL_001`) — 화면은 검색 주소의 조건 가운데 이 다섯만 넘긴다.
|
||||
- 강조할 말이 있으면 `searchHighlight: { subject: Range[], attachments: { [첨부 id]: Range[] }(맞은 첨부만), textBody: Range[] }` 를 싣고, `subject` · 첨부 이름 · `textBody` 를 NFC 로 싣는다. 없으면 `null`.
|
||||
- `htmlBody` 는 **정화한 뒤** 맞은 글 조각에 속성 없는 `<mark>` 를 넣는다 — 태그를 건너는 말 · 속성 · 주석 · `style` 안은 건드리지 않고, 한 통에 1,000개까지.
|
||||
- 강조할 말: `q` 는 제목 · 본문 · 첨부 이름에, `subject` 는 제목에, `body` 는 본문에. `from` · `to` 는 강조하지 않는다(섞인 검색인지 정하는 데만 쓴다). 본문에는 목록과 같이 본문을 찾은 두 글자 이상 낱말만이다. 화면은 `<mark>` 를 노랑 `#FCE7A4` · 글자색 그대로로 그린다(디자인 README 14단계).
|
||||
- **강조가 든 응답은 보이기만 한다** — 답장 · 전달의 원문 인용 · 인쇄 · 다른 화면의 메일 기억(목록 · 스토어)에는 검색 쿼리 없이 받은 응답을 쓴다(보내는 정화는 `mark` 를 지우지만, 인용 · 인쇄 모양이 검색에 따라 달라지지 않게).
|
||||
- 한 글자 낱말뿐이라 본문을 찾지 않았다는 표시는 응답에 없다 — 화면은 서버와 같은 낱말 나누기(짝 — backend `shared/utils/search-text.ts` · 표본 표 `backend/test/search-text-samples.ts`)로 알아보고 결과 머리에 한 줄 알린다.
|
||||
- 보낸 사람이 본문에 직접 쓴 `<mark>`(받은 메일 정화가 남긴다)도 같은 노랑으로 보여 검색 강조와 가르지 않는다(드물고 브라우저 기본도 노랑이다 — 알려진 한계).
|
||||
- **클라이언트 — 화면 주소**(14단계 웹, 3단계 주소에 더한다): 조건도 검색어처럼 화면 주소에 싣는다(앞단 nginx 의 접근 로그는 쿼리를 남기지 않는다).
|
||||
- 꼴: `/mail/search?q=…&in=<메일함 id | all>&from=…&to=…&subject=…&body=…&since=YYYY-MM-DD&until=YYYY-MM-DD&attachment=1`, 결과에서 연 메일은 `/mail/search/<메일 id>?…`(같은 쿼리).
|
||||
빈 칸은 싣지 않는다(검색어가 비면 `q` 도 뺀다 — 조건만으로 찾는다). `in` 이 비면 `all`.
|
||||
- `q` · `from` · `to` · `subject` · `body`: 앞뒤 공백을 지우고 200자에서 자른다. 같은 이름이 여럿이면 첫 값, 모르는 이름은 무시한다.
|
||||
- `since` · `until`: 브라우저 시간대의 날짜이고 **양 끝을 포함한다** — API 에는 `after` = `since` 날 0시, `before` = `until` 다음 날 0시(UTC ISO)로 보낸다. "최근 1주" 같은 기간은 고른 순간 날짜로 바꿔 싣는다(7일 전 · 달은 말일을 넘지 않는다).
|
||||
있는 날짜이고 1970-01-01 ~ 9999-12-30 이어야 한다(아니면 그 날짜만 버린다). `since` 가 `until` 보다 늦으면 기간을 통째로 버린다. 한쪽만 있어도 된다.
|
||||
- `attachment`: `1` 일 때만 `hasAttachment=true` 다(다른 값은 무시).
|
||||
- 검색어와 조건 칸 넷을 합친 낱말이 8개를 넘으면 목록을 묻지 않고 "찾을 말이 너무 많습니다"를 보인다(손으로 고친 주소 — 화면의 [찾기]는 보내기 전에 막는다). 그때 연 메일은 강조 쿼리 없이 받는다.
|
||||
- 읽기 화면 API 에는 `q` · `from` · `to` · `subject` · `body` 다섯만 넘긴다([이미지 보기]도 같은 쿼리로).
|
||||
- **클라이언트 — 이어 찾기**: `hasNext` 인데 쪽이 모자라면 한 번에(열 때 · [메일 더 보기] · [더 찾기] · 목록 보기의 [다음]) 요청을 5번까지 보낸다(화면 상수 `MAIL_SEARCH_FILL_MAX_REQUESTS`) — 새로 20통이 차거나 `hasNext` 가 false 면 멈춘다.
|
||||
그래도 못 찾았으면 [더 찾기] 화면이다. 다음 쪽의 `total` 이 `null` 이면 첫 쪽의 수를 남기고, 첫 쪽부터 `null` 이면 통 수 · "n / 전체"를 보이지 않는다.
|
||||
목록을 새로 고치는 동안은 이어 받지 않는다(옛 커서로 붙지 않게).
|
||||
- **클라이언트 — 준비 상태**: `GET /api/search-index` 를 받지 못하면(실패) "검색 준비 중" · 옛 한계 안내 · 한 글자 안내를 모두 보이지 않는다. 한 글자 안내는 `ready` 이고 본문을 찾을 말(검색어나 내용 조건)이 있을 때만이다.
|
||||
- 새 에러 코드는 없다(`VAL_001` · `RES_001` · `MAIL_001`).
|
||||
|
||||
### 대화 — 14단계 (같은 대화 · 대화로 묶어 보기)
|
||||
|
||||
읽기 화면은 그 메일의 같은 대화를 늘 보이고, 목록은 사용자가 켜면 대화로 묶는다(디자인 README 「14단계 — 검색 · 대화 묶기」의 `Thread14` · `ListThreads14`). 검색 결과는 묶지 않는다.
|
||||
|
||||
- **대화의 기준(`basis`)** — 응답마다 싣는다.
|
||||
- `index`(그 직원의 색인이 `ready`): 답장 관계(Message-ID · In-Reply-To · References)로 이어지고 **말머리를 뗀 제목이 같은** 메일이 한 대화다.
|
||||
말머리는 앞의 `Re` · `RE` · `Fw` · `FW` · `Fwd` · `AW` · `WG` · `회신` · `답장` · `전달`(뒤의 `[2]` 같은 수 · 전각 쌍점 `:` 도) + 쌍점과 대괄호 꼬리표(`[공지]` 등)이고 여러 겹을 뗀다(짝 — backend `shared/utils/subject-base.ts` · frontend `shared/utils/search-match.ts`).
|
||||
Message-ID 가 없는 메일은 혼자다. 원본이 늦게 와도 이어지고, 한 번 이어진 대화는 나뉘지 않는다(가운데 메일을 지워도). 답장 관계가 있어도 제목이 다르면 묶지 않는다(엔진과 같다).
|
||||
- `engine`(색인을 채우는 동안): 엔진의 스레드(`threadId`). 두 기준의 묶음은 조금 다를 수 있다 — 준비가 끝나면 묶음이 바뀔 수 있다.
|
||||
- 기준은 첫 쪽에서 정해지고 커서가 잇는다(검색과 같다). 색인 방식은 읽기 바로 전에 그 직원의 색인을 한 번 맞춘다(1.5초 안). 첫 쪽에서 색인을 읽지 못하면(DB 장애) 엔진으로 답한다.
|
||||
- **대화에 드는 메일**: 모든 메일함(보낸편지함 · 임시보관함 포함)의 메일 — 휴지통 · 스팸메일함에만 든 메일은 뺀다. 보내는 중인 내부 초안은 빼고, 붙잡힌 메일(보내기 취소 · 예약 — `pendingSend`)은 넣는다.
|
||||
|
||||
#### `GET /api/messages/{id}/conversation` — 같은 대화
|
||||
|
||||
- 쿼리 `cursor` · `limit`(1~100, 처음 100). 응답 `{ items: MessageSummary[], pageInfo, basis: 'index' | 'engine' }`.
|
||||
- **오래된 차례**(받은 시각 — 같으면 id). 그 메일은 휴지통 · 스팸메일함에 있어도 늘 든다. 한 통뿐이면 그 메일 하나다(화면은 칸을 보이지 않는다).
|
||||
- `total`: `index` 는 첫 쪽에서만(다음 쪽 `null`), `engine` 은 쪽마다(스레드가 1,000통을 넘으면 `null`).
|
||||
- **`hasNext: true` 인데 항목이 `limit` 보다 적거나 0개일 수 있다**(`engine` — 한 요청이 커서 자리부터 1,000통까지만 훑는다. 검색과 같은 결 — 응답은 늘 커서 자리보다 앞으로 간다).
|
||||
화면은 커서가 있으면 이어 묻되, 빈 쪽이 이어지면 몇 번까지만 묻고 [더 보기]로 넘긴다(검색의 이어 찾기와 같은 결 — 휴지통에만 든 메일이 많은 큰 스레드).
|
||||
- 커서는 그 메일 · 그 기준에서만 통한다 — 다른 메일 · 꾸민 커서는 400 `VAL_001`(그 메일이 색인에 없어도 커서를 먼저 본다).
|
||||
- 404 `RES_001`: 없는 메일 · 쪽을 넘기는 중에 색인에서 사라진 메일.
|
||||
503 `MAIL_001`: 엔진 장애 · `engine` 기준이 한 요청의 시간(15초) 안에 1,000통 이하 스레드를 다 읽지 못하거나 더 큰 스레드의 첫 묶음도 읽지 못함.
|
||||
- **클라이언트 — 읽기 화면의 같은 대화 칸**(14단계 웹 — 디자인 README "14단계 구현하며 정한 보완 — 같은 대화")
|
||||
- 한 번에 100통씩 받고, 쪽이 모자라면 한 번에 요청을 5번까지 이어 묻는다(화면 상수 `MAIL_CONVERSATION_FILL_MAX_REQUESTS` — 새로 100통이 차거나 `hasNext` 가 false 면 멈춘다). 남으면 [더 보기]. 화면의 시간 한도는 30초다.
|
||||
- 칸 머리의 통 수: `total` → (다음 쪽이 없으면) 받은 수 → 모르면 수 없이 "같은 대화"(짐작한 수는 보이지 않는다).
|
||||
- 처음 받기 · 다시 받기 · 펼친 본문은 알림 없이(`silent`) 칸 · 줄 안에 안내와 [다시 시도]를 둔다. 404 · 모양이 틀린 응답이면 칸이 없다(이 경로가 없는 옛 서버). [더 보기]만 인터셉터가 알린다.
|
||||
- 다시 받는 때: 메일함들의 통 수(`totalCount`)가 바뀌었을 때(새 메일 · 지움 · 옮김 — 읽음 · 중요 표시만 바뀐 소식으로는 받지 않는다), 쓰기 화면에서 돌아올 때, 붙잡힌 메일이 보낼 시각 + 2초가 됐을 때.
|
||||
받기가 끝난 때부터 10초 안에 온 소식은 모아 한 번 받고, 받는 중에 온 소식은 끝난 뒤 한 번 더 받는다. [더 보기]로 늘린 대화는 저절로 다시 받지 않는다(붙잡힌 메일만 `GET /api/messages/{id}` 로 고친다).
|
||||
알려진 한계: 같은 메일함에서 한 통이 들어오며 한 통이 빠지면 통 수가 같아 그 소식으로는 다시 받지 않는다.
|
||||
- 펼친 줄은 검색 쿼리 없이 `GET /api/messages/{id}` 로 받고, 받은 뒤에도 펼쳐 있으며 안 읽은 메일이면 `PATCH` 로 읽음으로 바꾼다(읽기 화면과 같은 길). 그 메일이 그사이 지워졌으면(404) 대화를 다시 받아 그 줄을 뺀다.
|
||||
- 줄의 읽음(2026-11-07): 안 읽은 줄을 목록처럼 굵게 그린다. 이 탭이 바꾼 읽음은 줄에 바로 적고, 받는 동안 바꾼 것 · 받기를 시작할 때 보내던 것은 받은 값보다 앞세운다(받은 값은 그 바꿈이 엔진에 닿기 전일 수 있다). 다른 탭 · 기기의 읽음은 다음 받기 때 보인다(읽음만 바뀐 소식으로는 다시 받지 않는다).
|
||||
- 줄 차례: 받은 메일은 응답 차례 그대로, 붙잡힌 메일(`pendingSend` — 보내기 취소 · 예약)은 받은 줄 뒤에 `sendAt` 차례로 둔다. 줄의 미리 보기는 우리 원문 인용 머리 "> 원본 메일"부터 잘라 내고(남는 글이 없으면 제목), 펼친 줄은 첨부 목록 대신 첨부 수만 안내한다(2026-11-07).
|
||||
|
||||
#### `GET /api/conversations` — 대화로 묶어 보기
|
||||
|
||||
```ts
|
||||
interface ConversationSummary {
|
||||
id: string // 대화 id — 불투명(기준마다 다르다)
|
||||
latest: MessageSummary // 그 메일함 안의 마지막 메일 — 줄의 제목 · 시각 · pendingSend
|
||||
participants: Address[] // 대화 전체의 보낸 사람(주소가 있는 사람만) — 받은 차례, 주소마다 한 번, 10명까지 · 이름 100자까지
|
||||
messageCount: number // 대화 전체의 메일 수 — 위 "대화에 드는 메일"에 그 메일함의 메일을 더한 수(휴지통 · 스팸메일함을 묶어 볼 때)
|
||||
messageIds: string[] // 그 메일함 안의 그 대화 메일 — 최신부터 500통까지
|
||||
pendingSendIds: string[] // messageIds 가운데 붙잡힌 메일(보내기 취소 · 예약 — pendingSend 가 있는 메일). 없으면 []
|
||||
isRead: boolean // 그 메일함 안의 메일을 모두 읽었으면 true
|
||||
isFlagged: boolean // 그 메일함 안에 중요 표시한 메일이 하나라도 있으면 true
|
||||
hasAttachment: boolean // 그 메일함 안에 첨부가 있는 메일이 하나라도 있으면 true
|
||||
}
|
||||
```
|
||||
|
||||
- 쿼리 `mailboxId`(필수) · `cursor` · `limit`(1~100, 처음 20). 검색 쿼리(`q` · 조건)는 받지 않는다(400 `VAL_001`).
|
||||
- 응답 `{ items: ConversationSummary[], pageInfo, basis }` — 그 메일함 안의 마지막 받은 시각 최신부터.
|
||||
- `total`: `index` 는 첫 쪽에서 그 메일함의 대화 수(다음 쪽 `null`), `engine` 은 엔진이 센 스레드 수(쪽마다).
|
||||
- 커서는 그 메일함 · 그 기준에서만 통한다(다른 메일함 · 다른 목록 · 꾸민 커서는 400 `VAL_001`). 없는 메일함은 404 `RES_001`, 엔진 장애(메일함을 읽지 못함 포함)는 503 `MAIL_001`.
|
||||
- **`engine` 기준의 상한**: 한 요청이 스레드마다 최신 500통 · 모두 2,000통까지 · 15초 안에서 읽는다(몫은 작은 스레드부터 다 주고 남는 것을 큰 스레드끼리 나눈다).
|
||||
시간이 끝나도 503 이 아니라 그 쪽의 줄을 읽은 만큼으로 채워 답한다(커서는 보통의 다음 쪽 커서다).
|
||||
상한에 걸린 줄(스레드마다 500 · 한 요청 2,000 · 15초 — 500통 이하 스레드도 한 쪽에 큰 스레드가 많으면 걸린다)은 `messageCount` 가 어림이다(읽지 못한 오래된 메일을 모두 센다 — 휴지통 · 스팸메일함에만 든 것도).
|
||||
`participants` · `messageIds` · `isRead` · `isFlagged` · `hasAttachment` · `pendingSendIds` 는 읽은 최신 메일로만 정한다.
|
||||
- 고르기 · 읽음 · 중요 · 이동 · 삭제는 그 줄의 `messageIds` 를 지금의 여러 통 API(`PATCH /api/messages` · `POST /api/message-deletions`)로 보낸다.
|
||||
**`pendingSendIds` 는 빼고 보낸다** — 붙잡힌 메일은 취소로만 보낸편지함을 떠난다(9단계 "보내기 취소 · 예약 발송" — 섞여 와도 서버가 `failedIds` · `skippedIds` 로 막는다).
|
||||
줄의 메일이 모두 붙잡힌 메일이면 그 줄은 고를 수 없다(목록의 고르기 칸과 같은 결).
|
||||
- 500통이 넘는 대화는 `messageIds` 가 최신 500통이다 — 그 줄에 한 일은 그만큼에만 하고, 남은 메일은 다음에 다시 맞춘 목록의 같은 줄로 남는다(드문 경우).
|
||||
- **알려진 한계**: 쪽을 넘기는 사이 늦게 온 원본으로 두 대화가 합쳐지면 줄이 겹치거나 빠질 수 있다 — 처음부터 다시 받으면 맞는다.
|
||||
마지막 메일이 그 메일함을 떠났는데 색인이 아직 맞추지 않았으면 그 줄이 한 번 빠진다.
|
||||
- **클라이언트 — 목록을 대화로 묶어 보기**(14단계 웹 — 디자인 README "14단계 구현하며 정한 보완 — 목록 대화로 묶어 보기")
|
||||
- 켜고 끄기: 목록 머리 [대화로 묶기]와 설정 › 메일 › 보내기 · 읽기의 한 줄이 같은 `groupByConversation` 을 `PATCH` 로 저장한다 — 화면을 먼저 바꾸고 끝내 실패하면 되돌린다(칸마다 저장은 한 번에 하나 — 아래 "보내기 · 읽기 설정").
|
||||
메일함 목록만 묶는다 — 검색 결과 · 「예약됨」은 묶지 않는다.
|
||||
- 첫 진입: 설정을 받기 전 · 받지 못했으면 이 브라우저가 그 직원에게서 마지막으로 안 값(localStorage — 직원마다, 처음이거나 읽지 못하면 끔)으로 연다. 받은 값이 다르면 바로 그 값으로 다시 연다
|
||||
(짐작이 틀리면 버릴 첫 쪽 요청이 하나 나간다). 받지 못한 동안에도 [대화로 묶기]를 누를 수 있고, 저장 응답(설정 전체)을 받은 설정으로 삼는다 — 실패하면 누르기 전 짐작으로 돌아간다.
|
||||
로그아웃 · 세션 만료는 짐작을 바꾸지 않고, 세션이 끝나면(제한된 세션 포함) 목록을 다시 부르지 않는다.
|
||||
- 쪽은 20줄 · 받은 커서 그대로. 켜고 끔 · 메일함 바꿈 · 다른 목록으로 가면 앞 목록의 응답을 버리고 그 요청을 끊는다(`AbortSignal` — `engine` 기준은 한 요청이 15초까지). 끊긴 요청은 알리지 않는다.
|
||||
- 새 메일 확인 · 실시간: 메일 목록 대신 불러온 줄 수만큼(최대 100줄) `silent` 로 처음 쪽부터 다시 받는다 — 요청 수는 메일 목록과 같다.
|
||||
- 다른 탭 · PC 에서 바꾼 설정: 메일 화면은 탭이 다시 보일 때 설정을 마지막으로 받은 지 60초가 지났으면 `GET /api/mail-settings` 를 `silent` 로 다시 받는다 — 저장하는 중이면 받지 않고, 받는 사이 고른 값이 있으면 받은 값을 버리고, 세션이 끝났으면 받지 않는다. 받은 `groupByConversation` 이 다르면 그 값으로 목록을 다시 연다(2026-11-07).
|
||||
- 줄에 하는 일: 할 일 메일 = `messageIds` − `pendingSendIds` − (마지막 메일이 붙잡힌 메일이면) 그 메일. 100통씩 나눠 보낸다.
|
||||
- 화면을 먼저 바꾸는 것은 줄의 메일을 모두 바꿨을 때만 줄 전체(읽음 · 중요 해제)다 — 일부만이면 그대로 두고 다음 맞추기가 서버 값으로 고친다. 중요 해제는 붙잡힌 메일까지 바꿨을 때만이다(붙잡힌 메일의 중요 표시는 남는다).
|
||||
- 줄의 별: 중요 표시가 하나라도 있으면 모두 풀고, 없으면 할 일 메일을 모두 표시한다. 붙잡힌 메일이 든 줄을 풀면 줄을 미리 풀지 않고 묶은 목록을 한 번 다시 받는다 — 그래도 중요 표시가 남으면 알린다.
|
||||
- 읽기 화면의 별(한 통)이 실패하면 그 메일이 든 줄을 앞 값으로 되돌린다.
|
||||
- 옮기기 · 삭제 뒤에는 묶은 목록을 한 번 다시 받는다(남은 줄의 마지막 메일 · 통 수를 맞추려고). 서버가 바꾸지 못한 메일(`failedIds` · `skippedIds`)이 든 줄은 앞 값으로 되돌린 뒤 다시 받는다. 안 읽은 수는 끝난 뒤 메일함 목록으로 맞춘다.
|
||||
- 목록 머리의 대화 수는 `total` 이다 — `null` 이면 그 부분을 뺀다.
|
||||
- 알려진 한계: 여러 통인 줄의 마지막 메일만 읽으면 다음 맞추기(실시간이면 곧, 아니면 1분)까지 그 줄이 굵은 채다 — 읽을 때마다 다시 받지 않는다(경로마다 1분 100회).
|
||||
마지막 메일이 아닌 붙잡힌 메일은 보낼 시각을 몰라, 풀린 뒤에도 다음 맞추기까지 그 메일만 든 줄을 고를 수 없다.
|
||||
|
||||
#### 설정
|
||||
|
||||
- `groupByConversation`(아래 "보내기 · 읽기 설정") — 처음 `false`. 서버는 저장해 돌려주기만 하고, 묶은 목록을 부를지는 화면이 정한다.
|
||||
|
||||
새 에러 코드는 없다(`VAL_001` · `RES_001` · `MAIL_001`).
|
||||
|
||||
### 여러 통 한꺼번에 (`PATCH /api/messages` · `POST /api/message-deletions`) — 3단계
|
||||
|
||||
```ts
|
||||
@@ -1134,7 +1303,7 @@ interface PendingSend {
|
||||
- **로그**: 취소 · 예약은 log(메일 id · 보낼 시각만) — 받는 사람 · 제목은 남기지 않는다.
|
||||
- **클라이언트**: [보내기]를 누르면 지금처럼 쓰기 화면을 닫고 떠난다. 알림 "메일을 보냈습니다." + [보내기 취소] + 남은 시간 막대 — `pendingSend.sendAt` 에 닫는다(마우스를 올려도 멈추지 않는다).
|
||||
일부 거절의 실패 알림에도 [보내기 취소]를 둔다. [보내기 취소] → `DELETE` → 쓰기 화면을 `?draft=<draftId>` 로 다시 연다(새 키) + 알림 "보내기를 취소했습니다. 쓰던 메일로 돌아왔습니다.".
|
||||
`MAIL_014` 는 인터셉터 알림. 예약은 알림 "‘시각’ 에 보냅니다." + [예약됨 보기]. 세션이 바뀌면 알림을 닫는다 — 메일은 그대로 보내진다.
|
||||
`MAIL_014` 는 인터셉터 알림. 예약은 알림 "‘시각’에 보냅니다." + [예약됨 보기]. 세션이 바뀌면 알림을 닫는다 — 메일은 그대로 보내진다.
|
||||
|
||||
### 외부 이미지 보기 (`images=show` · `/api/mail-images`) — 9단계
|
||||
|
||||
@@ -1222,19 +1391,23 @@ interface MailBlockList {
|
||||
interface MailSettings {
|
||||
undoSendSeconds: 0 | 5 | 10 | 20 | 30 // 보내기 취소 — 0 이면 바로 보낸다. 처음 10
|
||||
externalImages: 'ask' | 'trusted' | 'always' // 외부 이미지 — 늘 묻기 · 주소록과 사내 직원은 늘 보기 · 늘 보기. 처음 'ask'
|
||||
groupByConversation: boolean // 목록을 대화로 묶어 보기 — 처음 false (14단계 — 위 "대화 — 14단계")
|
||||
}
|
||||
```
|
||||
|
||||
- `groupByConversation` 은 서버가 저장해 돌려주기만 한다 — 묶은 목록(`GET /api/conversations`)을 부를지는 화면이 정한다. `PATCH` 는 boolean 만 받는다(`null` · `"true"` · `1` 은 400 `VAL_001`).
|
||||
|
||||
- 서버는 발송 때 `undoSendSeconds`, 메일을 읽을 때 `externalImages` 를 본다(직원마다 잠깐 기억해도 된다 — 그 서버에서 바꾸면 바로 맞춘다).
|
||||
서버마다 60초 기억하고, 읽지 못하면(DB 장애) 처음 값(10초 · `ask`)으로 보고 warn 을 남긴다(구현하며 정함).
|
||||
- 화면(설정 › 메일 › 보내기 · 읽기)은 고르는 대로 `PATCH` 하고 "저장했습니다"를 보인다(저장 단추 없음).
|
||||
칸마다 저장은 한 번에 하나다 — 끝났을 때 마지막으로 고른 값이 서버의 답과 다르면 그 값을 다시 보낸다(늦게 온 앞 응답 때문에 화면과 서버가 갈리지 않게 — 2026-10-29 리뷰).
|
||||
메일 화면은 탭이 다시 보일 때 마지막으로 받은 지 60초가 지났으면 조용히 다시 받는다(14단계 — 위 "대화 — 14단계"의 묶어 보기 클라이언트 줄).
|
||||
"저장했습니다"는 기다리던 저장이 모두 끝나고 마지막이 성공했을 때 보인다. 끝내 실패하면 서버가 마지막으로 확인해 준 값으로 되돌리고 인터셉터가 알린다.
|
||||
|
||||
### 이 단계에서 하지 않는 것
|
||||
|
||||
탭을 모두 닫아도 오는 알림(웹 푸시 — 엔진은 `webpush-vapid` 를 지원한다), 알림 소리(운영체제 설정을 따른다), 휴대폰 푸시, 본문 안 이미지(인라인 cid — 붙여 넣은 이미지는 첨부로, 서명 그림만 예외), 메일 안의 배경 그림 보이기 · 보낸 사람마다 고르는 "이미지 늘 보기" 목록(9단계는 설정 세 가지로),
|
||||
여러 개의 서명, 자체 검색 색인(붙여 쓴 말 · 앞부분 · 제목 말머리로 찾기), 조건 검색(보낸 사람 · 기간 · 첨부 있음), 검색어 강조,
|
||||
여러 개의 서명(자체 검색 색인 · 조건 검색 · 검색어 강조는 14단계에서 했다 — 위 "검색 — 14단계"),
|
||||
끌어다 놓아 옮기기, 메일함 옮기기(다른 상위로) · 하위 메일함까지 한꺼번에 지우기, 앱 비밀번호 · 2단계 인증(엔진은 지원한다), 부재중 응답의 서식 본문 · 사내/외부 구분,
|
||||
예약한 메일의 「지금 보내기」 · 7일 넘는 예약(엔진 한도) · 차단할 때 이미 받은 그 보낸 사람의 메일 모두 옮기기(9단계는 그 메일만).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user