feat(expiry): inbox 만 대상 + 오늘 당일 포함 + 헤딩/라벨/메모 바로가기
dogfood: 마감 알림이 (1) 완료/보관 status 노트도 포함하고 (2) 오늘 당일
마감 메모는 빠져 있어 사용자 불편.
NoteRepository.findExpiredCandidates 변경:
- due_date < today → <=today (오늘 당일 포함)
- status='active' 필터 추가 (inbox 만, completed/archived/trashed 제외)
- ORDER BY due_date DESC → 오늘 → 어제 → 그저께 순
ExpiryBanner UX:
- 헤딩 분리 카운트 "오늘 마감 X · 지난 Y" (한 쪽만이면 단독 표시)
- 노트 옆 due_date → 상대 라벨 ([오늘] / [N일 지남]) + hover tooltip 으로
원본 ISO 날짜 노출
- 노트 제목 클릭 → note-{id} 로 smooth scroll (RecallBanner 와 동일 패턴).
checkbox 와 분리하기 위해 label → div + button 으로 구조 변경.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -592,6 +592,7 @@ describe('NoteRepository.findExpiredCandidates', () => {
|
||||
edited?: boolean;
|
||||
deletedAt?: string | null;
|
||||
aiStatus?: 'pending' | 'done' | 'failed';
|
||||
status?: 'active' | 'completed' | 'archived' | 'trashed';
|
||||
}): string {
|
||||
const { id } = repo.create({ rawText: opts.rawText });
|
||||
db.prepare(
|
||||
@@ -599,19 +600,21 @@ describe('NoteRepository.findExpiredCandidates', () => {
|
||||
SET due_date = ?,
|
||||
due_date_edited_by_user = ?,
|
||||
ai_status = ?,
|
||||
deleted_at = ?
|
||||
deleted_at = ?,
|
||||
status = ?
|
||||
WHERE id = ?`
|
||||
).run(
|
||||
opts.dueDate,
|
||||
opts.edited ? 1 : 0,
|
||||
opts.aiStatus ?? 'done',
|
||||
opts.deletedAt ?? null,
|
||||
opts.status ?? 'active',
|
||||
id
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
it('returns notes with due_date < today (KST), ORDER BY created_at DESC', () => {
|
||||
it('returns notes with due_date <= today (KST), ORDER BY due_date DESC then created_at DESC', () => {
|
||||
const a = makeDone({ rawText: 'a', dueDate: '2026-04-20' });
|
||||
db.prepare(`UPDATE notes SET created_at = ? WHERE id = ?`).run('2026-04-30T10:00:00Z', a);
|
||||
const b = makeDone({ rawText: 'b', dueDate: '2026-04-25' });
|
||||
@@ -620,6 +623,14 @@ describe('NoteRepository.findExpiredCandidates', () => {
|
||||
expect(r.map((n) => n.id)).toEqual([b, a]);
|
||||
});
|
||||
|
||||
it('includes notes with due_date == today (오늘 당일 우선 표시)', () => {
|
||||
const past = makeDone({ rawText: 'a', dueDate: '2026-04-30' });
|
||||
const todayNote = makeDone({ rawText: 'b', dueDate: '2026-05-01' });
|
||||
const r = repo.findExpiredCandidates(new Date('2026-05-01T12:00:00Z'));
|
||||
// 오늘 당일이 먼저, 그 다음 지난 메모.
|
||||
expect(r.map((n) => n.id)).toEqual([todayNote, past]);
|
||||
});
|
||||
|
||||
it('includes both AI-extracted and user-edited due_date (Q1=B 회귀 가드)', () => {
|
||||
const ai = makeDone({ rawText: 'a', dueDate: '2026-04-20', edited: false });
|
||||
const manual = makeDone({ rawText: 'b', dueDate: '2026-04-22', edited: true });
|
||||
@@ -629,7 +640,7 @@ describe('NoteRepository.findExpiredCandidates', () => {
|
||||
|
||||
it('excludes trashed notes (deleted_at IS NOT NULL)', () => {
|
||||
const a = makeDone({ rawText: 'a', dueDate: '2026-04-20' });
|
||||
makeDone({ rawText: 'b', dueDate: '2026-04-21', deletedAt: '2026-04-30T00:00:00Z' });
|
||||
makeDone({ rawText: 'b', dueDate: '2026-04-21', deletedAt: '2026-04-30T00:00:00Z', status: 'trashed' });
|
||||
const r = repo.findExpiredCandidates(new Date('2026-05-01T12:00:00Z'));
|
||||
expect(r.map((n) => n.id)).toEqual([a]);
|
||||
});
|
||||
@@ -649,11 +660,12 @@ describe('NoteRepository.findExpiredCandidates', () => {
|
||||
expect(r.map((n) => n.id)).toEqual([dated]);
|
||||
});
|
||||
|
||||
it('excludes notes with due_date == today (boundary, not expired)', () => {
|
||||
const past = makeDone({ rawText: 'a', dueDate: '2026-04-30' });
|
||||
makeDone({ rawText: 'b', dueDate: '2026-05-01' });
|
||||
it('excludes completed / archived notes (inbox 만 — 사용자 의도: 완료/보관은 알림 제외)', () => {
|
||||
const active = makeDone({ rawText: 'a', dueDate: '2026-04-20' });
|
||||
makeDone({ rawText: 'b', dueDate: '2026-04-20', status: 'completed' });
|
||||
makeDone({ rawText: 'c', dueDate: '2026-04-20', status: 'archived' });
|
||||
const r = repo.findExpiredCandidates(new Date('2026-05-01T12:00:00Z'));
|
||||
expect(r.map((n) => n.id)).toEqual([past]);
|
||||
expect(r.map((n) => n.id)).toEqual([active]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user