generated from ttipo/project-base
feat(backend): 메일 검색 색인의 바탕(표 · 맞추기 · 지난 메일 채우기 · 준비 상태)을 더한다
14단계 자체 검색 색인의 1차다. 검색 API 를 색인으로 바꾸는 일과 대화 키 잇기는 다음 차례다. - 마이그레이션 AddMailSearchIndex 하나: 표 mail_search_messages · mail_search_states 와 mail_settings.group_by_conversation 열(3차의 대화로 묶어 보기 설정 — 같은 배포에서 한 번에 만든다) - 접기 · 글자 두 개 묶음 · 맞은 곳 · 말머리 떼기 순수 함수와 표본 표(나중에 frontend 와 짝) - 엔진 포트 MAIL_INDEX_SOURCE + JMAP 어댑터: Email/changes 맞추기(실시간 · 5분 · 검색 전, 서버 전체 동시 4), 밤 창(KST 22:00~02:45 · 05:00~07:30) 지난 메일 채우기, 전체 대조 - GET /api/search-index, 로그인 · 새 비밀번호 때 상태 줄, 퇴사 마무리에 표 둘 - 로그 가림에 조건 검색 이름 넷, 개발 환경 DB 쿼리 로그 끔(메일 글이 DB 에 든다) 리뷰 반영: 시간 끝은 실패가 아니라 이어 하기, 본문 읽기 묶음의 메일 크기 상한(32MiB · 16MiB 넘으면 혼자), 겹친 id 빼기, 사내 도메인 주소만 색인(개발 서버 cron 이 e2e 줄을 건드리지 않게). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 메일 검색의 접기 · 글자 두 개 묶음 · 맞은 곳 · 말머리 떼기 표본 — 단위 테스트 전용 (앱 코드에서 import 하지 않는다).
|
||||
* 서버의 색인 · 찾기(src/shared/utils/search-text.ts · subject-base.ts)와 화면의 미리 확인 · 강조(frontend `shared/utils/search-match.ts`)를 같은 표로 본다.
|
||||
*
|
||||
* ⚠️ frontend `src/test-utils/search-text-samples.ts` 와 **같은 표**다(행과 순서까지 그대로 — 14단계 frontend 차례에 옮긴다).
|
||||
* 한쪽을 고치면 둘 다 고친다 — 접기 · 위치 규칙을 바꾸면 두 쪽 함수와 두 표를 함께 (서버가 준 맞은 곳의 위치를 화면이 그대로 그린다).
|
||||
* 답은 손으로 적었고 두 쪽 테스트가 함수의 결과와 견준다. 단위 테스트는 `@/../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:회의'],
|
||||
['[공지]', '[공지]'],
|
||||
// 전각 공백(U+3000)은 바꾸지 않는다 — 공백은 위치를 지키려고 그대로 둔다
|
||||
['회의\u3000일정', '회의\u3000일정'],
|
||||
// 소문자가 코드 포인트 하나 · 같은 길이일 때만 — 'İ'(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],
|
||||
],
|
||||
],
|
||||
// 구절은 띄어쓰기까지 그대로
|
||||
['회의 일정 안내', ['회의 일정'], [[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:', ''],
|
||||
['[공지]', ''],
|
||||
['', ''],
|
||||
];
|
||||
Reference in New Issue
Block a user