refactor(v026): #3+#19+#34 KST helper 통합 → src/shared/util/kstDate.ts
기존 src/main/util/kstDate.ts (2 함수) 를 shared 로 이동 + kstTodayAsDate 추가. main + renderer 양쪽 import 가능. 6 callsite 통합: - NoteRepository.findExpiredCandidates (todayInKstString → kstTodayIso) - TelemetryService.todayKstIso (inline 제거) - telemetryStats.kstDate (inline 제거) - AiWorker.todayKstAsDate / todayKstAsIso (inline 제거) - store.snoozeExpired + snoozeRecall (inline 제거 → nextKstMidnightMs) API: kstTodayIso(now) / nextKstMidnightMs(now) / kstTodayAsDate(now) + KST_OFFSET_MS, DAY_MS 상수 export. 단위 +4 cases (boundary, format, midnight, asDate). 418 → 422. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,18 +3,7 @@ import type { Note } from '@shared/types';
|
||||
import { ProviderHolder } from './ProviderHolder.js';
|
||||
import { parseAllCandidates } from '../services/dueDateParser.js';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
|
||||
function todayKstAsDate(now: Date): Date {
|
||||
// Returns a Date object whose UTC year/month/day match KST today
|
||||
const k = new Date(now.getTime() + KST_OFFSET_MS);
|
||||
return new Date(Date.UTC(k.getUTCFullYear(), k.getUTCMonth(), k.getUTCDate()));
|
||||
}
|
||||
|
||||
function todayKstAsIso(now: Date): string {
|
||||
return todayKstAsDate(now).toISOString().slice(0, 10);
|
||||
}
|
||||
import { kstTodayAsDate, kstTodayIso } from '../../shared/util/kstDate.js';
|
||||
|
||||
function classifyReason(err: unknown): 'unreachable' | 'schema' | 'timeout' | 'other' {
|
||||
if (err instanceof ZodError) return 'schema';
|
||||
@@ -131,8 +120,8 @@ export class AiWorker {
|
||||
const note = this.repo.findById(job.noteId);
|
||||
if (!note || note.deletedAt !== null || note.aiStatus !== 'pending') return;
|
||||
const nowDate = this.now();
|
||||
const todayDate = todayKstAsDate(nowDate);
|
||||
const todayIso = todayKstAsIso(nowDate);
|
||||
const todayDate = kstTodayAsDate(nowDate);
|
||||
const todayIso = kstTodayIso(nowDate);
|
||||
const candidates = parseAllCandidates(note.rawText, todayDate);
|
||||
const vocab = this.repo.getTopUsedTags(20);
|
||||
const res = await this.holder.get().generate({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import { v7 as uuidv7, v4 as uuidv4 } from 'uuid';
|
||||
import type { Note, NoteMedia, NoteTag } from '@shared/types';
|
||||
import { todayInKstString } from '../util/kstDate.js';
|
||||
import { kstTodayIso } from '../../shared/util/kstDate.js';
|
||||
|
||||
export interface CreateNoteInput { rawText: string; }
|
||||
|
||||
@@ -601,7 +601,7 @@ export class NoteRepository {
|
||||
* Caller may inject `now` for testability; defaults to `new Date()`.
|
||||
*/
|
||||
findExpiredCandidates(now: Date = new Date()): Note[] {
|
||||
const today = todayInKstString(now);
|
||||
const today = kstTodayIso(now);
|
||||
const rows = this.db
|
||||
.prepare(
|
||||
`SELECT * FROM notes
|
||||
|
||||
@@ -2,15 +2,7 @@ import { mkdir, appendFile, readFile, readdir, unlink, writeFile } from 'node:fs
|
||||
import { join } from 'node:path';
|
||||
import { validateEvent, TelemetryEvent } from './telemetryEvents.js';
|
||||
import { aggregateStats } from './telemetryStats.js';
|
||||
|
||||
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function todayKstIso(now: Date): string {
|
||||
const k = new Date(now.getTime() + KST_OFFSET_MS);
|
||||
return new Date(Date.UTC(k.getUTCFullYear(), k.getUTCMonth(), k.getUTCDate()))
|
||||
.toISOString().slice(0, 10);
|
||||
}
|
||||
import { kstTodayIso, DAY_MS } from '../../shared/util/kstDate.js';
|
||||
|
||||
export interface TelemetryServiceOptions {
|
||||
silent?: boolean;
|
||||
@@ -54,7 +46,7 @@ export class TelemetryService {
|
||||
return { removed };
|
||||
}
|
||||
const cutoff = new Date(this.now().getTime() - this.retentionDays * DAY_MS);
|
||||
const cutoffIso = todayKstIso(cutoff); // KST 일자 비교
|
||||
const cutoffIso = kstTodayIso(cutoff); // KST 일자 비교
|
||||
for (const name of entries) {
|
||||
const m = /^events-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(name);
|
||||
if (!m) continue;
|
||||
@@ -77,7 +69,7 @@ export class TelemetryService {
|
||||
const nowDate = this.now();
|
||||
const ts = nowDate.toISOString();
|
||||
const event = validateEvent({ ts, kind: input.kind, payload: input.payload });
|
||||
const filePath = join(this.dir, `events-${todayKstIso(nowDate)}.jsonl`);
|
||||
const filePath = join(this.dir, `events-${kstTodayIso(nowDate)}.jsonl`);
|
||||
try {
|
||||
await mkdir(this.dir, { recursive: true });
|
||||
await appendFile(filePath, JSON.stringify(event) + '\n', 'utf8');
|
||||
@@ -96,7 +88,7 @@ export class TelemetryService {
|
||||
return events;
|
||||
}
|
||||
const cutoffMs = this.now().getTime() - this.retentionDays * DAY_MS;
|
||||
const cutoffIso = todayKstIso(new Date(cutoffMs));
|
||||
const cutoffIso = kstTodayIso(new Date(cutoffMs));
|
||||
// 회차 1 review (PR #13) — 매직 슬라이스 `n.slice(7, 17)` 대신 정규식 capture 그룹으로
|
||||
// 일자를 추출. prefix 변경 시 정규식 한 곳만 고치면 됨.
|
||||
const datePattern = /^events-(\d{4}-\d{2}-\d{2})\.jsonl$/;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import type { TelemetryEvent } from './telemetryEvents.js';
|
||||
|
||||
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
import { kstTodayIso } from '../../shared/util/kstDate.js';
|
||||
|
||||
function kstDate(ts: string): string {
|
||||
const d = new Date(ts);
|
||||
const k = new Date(d.getTime() + KST_OFFSET_MS);
|
||||
return new Date(Date.UTC(k.getUTCFullYear(), k.getUTCMonth(), k.getUTCDate()))
|
||||
.toISOString().slice(0, 10);
|
||||
return kstTodayIso(new Date(ts));
|
||||
}
|
||||
|
||||
interface DailyRow {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Calendar date (YYYY-MM-DD) in Asia/Seoul timezone for the given instant.
|
||||
*
|
||||
* v0.2.3 #5 — used by NoteRepository.findExpiredCandidates to compare against
|
||||
* notes.due_date (also stored as YYYY-MM-DD per slice §F1).
|
||||
*/
|
||||
export function todayInKstString(now: Date): string {
|
||||
const k = new Date(now.getTime() + KST_OFFSET_MS);
|
||||
return new Date(
|
||||
Date.UTC(k.getUTCFullYear(), k.getUTCMonth(), k.getUTCDate())
|
||||
).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Epoch ms of the next 00:00 KST strictly after `now`.
|
||||
*
|
||||
* v0.2.3 #5 — used by store.snoozeExpired to compute the in-memory snooze
|
||||
* deadline ("오늘 그만").
|
||||
*/
|
||||
export function nextKstMidnightMs(now: number): number {
|
||||
const kstNow = now + KST_OFFSET_MS;
|
||||
// Floor to KST midnight, then add one day.
|
||||
const kstMidnightFloor = Math.floor(kstNow / 86_400_000) * 86_400_000;
|
||||
const nextKstMidnight = kstMidnightFloor + 86_400_000;
|
||||
return nextKstMidnight - KST_OFFSET_MS;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Note, WeeklyContinuity } from '@shared/types';
|
||||
import { inboxApi } from './api.js';
|
||||
import { nextKstMidnightMs } from '@shared/util/kstDate.js';
|
||||
|
||||
export { selectFilteredNotes } from './selectFilteredNotes.js';
|
||||
|
||||
@@ -177,12 +178,7 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
snoozeExpired() {
|
||||
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const kstNow = now + KST_OFFSET_MS;
|
||||
const kstMidnightFloor = Math.floor(kstNow / 86_400_000) * 86_400_000;
|
||||
const nextKstMidnight = kstMidnightFloor + 86_400_000;
|
||||
set({ expiredSnoozeUntilMs: nextKstMidnight - KST_OFFSET_MS });
|
||||
set({ expiredSnoozeUntilMs: nextKstMidnightMs(Date.now()) });
|
||||
},
|
||||
async recheckOllama() {
|
||||
const status = await inboxApi.ollamaRecheck();
|
||||
@@ -212,12 +208,7 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
set({ recallCandidate, recallSnoozeUntilMs: null });
|
||||
},
|
||||
async snoozeRecall() {
|
||||
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const kstNow = now + KST_OFFSET_MS;
|
||||
const kstMidnightFloor = Math.floor(kstNow / 86_400_000) * 86_400_000;
|
||||
const nextKstMidnight = kstMidnightFloor + 86_400_000;
|
||||
set({ recallSnoozeUntilMs: nextKstMidnight - KST_OFFSET_MS });
|
||||
set({ recallSnoozeUntilMs: nextKstMidnightMs(Date.now()) });
|
||||
// m1 fix — candidate=null 인 race 케이스 (사용자가 banner 닫힌 직후 클릭) 시
|
||||
// snooze 는 적용하되 emit 만 skip. telemetry 누락 받아들임 (의도적).
|
||||
const candidate = get().recallCandidate;
|
||||
|
||||
42
src/shared/util/kstDate.ts
Normal file
42
src/shared/util/kstDate.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* KST timezone helpers — main + renderer 양쪽에서 import 가능.
|
||||
* v0.2.6 C1: backlog #3+#19+#34 통합 (기존 src/main/util/kstDate.ts 이동).
|
||||
*/
|
||||
|
||||
export const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
||||
export const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* KST 자정 기준 today YYYY-MM-DD.
|
||||
*
|
||||
* 기존 todayInKstString (NoteRepository.findExpiredCandidates),
|
||||
* TelemetryService.todayKstIso, telemetryStats.kstDate, AiWorker.todayKstAsIso
|
||||
* 4 callsite 통합.
|
||||
*/
|
||||
export function kstTodayIso(now: Date = new Date()): string {
|
||||
const k = new Date(now.getTime() + KST_OFFSET_MS);
|
||||
return new Date(Date.UTC(k.getUTCFullYear(), k.getUTCMonth(), k.getUTCDate()))
|
||||
.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 KST 자정의 epoch ms (UTC).
|
||||
*
|
||||
* 기존 nextKstMidnightMs (store.snoozeExpired) + store.snoozeRecall inline 통합.
|
||||
*/
|
||||
export function nextKstMidnightMs(now: number = Date.now()): number {
|
||||
const kstNow = now + KST_OFFSET_MS;
|
||||
const kstMidnightFloor = Math.floor(kstNow / DAY_MS) * DAY_MS;
|
||||
const nextKstMidnight = kstMidnightFloor + DAY_MS;
|
||||
return nextKstMidnight - KST_OFFSET_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* KST today (00:00 KST 의 UTC Date 객체). AiWorker 의 dueDateParser 가 candidate 비교용.
|
||||
*
|
||||
* 기존 AiWorker.todayKstAsDate 통합.
|
||||
*/
|
||||
export function kstTodayAsDate(now: Date = new Date()): Date {
|
||||
const k = new Date(now.getTime() + KST_OFFSET_MS);
|
||||
return new Date(Date.UTC(k.getUTCFullYear(), k.getUTCMonth(), k.getUTCDate()));
|
||||
}
|
||||
@@ -1,18 +1,29 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { todayInKstString, nextKstMidnightMs } from '@main/util/kstDate.js';
|
||||
import { kstTodayIso, nextKstMidnightMs, kstTodayAsDate } from '@shared/util/kstDate.js';
|
||||
|
||||
describe('todayInKstString', () => {
|
||||
describe('kstTodayIso', () => {
|
||||
it('returns KST calendar date as YYYY-MM-DD', () => {
|
||||
// 2026-05-01 12:00 UTC = 2026-05-01 21:00 KST
|
||||
expect(todayInKstString(new Date('2026-05-01T12:00:00Z'))).toBe('2026-05-01');
|
||||
expect(kstTodayIso(new Date('2026-05-01T12:00:00Z'))).toBe('2026-05-01');
|
||||
});
|
||||
|
||||
it('handles UTC→KST date rollover (UTC 23:30 → KST next day 08:30)', () => {
|
||||
expect(todayInKstString(new Date('2026-05-01T23:30:00Z'))).toBe('2026-05-02');
|
||||
expect(kstTodayIso(new Date('2026-05-01T23:30:00Z'))).toBe('2026-05-02');
|
||||
});
|
||||
|
||||
it('handles KST midnight exactly (UTC 15:00 = KST 00:00 next day)', () => {
|
||||
expect(todayInKstString(new Date('2026-05-01T15:00:00Z'))).toBe('2026-05-02');
|
||||
expect(kstTodayIso(new Date('2026-05-01T15:00:00Z'))).toBe('2026-05-02');
|
||||
});
|
||||
|
||||
it('boundary — UTC 14:59:59 still KST 23:59:59 same day', () => {
|
||||
// KST 5/4 23:59:59 = UTC 5/4 14:59:59
|
||||
const utcDate = new Date('2026-05-04T14:59:59Z');
|
||||
expect(kstTodayIso(utcDate)).toBe('2026-05-04');
|
||||
});
|
||||
|
||||
it('KST 5/5 00:30 (UTC 5/4 15:30) returns 2026-05-05', () => {
|
||||
const utcDate = new Date('2026-05-04T15:30:00Z');
|
||||
expect(kstTodayIso(utcDate)).toBe('2026-05-05');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,4 +45,19 @@ describe('nextKstMidnightMs', () => {
|
||||
expect(next - now).toBeGreaterThan(23 * 60 * 60 * 1000);
|
||||
expect(next - now).toBeLessThan(24 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it('KST 5/5 00:30 → next KST midnight = 5/6 00:00 KST = 5/5 15:00 UTC', () => {
|
||||
const utcMs = new Date('2026-05-04T15:30:00Z').getTime();
|
||||
const next = nextKstMidnightMs(utcMs);
|
||||
expect(new Date(next).toISOString()).toBe('2026-05-05T15:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('kstTodayAsDate', () => {
|
||||
it('returns UTC Date at KST 00:00', () => {
|
||||
// KST 5/5 00:30 → KST 5/5 00:00 = UTC 5/4 15:00
|
||||
const utcDate = new Date('2026-05-04T15:30:00Z');
|
||||
const result = kstTodayAsDate(utcDate);
|
||||
expect(result.toISOString()).toBe('2026-05-05T00:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user