v0.3.4 까지 누적된 dogfood UX 결함 hotfix.
사용자 직접 보고 3건 (inbox 재진입, 회고 탈출, 이동 modal 중복) + 동반 갭 4건
(count stale, 필터 잔류, 초기 로드 불일치, 배너 컨텍스트 누수).
데이터/마이그레이션 변경 없음 (스키마 v8).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #33 회차 1 review 반영. 두 파일 sibling 인데 anchor union 두 곳 정의 →
import type 1줄로 단일 source of truth. 향후 anchor 추가 시 두 파일 동시
수정 누락 risk + TS 가 catch 하지 않던 silent drift risk 제거.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
리뷰 minor 반영. busy (저장/테스트/sync 진행) 시 정확히 사용자가
도움말이 가장 필요한 시점이라 disable 회수. read-only 컴포넌트라
race risk 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
final code review (Opus) 발견 2 important issues:
1. SyncConflict.noteId 가 실제로 export filename slug (date-id8-slug) 였음 — UUID 가
아니라 git checkout path 의 stem. 명명 혼동 → 'path' 로 rename (실제 의미와 일치).
2. ConflictModal preview 가 항상 빈 문자열이라 사용자가 비교 없이 local/remote 선택해야
했음. runSync 의 conflict 분기에서 `git show :2:<path>` (ours) + `:3:<path>`
(theirs) 호출 추가하여 localText/remoteText 채움.
영향:
- SyncService.SyncConflict + shared/types.ts.SyncConflict: noteId → path
- SyncService.resolveConflict(path, choice) — 'notes/...md' 그대로 받음
- pathToNoteId 헬퍼 제거 (불필요)
- ConflictModal: c.noteId → c.path, busy 상태 + 표시 모두 path 키
- IPC handler / preload bridge / InboxApi 시그니처 모두 path 로 통일
- SyncService.bidirectional/resolveConflict/sync-ipc/ConflictModal 4 test 갱신
regression 회귀 패턴 검사: rename 후 NoteRepository / SyncService / IPC / UI 의 모든
conflict-related path 일관 (typecheck 0).
Cut B Task 6 — 모든 view 공통 "이동 ▾" dropdown.
- 기존 휴지통/삭제 버튼 위치에 dropdown 추가 (모든 mode 공통)
- 현재 status 외 3개 목적지만 표시 (active 노트 → 완료/보관/휴지통)
- 메뉴 항목 클릭 → MoveStatusModal(initialTarget) 열기
- onMoved → local 상태 갱신 + onUpdated + (status 변경 시) onDeleted (list 제거)
- trash mode 의 영구 삭제/복구 버튼은 보존 (휴지통 단독 액션)
- 사용되지 않게 된 handleDelete 제거 (deleteNote 는 capture path 만)
- NoteCard 메뉴 단위 테스트 2건 (메뉴 표시 / 클릭 → modal → setStatus)
- 회색 placeholder div → <img src=inkling-media://...> 로 교체
- onClick 으로 inboxApi.openMedia(relPath) 호출 (현재는 InboxApi 인터페이스에 부재 → unknown cast 사용; Task 3 에서 정식 시그니처 추가 후 cast 제거 예정)
- alt='' 로 decorative 처리 (role=presentation), title 에 relPath 유지
- flex-wrap 추가 — 다수 이미지 시 줄바꿈
Tests: tests/unit/NoteCard.test.tsx 신규 2건 (img src 검증, click → openMedia 호출)
회귀: 468 → 470 pass
- types.ts:119 stale "Task 25 cleanup" comment 제거 (Task 25 이미 완료)
- BackupSection.tsx 의 dead try/catch 제거 + status 단순화 — 모든 IPC 핸들러가 자체 try/catch + Notification 으로 결과 알림. 컴포넌트 status 는 진행 표시 보조용
- index.ts startup #45 autostart 진단 로그를 AutostartDiagnostic.collectAutostartState() 호출로 통합 — single source of truth (SettingsPage 진단 패널과 동일 데이터 소스)
460/460 pass, typecheck 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v027 plan Task 7. zustand store 의 showSettings 를 사용하는 첫 컴포넌트.
4 섹션 (AI 제공자/자동 실행/백업·복원/정보) placeholder 와 헤더 + 돌아가기 버튼만.
실 콘텐츠는 후속 Task 8-11 에서 채움.
테스트 인프라 동시 추가 (v027 의 첫 React 컴포넌트 테스트):
- @testing-library/react + @testing-library/jest-dom + jsdom devDep 추가
- vitest.config: plugin-react 적용, include 에 .test.tsx 포함
- 환경 분리는 per-file `// @vitest-environment jsdom` directive 로 처리
(vitest 4.x 에서 environmentMatchGlobs 미지원 — 기존 .ts 단위 테스트는 node env 유지)
- onDeleted: () => void → onDeleted?: () => void (inbox mode 전용 명시)
- handleDelete 내부 onDeleted() → onDeleted?.()
- App.tsx 의 trash mode NoteCard 가 onDeleted prop 미전달 (dead-code 제거)
- API 시그니처 정리 — trash mode 는 onPermanentDelete/onRestore 만 의미
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>
m1 — HealthChecker.last={ok:true} sentinel 의도 주석 (line 17).
첫 healthy=ok=true 면 transition 으로 인식 안 됨, ok=false 면 unreachable
transition 으로 정상 인식. telemetry 누락 0.
m2 — runOnce in-flight guard 추가. polling 첫 호출이 늦게 끝나는 동안
setInterval 가 두 번째 호출 시작하면 같은 promise 반환. healthCheck 가
idempotent HTTP 라 race 안전하지만, 이중 onUpdate/telemetry emit 회피.
m3 — main.ts before-quit 핸들러 통합. trayInterval cleanup 별도 핸들러
(line 349) 제거하고 health.stop() 핸들러 안에 흡수. 모든 cleanup 한 곳.
n1 — OllamaBanner 재확인 button 의 onClick 에 .catch 추가.
recheckOllama Promise rejection 시 console.warn (silent swallow 회피).
n2 — App.tsx useEffect deps array 의도 주석 1줄. onOllamaStatus 콜백이
useInbox.setState 직접 호출 — store reference 안정적이라 deps 불필요.
n3 — HealthChecker idempotent test 강화. <=2 → ===2 (정확).
두 timer 등록되면 4 (각 timer 마다 즉시+1s) 가 됨.
n4 — runOnce 의 manual emit 이 healthCheck *전에* fire 인 의도 주석.
provider 실패 시에도 manual 카운트 1:1 보장.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
m1 — spec §5.3 dialog 버튼 순서를 impl 패턴 (`['옮기기','취소'], defaultId=1, cancelId=1`) 으로 보정. project 의 permanentDelete/emptyTrash 와 일관 (위험 액션은 default focus = 취소).
m2 — telemetryEvents.test.ts 에 `expired_batch_trash` 의 extra-field 회귀 가드 추가. `expired_banner_shown` 과 대칭 (privacy invariant).
m3 — ExpiryBanner.InnerProps.candidates 타입을 narrow subset → `Note` 로 통일. v0.2.4 에서 Note 타입 진화 시 silent drift 방지.
m4 — onTrash 의 `void trashExpiredBatch(ids)` → `.catch((e) => console.warn(...))` 로 Promise rejection 가시화. (project-wide error toast 도입은 v0.2.4 backlog 유지)
n1 — 24h+ 앱 켜둔 상태에서 snooze 자동 만료. `setTimeout(snoozeUntilMs - now)` 으로 자정 KST 시점에 force re-render. (refreshMeta trigger 의존 제거)
n2 — CaptureService.listExpired 의 dedup signature reset on empty 의도 주석 1줄. future maintainer 위해.
n3 (`as any[]`) 은 repo 전체 hydrate 패턴 — 단독 fix 시 inconsistency. v0.2.4 backlog #22 로 합산.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
F4-C·F (cue 강화) — Inkling 의 두 표면에 정체성 신호를 추가.
F4-C 환경 앵커 (tray):
- nativeImage 색 변경은 미지원이므로 색 뱃지 대신 tooltip + 메뉴 첫
비활성 라벨로 대체. tooltip 은 항상 `Inkling — 오늘 N`,
메뉴 첫 항목은 N>0 일 때 `오늘 N번 잡아둠` (비활성).
- `tray.ts` 가 `refreshTray(todayCount)` 를 export 하여 main 이
60s interval + AiWorker.onUpdate hook 에서 갱신을 트리거.
- N=0 일 때는 라벨을 띄우지 않아 메뉴가 자연스럽게 시작.
F4-F 정체성 고리 (Inbox 헤더):
- ContinuityBadge 옆에 새 IdentityCounter 컴포넌트.
- N>0 → `오늘 N번 잡아뒀다` (정체성 강화 카피).
- N=0 → `오늘은 처음 한 줄?` (priming 카피로 첫 캡처 유도).
- 갱신은 `loadInitial` / `refreshMeta` (focus + note:updated) 경로
공유 — 별도 IPC subscription 없음.
Wiring:
- `NoteRepository.countToday()` 를 `inbox:todayCount` IPC 로 노출.
- preload bridge `getTodayCount`, `InboxApi.getTodayCount()` 타입.
- 스토어에 `todayCount: number` 필드 추가, 두 메타 fetch 경로 모두에서 갱신.
스키마 변경 없음. 197/197 unit pass, 1/1 e2e pass.
Splits the tag chip into two actions per F2 dogfood feedback:
- short click on chip text → applies the tag to the inbox filter
(Inbox header shows "🔎 필터: #tag (n개)" banner with ✕ 해제 button)
- × button on chip → immediately removes the tag and surfaces a
module-level pub/sub undo toast for 5 seconds; clicking the toast
restores the tag
`TagUndoToast` is a tiny self-contained component: `pushTagUndo()` from
NoteCard publishes an entry, the mounted `<TagUndoToast />` near the
end of `<App>` subscribes and renders it. Auto-dismiss after 5 s,
click-to-undo cancels the timer and runs the restore callback.
AI vs user tags share the same behaviour — only the chip background
colour distinguishes them, matching the F2 decision table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>