generated from ttipo/project-base
fix: 화면 · 오류 문구를 사람이 말하듯 다듬고 엔진 검색의 기간 시작 경계를 고친다 #19
@@ -898,7 +898,7 @@ describe('JmapMailProvider', () => {
|
||||
],
|
||||
];
|
||||
|
||||
it('조건은 칸마다 엔진 필터로 — 받는 사람은 to · cc 가운데 하나, 기간은 받은 시각(after 이상 · before 미만), 첨부는 hasAttachment', async () => {
|
||||
it('조건은 칸마다 엔진 필터로 — 받는 사람은 to · cc 가운데 하나, 기간은 받은 시각(after 이상 — 엔진의 after 는 그 초를 넣지 않아 한 초 앞 · before 미만), 첨부는 hasAttachment', async () => {
|
||||
const { provider, recorded } = fakeClient([
|
||||
mailboxRoles,
|
||||
searchResponses(['m1'], 1),
|
||||
@@ -942,13 +942,37 @@ describe('JmapMailProvider', () => {
|
||||
operator: 'OR',
|
||||
conditions: particleForms('견적').map((body) => ({ body })),
|
||||
},
|
||||
{ after: '2026-10-31T15:00:00Z' },
|
||||
{ after: '2026-10-31T14:59:59Z' },
|
||||
{ before: '2026-11-30T15:00:00Z' },
|
||||
{ hasAttachment: true },
|
||||
]);
|
||||
expect(page.items.map((item) => item.id)).toEqual(['m1']);
|
||||
});
|
||||
|
||||
it('기간의 after 는 색인 길(>=)과 같은 경계로 — 정수 초는 한 초 앞(달 · 날이 바뀌는 0시도), 소수 초는 초로 올린 값의 한 초 앞(= 그 초)', async () => {
|
||||
const cases: [string, string][] = [
|
||||
['2026-11-01T00:00:00.000Z', '2026-10-31T23:59:59Z'],
|
||||
['2026-10-31T15:00:00Z', '2026-10-31T14:59:59Z'],
|
||||
['2026-10-31T15:00:00.500Z', '2026-10-31T15:00:00Z'],
|
||||
['2026-10-31T15:00:00.001Z', '2026-10-31T15:00:00Z'],
|
||||
['2026-10-31T15:00:00.999Z', '2026-10-31T15:00:00Z'],
|
||||
];
|
||||
for (const [after, engineAfter] of cases) {
|
||||
const { provider, recorded } = fakeClient([
|
||||
mailboxRoles,
|
||||
searchResponses([], 0),
|
||||
]);
|
||||
await provider.listMessages(ARA, { limit: 20, conditions: { after } });
|
||||
const { filter } = argsOf(recorded[1]) as {
|
||||
filter: { conditions: unknown[] };
|
||||
};
|
||||
expect({ after, conditions: filter.conditions.slice(1) }).toEqual({
|
||||
after,
|
||||
conditions: [{ after: engineAfter }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('검색어와 조건은 함께 AND — 커서 지문에 조건이 들어가 조건이 다른 검색의 커서는 엔진에 묻기 전에 VAL_001', async () => {
|
||||
const conditions = { from: '이도윤', hasAttachment: true as const };
|
||||
const first = fakeClient([
|
||||
|
||||
@@ -134,6 +134,7 @@ import {
|
||||
selectAccountQuota,
|
||||
toAddresses,
|
||||
toDetailSource,
|
||||
toJmapAfterInclusive,
|
||||
toJmapUtcDate,
|
||||
toMailbox,
|
||||
toSummary,
|
||||
@@ -310,6 +311,7 @@ function conditionsKey(
|
||||
|
||||
/**
|
||||
* 조건 → 엔진 필터 조건들 (AND 로 묶어 쓴다) — 글 칸은 검색어와 같은 낱말 · 구절 · 조사 형태, 기간은 받은 시각(after 이상 · before 미만), 첨부는 엔진의 판정.
|
||||
* after 는 엔진이 그 초를 넣지 않아 한 초 앞을 넘긴다(toJmapAfterInclusive — 색인 길의 `>=` 와 같은 경계)
|
||||
* 글을 준 칸에 찾을 낱말이 하나도 없으면 null — 아무것도 맞지 않는다(묻지 않고 빈 결과)
|
||||
*/
|
||||
function conditionFilters(
|
||||
@@ -325,7 +327,7 @@ function conditionFilters(
|
||||
filters.push(...found);
|
||||
}
|
||||
if (conditions.after)
|
||||
filters.push({ after: toJmapUtcDate(conditions.after) });
|
||||
filters.push({ after: toJmapAfterInclusive(conditions.after) });
|
||||
if (conditions.before) {
|
||||
filters.push({ before: toJmapUtcDate(conditions.before) });
|
||||
}
|
||||
|
||||
@@ -378,6 +378,25 @@ export function toJmapUtcDate(iso: string): string {
|
||||
return new Date(Date.parse(iso)).toISOString().replace(/\.\d+Z$/, 'Z');
|
||||
}
|
||||
|
||||
/** JMAP UTCDate 의 단위 — 초 (엔진은 받은 시각을 초로 둔다) */
|
||||
const JMAP_DATE_RESOLUTION_MS = 1_000;
|
||||
|
||||
/**
|
||||
* "받은 시각이 그 시각 이상"을 엔진의 Email/query `after` 값으로 — 엔진의 after 는 **그 초를 넣지 않는다**
|
||||
* ("그 초보다 뒤" — RFC 8621 은 "같거나 뒤"라 적지만 Stalwart 0.16.27 은 아니다, 2026-11-08 개발 엔진에서 확인).
|
||||
* 받은 시각은 초라 "그 시각 이상"은 "그 시각을 초로 올린 값 이상"이고, 엔진에는 그 한 초 앞을 넘긴다 — 색인 길(mail-search/search-sql.ts 의 `>=`)과
|
||||
* 같은 경계다. 화면이 보내는 0시(정수 초)면 한 초 앞, 소수 초면 그 초 그대로.
|
||||
* ⚠️ 엔진이 RFC 대로 같은 초를 넣게 바뀌면 한 초 앞의 메일까지 맞는다 — 엔진 판을 올릴 때 다시 본다
|
||||
*/
|
||||
export function toJmapAfterInclusive(iso: string): string {
|
||||
const ceiled =
|
||||
Math.ceil(Date.parse(iso) / JMAP_DATE_RESOLUTION_MS) *
|
||||
JMAP_DATE_RESOLUTION_MS;
|
||||
return toJmapUtcDate(
|
||||
new Date(ceiled - JMAP_DATE_RESOLUTION_MS).toISOString(),
|
||||
);
|
||||
}
|
||||
|
||||
/** 발신 ID — 직원 주소와 같은 것을 고르고, 없으면 첫 번째 */
|
||||
export function pickIdentity(
|
||||
identities: JmapIdentity[],
|
||||
|
||||
Reference in New Issue
Block a user