feat(recall): #6 리마인드 1 spike — RecallBanner + telemetry (v0.2.3 7/7 final) #19
Reference in New Issue
Block a user
Delete Branch "feat/v023-recall-spike"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
v0.2.3 dogfood feedback roadmap §3 #6 cut — 마지막 항목 (7/7). Inbox ���단에 "오늘 회상해볼 노트" 1건 추천 배너 + 4종 telemetry. 머지 후 v0.2.3 cut 7/7 완료 → binary 빌드 단계.
Decisions (mini-brainstorm)
last_recalled_at ?? created_at기준 (algo trigger 와 동일 axis)자명 결정: Banner 위치 = ExpiryBanner 다음 / 0건 시 null return / "열어보기" = scrollIntoView / scroll target =
id="note-${id}".Changes
findRecallCandidate()(KST 보정 + 7일/30일/마감 + LIMIT 1) +markRecallOpened+dismissRecallRecallShownPayloadzod (.strict for privacy) + 4 union members + stats 누적 (열림율 + 평균 ageDays)recallCandidate+recallSnoozeUntilMs+recallShownIds+ 4 actionsid="note-${note.id}"(scrollIntoView target)Spec & plan
Test Plan
Roadmap
머지 후 v0.2.3 cut 7/7 완료 → v0.2.3 binary 빌드 + 핸드오프.
mini-brainstorm 2개 결정: - Q1=A: snooze in-memory (KST 다음 자정, ExpiryBanner 패턴 일관) - Q2=B: ageDays = last_recalled_at ?? created_at 기준 자명 결정: - Banner 위치: ExpiryBanner 다음 (stack 끝) - 0건 시 null return - "열어보기" 동작: scrollIntoView (NoteCard 항상 expanded) - scroll target: id="note-${id}" (ref 시스템 복잡도 회피) 핵심 invariants 6개 + privacy invariant + tests 17개 약속. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>- findRecallCandidate(): 7일+ 안 본 + 30일+ dismiss 만료 + ai='done' + 마감 안 임박 + LIMIT 1 - markRecallOpened(id, now): last_recalled_at 갱신 - dismissRecall(id, now): recall_dismissed_at 갱신 - KST 보정 SQL date('now','+9 hours') - 단위 +5 cases (empty/recent/old/dismiss expiry/exclude variants) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>- RecallShownPayload { noteId, ageDays: int>=0 } .strict() - recall_opened/dismissed/snoozed → NoteIdPayload 재사용 - TelemetryEventSchema union 15 → 19 - 단위 +3 cases (recall_shown valid, extra field 거부, opened/dismissed/snoozed valid) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>- DailyRow +4 cols (recall_shown/opened/dismissed/snoozed) - accumulators + 4 branches + recallAgeDaysSum - table 컬럼 +4 - summary lines: "- 회상 추천: shown N / opened O / dismissed D / snoozed S (열림율 X%)" "- 회상 평균 ageDays: avg" - TelemetryService.EmitInput union 15 → 19 - 단위 +2 cases Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>- RecallBanner: 노트 제목 + N일 전 + 3 버튼 (열어보기/다음에/더 이상) - 첫 렌더 시 emitRecallShown (recallShownIds Set 으로 per-session 1회 제약) - snoozeUntilMs 만료 setTimeout (ExpiryBanner 패턴) - 위치: ExpiryBanner 다음 (banner stack 끝) - NoteCard 외곽 div 에 id="note-${note.id}" — "열어보기" scrollIntoView target - 컬러 테마: 파랑 (#e8f0fe / #4a7ec0) — 다른 banner (적/황/적) 와 구별 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Round 1 review (controller-side)
i1 (Important) — recall_shown double-emit race (
RecallBanner.tsx:24-30)useEffect deps에
shownIds가 있어 setState 후 재트리거. React 18 batching 으로 평소엔 OK 지만 candidate 가 빠르게 새로 들어올 때 emit 2회 가능성.Fix: useState 대신 useRef 로
shownIds보유 — re-render 트리거 X, race 차단.m1 (Minor) — snoozeRecall candidate-null 시 emit 누락 (
store.ts:215-226)candidate=null 일 때 snooze 만 set 되고 emit skip. 의도적이지만 명시 코멘트 필요.
Fix: 1줄 코멘트 추가.
m2 (Minor) — snooze+dismiss 시 snoozeUntilMs 미clear (
store.ts:210-214)사용자가 "다음에" 후 다음 candidate 로 dismiss 시 이전 snooze 가 새 candidate 에도 적용. 의도 위반.
Fix:
dismissRecallNote가set({ recallSnoozeUntilMs: null })추가.m3 (Minor) — 미사용 local
before(CaptureService.ts:194)Fix: rename
before→_또는 inline check.m4 (Minor) — 빈 rawText + null aiTitle 시 빈 제목 (
RecallBanner.tsx:36)Fix:
'(제목 없음)'fallback 추가.n2 (Nit) — NoteCard id 의 load-bearing 의미 코멘트
Fix: 1줄 코멘트.
n1 / n3 (skip)
nextKstMidnightMs통합 영역Plan
i1+m1+m2+m3+m4+n2 fix 후 round 2 자체 verify. n1+n3 skip.
Verdict
APPROVE WITH FIX — 6개 inline 수정 후 round 2.
- i1 (Important): RecallBanner shownIds → useRef (state setState 트리거 race 차단) store 의 recallShownIds 필드 제거 (dead — useRef 가 대체) - m1 (Minor): snoozeRecall candidate-null race 코멘트 (의도적 emit skip 명시) - m2 (Minor): dismissRecallNote 후 recallSnoozeUntilMs = null clear - m3 (Minor): CaptureService.markRecallOpened 의 dead local 'before' inline check 로 제거 - m4 (Minor): RecallBanner title 빈 케이스 fallback '(제목 없음)' - n2 (Nit): NoteCard id load-bearing 의미 1줄 코멘트 skip: n1 (KST 4번째 inline duplicate — 프로젝트 전반 패턴, v0.2.4 nextKstMidnightMs 통합), n3 (ipcMain.on vs handle — 다른 IPC 와 패턴 일관) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Round 2 — APPROVE
61b6fa6before)skip 항목:
nextKstMidnightMsutil 통합 영역머지 후 알려줘. closure 단계 (local main sync + 브랜치 정리 + memory backlog + v0.2.3 cut 7/7 완료 표시 + binary 빌드 안내) 진행.