Files
arasystem/backend/test/search-text-samples.ts
T
ttipo 425cbc0e29 feat(backend): 대화 묶기를 더한다 — 대화 키 · 같은 대화 · 대화로 묶은 목록 · 설정
14단계 3차다. 같은 대화(답장 · 전달로 이어진 메일)를 읽기 화면에 보이고, 목록을 대화로
묶어 볼 수 있게 한다. 색인이 준비된 직원은 우리 대화 키로, 채우는 중인 직원은 엔진의
스레드로 묶는다(응답의 basis — 첫 쪽에서 정하고 커서가 잇는다).

- 대화 키: 색인 쓰기 때 넣은 메일의 부모 · 자식(References · In-Reply-To) 가운데 말머리
  (회신 · 답장 · 전달 포함)를 뗀 제목이 같은 것을 가장 작은 키로 합친다. 늦게 온 원본도
  잇고, 다시 넣어도 키는 풀리지 않는다. 색인 판 4
- GET /api/messages/{id}/conversation — 오래된 차례, 휴지통 · 스팸메일함에만 든 메일과
  발송 중인 내부 초안은 빼고 지금 메일은 늘 넣는다
- GET /api/conversations?mailboxId= — 그 메일함의 대화 한 줄씩(참여자 10명 · 통 수 ·
  messageIds 최신 500 · pendingSendIds). 직원 주소 btree 로 먼저 좁힌다(10명 흉내 큰 메일함
  첫 쪽 81ms)
- 설정 groupByConversation(처음 끔) — 서버는 저장만 하고 묶을지는 화면이 정한다
- 마이그레이션 AddMailSearchSender — 참여자는 따로 둔 첫 보낸 사람 칸에서만 읽는다

리뷰 반영: 엔진 방식에 개수 · 시간 상한(같은 대화 한 요청 1,000통 · 묶은 목록 스레드마다
500 · 한 요청 2,000 · 15초, 요약 속성은 내보낼 메일만), Mailbox/get 장애는 503,
꾸민 커서는 엔진 · DB 보다 먼저 400, 같은 대화 한 문장, 이름 안의 <주소>로 참여자를
꾸미지 못하게, 로그 추적 ID. 검색 표본 표 머리 주석(frontend 로 옮겼다)도 함께 고친다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 05:39:10 +09:00

147 lines
5.6 KiB
TypeScript

/**
* 메일 검색의 접기 · 글자 두 개 묶음 · 맞은 곳 · 말머리 떼기 표본 — 단위 테스트 전용 (앱 코드에서 import 하지 않는다).
* 서버의 색인 · 찾기(src/shared/utils/search-text.ts · subject-base.ts)와 화면의 미리 확인 · 강조(frontend `shared/utils/search-match.ts`)를 같은 표로 본다.
*
* ⚠️ frontend `src/test-utils/search-text-samples.ts` 와 **같은 표**다(행과 순서까지 그대로).
* 한쪽을 고치면 둘 다 고친다 — 접기 · 위치 규칙을 바꾸면 두 쪽 함수와 두 표를 함께 (서버가 준 맞은 곳의 위치를 화면이 그대로 그린다).
* 답은 손으로 적었고 두 쪽 테스트가 함수의 결과와 견준다. 단위 테스트는 `@/../test/search-text-samples` 로 부른다 (cpu-time.ts 와 같다).
*/
/** 접기 — [넣은 글, 접은 글]. 접은 글은 NFC 원문과 UTF-16 길이가 늘 같다 */
export const FOLD_SAMPLES: readonly (readonly [
input: string,
folded: string,
])[] = [
['계약서', '계약서'],
['Project PLAN', 'project plan'],
// 전각 ASCII → 반각 (대문자는 소문자로까지)
['ABC123!', 'abc123!'],
['RE:회의', 're:회의'],
['[공지]', '[공지]'],
// 공백류(전각 공백 · 줄바꿈 · 탭 · NBSP · CR)는 띄어쓰기 하나로 1:1 (색인 판 2) — 이어진 공백을 줄이지는 않는다(위치를 지킨다)
['회의\u3000일정', '회의 일정'],
['계약\n갱신\t안내\u00a0끝', '계약 갱신 안내 끝'],
['a\r\n\nb', 'a b'],
// 소문자가 코드 포인트 하나 · 같은 길이일 때만 — 'İ'(U+0130)는 'i̇' 두 글자가 되므로 그대로
['İstanbul', 'İstanbul'],
['ÀÉÎÕÜ', 'àéîõü'],
['ΣΑΣ', 'σασ'],
// 풀어 쓴 한글(NFD)은 NFC 로 모은다
['\u1112\u1161\u11ab\u1100\u1173\u11af', '한글'],
// ㈜ 는 그대로 (NFKC 가 아니다 — 길이를 바꾸지 않는다)
['㈜아라', '㈜아라'],
// BMP 밖 글자 — Deseret 대문자는 소문자로(둘 다 두 단위), 이모지는 그대로
['\u{10400}😀', '\u{10428}😀'],
['', ''],
];
/** 글자 두 개 묶음 — [접은 글, 묶음(나온 차례, 겹치지 않게)] */
export const GRAM_SAMPLES: readonly (readonly [
fold: string,
grams: readonly string[],
])[] = [
['계약서', ['계약', '약서']],
['회의 일정', ['회의', '일정']],
['계약', ['계약']],
['계', []],
['a b c', []],
['aaaa', ['aa']],
['ab\nab\tab', ['ab']],
// 전각 공백 · NBSP 도 공백이다
['회의\u3000일정\u00a0안내', ['회의', '일정', '안내']],
// 문장부호는 글자다 — 're:' 의 'e:' 도 묶음
['re:회의', ['re', 'e:', ':회', '회의']],
// BMP 밖 글자는 코드 포인트 하나
['😀😁', ['😀😁']],
['', []],
];
/** 맞은 곳 — [원문(NFC), 찾는 말들, 맞은 곳들 [시작, 끝) UTF-16] — 겹치거나 맞닿으면 합친다 */
export const MATCH_SAMPLES: readonly (readonly [
original: string,
terms: readonly string[],
ranges: readonly (readonly [number, number])[],
])[] = [
['계약서를 보냈습니다', ['계약'], [[0, 2]]],
['Project plan', ['PROJ'], [[0, 4]]],
['ABC 주문', ['abc'], [[0, 3]]],
[
'회의 회의록 회의실',
['회의'],
[
[0, 2],
[3, 5],
[7, 9],
],
],
// 겹치는 자리도 찾아 합친다
['aaa', ['aa'], [[0, 3]]],
// 맞닿은 두 말은 하나로
['계약서', ['계약', '서'], [[0, 3]]],
[
'회의 일정 안내',
['일정', '회의'],
[
[0, 2],
[3, 5],
],
],
// 구절은 줄바꿈 · NBSP 로 갈린 글에도 맞는다 (공백류 → 띄어쓰기 1:1) — 위치는 원문 그대로
['계약\n갱신 안내', ['계약 갱신'], [[0, 5]]],
['계약\u00a0갱신', ['계약 갱신'], [[0, 5]]],
// 구절은 띄어쓰기까지 그대로
['회의 일정 안내', ['회의 일정'], [[0, 5]]],
['회의 일정', ['회의 일정'], []],
[
'[공지] Re: 회의',
['공지', 're'],
[
[1, 3],
[5, 7],
],
],
// 빈 말은 건너뛴다 · 없는 말은 맞은 곳 없음
['계약', ['', '없음'], []],
// BMP 밖 글자 앞뒤에서도 UTF-16 위치
['😀계약', ['계약'], [[2, 4]]],
['', ['계약'], []],
];
/** 말머리 떼기 — [제목, 말머리를 뗀 접기 글] */
export const SUBJECT_BASE_SAMPLES: readonly (readonly [
subject: string,
base: string,
])[] = [
['회의 일정 안내', '회의 일정 안내'],
['Re: 회의 일정 안내', '회의 일정 안내'],
['RE: Re: FW: 회의 일정 안내', '회의 일정 안내'],
['Re:회의', '회의'],
['Fwd: 견적서', '견적서'],
['AW: WG: Angebot', 'angebot'],
// 한국어 Outlook — 엔진은 떼지 않는다
['회신: 회의 일정 안내', '회의 일정 안내'],
['답장: 회신: 전달: 회의', '회의'],
['회신 : 회의', '회의'],
// 수 · 전각 쌍점
['Re[2]: 회의', '회의'],
['RE(3): 회의', '회의'],
['Re:회의', '회의'],
['RE: 회의', '회의'],
// 대괄호 꼬리표 (여러 겹 · 말머리와 섞여도)
['[공지] 회의 일정', '회의 일정'],
['Re: [공지] [중요] 회의', '회의'],
['[외부] RE: 견적', '견적'],
// 공백은 하나로 · 대소문자 없이
[' Re: 회의 일정 ', '회의 일정'],
['Project PLAN', 'project plan'],
// 쌍점이 없으면 말머리가 아니다
['Re 회의', 're 회의'],
['Reply to 회의', 'reply to 회의'],
['Regarding: 회의', 'regarding: 회의'],
['회신드립니다', '회신드립니다'],
// 다 떼면 빈 글
['Re:', ''],
['[공지]', ''],
['', ''],
];