Files
inkling/src/main/services/ContinuityService.ts
altair823 41310dbe6a refactor(v032): KST_OFFSET_MS inline → @shared/util/kstDate import (#19)
5 callsite (NoteRepository, ftsHelpers, BackupService, ContinuityService,
NoteCard) 모두 canonical export 로 정리. 알고리즘 동일 (9 * 60 * 60 * 1000),
회귀 PASS 검증.

v0.2.6 commit 3cfa60b 가 4 callsite migrate 했지만 5 callsite 잔여.
Cut F audit 에서 발견.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:38:58 +09:00

86 lines
2.5 KiB
TypeScript

import type Database from 'better-sqlite3';
import type { WeeklyContinuity } from '@shared/types';
import { KST_OFFSET_MS } from '../../shared/util/kstDate.js';
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const WEEK_TARGET = 7;
const RECOVERY_GAP_DAYS = 7;
function toKstDateKey(d: Date): string {
const k = new Date(d.getTime() + KST_OFFSET_MS);
return k.toISOString().slice(0, 10);
}
function kstMondayOf(d: Date): string {
const k = new Date(d.getTime() + KST_OFFSET_MS);
const dayIdx = (k.getUTCDay() + 6) % 7;
k.setUTCDate(k.getUTCDate() - dayIdx);
return k.toISOString().slice(0, 10);
}
function addDaysIso(iso: string, days: number): string {
const d = new Date(iso + 'T00:00:00Z');
d.setUTCDate(d.getUTCDate() + days);
return d.toISOString().slice(0, 10);
}
export class ContinuityService {
constructor(
private db: Database.Database,
private now: () => Date = () => new Date()
) {}
get(): WeeklyContinuity {
const rows = this.db
.prepare(
`SELECT created_at FROM notes WHERE deleted_at IS NULL ORDER BY created_at ASC`
)
.all() as Array<{ created_at: string }>;
const dates = rows.map((r) => new Date(r.created_at));
if (dates.length === 0) {
return {
weekStart: kstMondayOf(this.now()),
weekCount: 0,
weekTarget: WEEK_TARGET,
consecutiveCompleteWeeks: 0,
showRecoveryToast: false,
lastNoteAt: null
};
}
const byWeek = new Map<string, number>();
for (const d of dates) {
const wk = kstMondayOf(d);
byWeek.set(wk, (byWeek.get(wk) ?? 0) + 1);
}
const currentWeek = kstMondayOf(this.now());
const weekCount = byWeek.get(currentWeek) ?? 0;
let consecutive = 0;
let cursor = weekCount >= WEEK_TARGET ? currentWeek : addDaysIso(currentWeek, -7);
while ((byWeek.get(cursor) ?? 0) >= WEEK_TARGET) {
consecutive += 1;
cursor = addDaysIso(cursor, -7);
}
const last = dates[dates.length - 1]!;
const todayKst = toKstDateKey(this.now());
const lastIsToday = toKstDateKey(last) === todayKst;
let showRecoveryToast = false;
if (lastIsToday && dates.length >= 2) {
const prev = dates[dates.length - 2]!;
const gapMs = last.getTime() - prev.getTime();
if (gapMs >= RECOVERY_GAP_DAYS * ONE_DAY_MS) showRecoveryToast = true;
}
return {
weekStart: currentWeek,
weekCount,
weekTarget: WEEK_TARGET,
consecutiveCompleteWeeks: consecutive,
showRecoveryToast,
lastNoteAt: last.toISOString()
};
}
}