feat(repo): countToday(now?) — KST midnight bucket count

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.
This commit is contained in:
altair823
2026-04-26 11:47:03 +09:00
parent 5b6003bdcd
commit cca3029b7e
2 changed files with 61 additions and 0 deletions

View File

@@ -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`)

View File

@@ -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);
});
});