docs: 계약 2절에 14단계 검색(색인 · 조건 · 강조) 규칙을 적는다

쿼리 일곱 · 합친 낱말 8개 · 찾는 규칙(부분 일치 · 구절 · 한 글자) · 목록과 읽기 화면의
searchHighlight · hasNext 인데 빈 쪽 · total null 이면 화면이 통 수를 숨기는 것 · 강조 응답은
보이기만(인용 · 인쇄에 쓰지 않음)을 적고, 3단계의 알려진 한계는 색인을 채우는 동안에만
남는다고 고쳤다. nestjs 스킬의 mail-search 줄도 2차에 맞췄다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 00:18:42 +09:00
parent f9fbf8a20f
commit 53a8d7176d
2 changed files with 45 additions and 4 deletions
+44 -3
View File
@@ -628,7 +628,7 @@ interface MailState {
영문 · 숫자로 끝나는 낱말(ERP · 2026)은 받침을 알 수 없어 두 가지를 다 붙인다. 따옴표 구절에는 붙이지 않는다.
조사 목록은 서버 상수 한 곳(`jmap/search-terms.ts`)에 둔다 — 한글로 끝나는 낱말은 형태 44개, 영문 · 숫자로 끝나면 59개가 되고,
개발 엔진에서 낱말 8개(조건 472개)를 전체 메일함에서 찾는 데 0.16초였다.
- **알려진 한계** (엔진 색인 때문 — 자체 검색 색인을 두기 전까지): 붙여 쓴 말의 일부('회의실'의 '회의', '서버이전'의 '서버'), 앞부분만 친 말('견적' → '견적서'),
- **알려진 한계** (엔진 색인 때문 — 14단계부터는 그 직원의 자체 색인을 채우는 동안(`building`)의 검색에만 남는다, 아래 "검색 — 14단계"): 붙여 쓴 말의 일부('회의실'의 '회의', '서버이전'의 '서버'), 앞부분만 친 말('견적' → '견적서'),
끝이 바뀐 말('보냈습니다'의 '보내'), **제목 맨 앞의 말머리**('[공지]' · '[긴급]' · 'Re:' · 'Fwd:' — 엔진이 제목을 정규화하며 색인에서 뺀다. 제목 가운데의 '[결재]'는 찾는다).
화면은 결과가 없을 때 이 한계를 한 줄로 알린다.
- 결과는 받은 시각 내림차순, `total` 포함. 커서는 그 검색어 · 범위로만 쓴다(다르면 400 `VAL_001`). 발송 중인 내부 초안은 지금처럼 빠진다.
@@ -642,7 +642,7 @@ interface MailState {
범위의 기본값은 검색을 시작한 메일함이고 결과 머리에서 "그 메일함 | 전체 메일함"으로 바꾼다(휴지통 · 스팸메일함에서 시작하면 그 안에서 찾는다).
✕ · Esc · "검색 닫기"는 검색 전 메일함으로 돌아간다. 임시보관함의 결과를 누르면 지금처럼 쓰기 화면에서 이어 쓴다.
### 검색 색인 (`GET /api/search-index`) — 14단계 (색인 바탕 — 검색 · 대화는 다음 차례에 이 절에 더한다)
### 검색 색인 (`GET /api/search-index`) — 14단계 (색인 바탕 — 검색은 아래 "검색 — 14단계", 대화는 다음 차례)
14단계는 엔진 검색 대신 **서버의 자체 색인**(지금 DB — 디자인 README 「14단계 — 검색 · 대화 묶기」)으로 찾는다. 이 절은 그 바탕 — 색인을 만들고 맞추는 규칙과 준비 상태다.
@@ -659,6 +659,47 @@ interface MailState {
- 로그인 필요(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 사용자 확정 — 본문을 다 훑으면 메일이 많은 직원은 수 초. 화면은 결과 머리에 한 줄 알린다).
- 본문은 앞 2만 자까지 찾는다. 첨부 파일 안의 글 · 끝이 바뀐 말('보내' → '보냈습니다')은 찾지 않는다(첨부 이름은 찾는다). 받는 사람 칸은 주소들을 이어 보므로 구절이 이웃한 두 주소에 걸쳐 맞을 수 있다(작은 한계).
- **응답 (목록)**
- 항목(MessageSummary)에 `searchHighlight: { subject: [start, end][], preview: { text, ranges: [start, end][] } } | null` — 색인 방식 검색에서만 차고, 메일함 목록 · 엔진 방식은 `null`.
위치는 NFC 글의 UTF-16 [시작, 끝)이고, 강조가 있으면 `subject` 도 NFC 로 싣는다. `preview.text` 는 본문에서 맞았으면 맞은 말 둘레 한 줄(잘린 쪽에 "…"), 아니면 엔진 미리 보기다.
- **`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`)로 알아보고 결과 머리에 한 줄 알린다.
- 화면은 조건도 화면 주소에 싣는다(검색어처럼 — 앞단 nginx 의 접근 로그는 쿼리를 남기지 않는다). 새 에러 코드는 없다(`VAL_001` · `RES_001` · `MAIL_001`).
### 여러 통 한꺼번에 (`PATCH /api/messages` · `POST /api/message-deletions`) — 3단계
```ts
@@ -1217,7 +1258,7 @@ interface MailSettings {
### 이 단계에서 하지 않는 것
탭을 모두 닫아도 오는 알림(웹 푸시 — 엔진은 `webpush-vapid` 를 지원한다), 알림 소리(운영체제 설정을 따른다), 휴대폰 푸시, 본문 안 이미지(인라인 cid — 붙여 넣은 이미지는 첨부로, 서명 그림만 예외), 메일 안의 배경 그림 보이기 · 보낸 사람마다 고르는 "이미지 늘 보기" 목록(9단계는 설정 세 가지로),
여러 개의 서명, 자체 검색 색인(붙여 쓴 말 · 앞부분 · 제목 말머리로 찾기), 조건 검색(보낸 사람 · 기간 · 첨부 있음), 검색어 강조,
여러 개의 서명(자체 검색 색인 · 조건 검색 · 검색어 강조는 14단계에서 했다 — 위 "검색 — 14단계"),
끌어다 놓아 옮기기, 메일함 옮기기(다른 상위로) · 하위 메일함까지 한꺼번에 지우기, 앱 비밀번호 · 2단계 인증(엔진은 지원한다), 부재중 응답의 서식 본문 · 사내/외부 구분,
예약한 메일의 「지금 보내기」 · 7일 넘는 예약(엔진 한도) · 차단할 때 이미 받은 그 보낸 사람의 메일 모두 옮기기(9단계는 그 메일만).