From cca3029b7e33f2a6bddb630b77cd36183ae17bd5 Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 26 Apr 2026 11:47:03 +0900 Subject: [PATCH] =?UTF-8?q?feat(repo):=20countToday(now=3F)=20=E2=80=94=20?= =?UTF-8?q?KST=20midnight=20bucket=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the F4-C·F cue strengthening surfaces (tray tooltip + Inbox identity counter), main + renderer need a single source of truth for "오늘 N번 잡아뒀다". Implements `NoteRepository.countToday(now?)` that computes the half-open UTC interval covering the KST calendar date of `now` and counts rows whose `created_at` falls inside. `now` is injectable for deterministic tests across the KST/UTC boundary (02:00 KST and 23:00 KST land on different UTC dates yet the same / a different KST day). Four new cases cover empty DB, KST-day filtering, KST-midnight crossover, and the default-arg branch. --- src/main/repository/NoteRepository.ts | 22 +++++++++++++++ tests/unit/NoteRepository.test.ts | 39 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts index ee4f4f4..13d26cb 100644 --- a/src/main/repository/NoteRepository.ts +++ b/src/main/repository/NoteRepository.ts @@ -302,6 +302,28 @@ export class NoteRepository { return row.c; } + /** + * Count notes whose `created_at` falls on the KST calendar date of `now`. + * KST = UTC+9. We compute the UTC half-open interval + * [KST-midnight today, KST-midnight tomorrow) + * and count rows whose UTC ISO `created_at` lies inside. + */ + countToday(now: Date = new Date()): number { + const KST_OFFSET_MS = 9 * 60 * 60 * 1000; + const kstNow = new Date(now.getTime() + KST_OFFSET_MS); + const kstYear = kstNow.getUTCFullYear(); + const kstMonth = kstNow.getUTCMonth(); + const kstDate = kstNow.getUTCDate(); + const kstMidnightUtc = Date.UTC(kstYear, kstMonth, kstDate) - KST_OFFSET_MS; + const nextKstMidnightUtc = kstMidnightUtc + 24 * 60 * 60 * 1000; + const startIso = new Date(kstMidnightUtc).toISOString(); + const endIso = new Date(nextKstMidnightUtc).toISOString(); + const row = this.db + .prepare(`SELECT COUNT(*) AS c FROM notes WHERE created_at >= ? AND created_at < ?`) + .get(startIso, endIso) as { c: number }; + return row.c; + } + getAllPendingJobs(): Array<{ noteId: string; attempts: number; nextRunAt: string }> { const rows = this.db .prepare(`SELECT note_id, attempts, next_run_at FROM pending_jobs`) diff --git a/tests/unit/NoteRepository.test.ts b/tests/unit/NoteRepository.test.ts index 0ae09a6..5fde31d 100644 --- a/tests/unit/NoteRepository.test.ts +++ b/tests/unit/NoteRepository.test.ts @@ -175,4 +175,43 @@ describe('NoteRepository', () => { const note = repo.findById(id)!; expect(note.dueDate).toBeNull(); }); + + it('countToday returns 0 for empty DB', () => { + expect(repo.countToday(new Date('2026-04-26T12:00:00Z'))).toBe(0); + }); + + it('countToday counts notes created on the KST date of "now"', () => { + // now = 2026-04-26 14:00 KST (= 2026-04-26T05:00:00Z UTC). + // a, b: 2026-04-26 KST → counted. + // c: 2026-04-25 KST → excluded. + db.prepare( + `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) + VALUES (?, 'x', 'pending', ?, ?)` + ).run('a', '2026-04-25T17:00:00Z', '2026-04-25T17:00:00Z'); // 04-26 02:00 KST + db.prepare( + `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) + VALUES (?, 'x', 'pending', ?, ?)` + ).run('b', '2026-04-25T18:00:00Z', '2026-04-25T18:00:00Z'); // 04-26 03:00 KST + db.prepare( + `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) + VALUES (?, 'x', 'pending', ?, ?)` + ).run('c', '2026-04-25T14:00:00Z', '2026-04-25T14:00:00Z'); // 04-25 23:00 KST + expect(repo.countToday(new Date('2026-04-26T05:00:00Z'))).toBe(2); + }); + + it('countToday handles KST midnight crossover', () => { + // now = 2026-04-26 14:00 KST. A note at 2026-04-26T23:30Z = 2026-04-27 08:30 KST + // belongs to "tomorrow" (KST), so MUST NOT be counted as "today". + db.prepare( + `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) + VALUES (?, 'x', 'pending', ?, ?)` + ).run('a', '2026-04-26T23:30:00Z', '2026-04-26T23:30:00Z'); + expect(repo.countToday(new Date('2026-04-26T05:00:00Z'))).toBe(0); + }); + + it('countToday default arg uses Date.now()', () => { + const n = repo.countToday(); + expect(typeof n).toBe('number'); + expect(n).toBeGreaterThanOrEqual(0); + }); });