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>
86 lines
2.5 KiB
TypeScript
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()
|
|
};
|
|
}
|
|
}
|