From b9fec25b9d4a2ef57853a33d652e0c409bc68fe7 Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Fri, 15 May 2026 10:28:49 +0900 Subject: [PATCH] =?UTF-8?q?feat(ipc):=20inboxApi=20list/search/counts=20?= =?UTF-8?q?=EC=97=90=20notebookId=20=EC=98=B5=EC=85=98=20+=20counts=20arch?= =?UTF-8?q?ived=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InboxApi.listNotes / listByStatus / search signature 에 notebookId? 옵션 추가 - countsByStatus 반환 타입에서 archived 제거 (active/completed/trashed 만) - inbox:list-by-status 핸들러: archived 수신 시 빈 배열 graceful fallback - inbox:counts-by-status 핸들러: notebookId opts 추가, archived 키 제거 - store.ts: countsByStatus 결과 spread 시 archived:0 fallback (Task 15/16 까지 UI 보존) - App.test.tsx: countsByStatus mock 에서 archived 제거 + 탭 count 기대값 보관(0) 으로 조정 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/ipc/inboxApi.ts | 24 ++++++++++++++---------- src/preload/index.ts | 6 ++++-- src/renderer/inbox/store.ts | 6 ++++-- src/shared/types.ts | 10 ++++++---- tests/unit/App.test.tsx | 8 +++++--- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/main/ipc/inboxApi.ts b/src/main/ipc/inboxApi.ts index 43c464f..98c99b4 100644 --- a/src/main/ipc/inboxApi.ts +++ b/src/main/ipc/inboxApi.ts @@ -32,7 +32,7 @@ export interface InboxIpcDeps { } export function registerInboxApi(deps: InboxIpcDeps): void { - ipcMain.handle('inbox:list', (_e, opts: { limit: number; cursor?: string }) => + ipcMain.handle('inbox:list', (_e, opts: { limit: number; cursor?: string; notebookId?: string }) => deps.repo.list(opts) ); @@ -197,21 +197,24 @@ export function registerInboxApi(deps: InboxIpcDeps): void { }); // v0.2.9 Cut B Task 4 — status 별 노트 목록. + // v0.4 — notebookId 옵션 추가. archived 는 v0.4 에서 제거된 status — graceful fallback. ipcMain.handle( 'inbox:list-by-status', - (_e, status: NoteStatus, opts: { limit?: number } = {}) => { - const VALID: readonly NoteStatus[] = ['active', 'completed', 'archived', 'trashed']; + (_e, status: NoteStatus, opts: { limit?: number; notebookId?: string } = {}) => { + // archived 는 v0.4 에서 completed 로 통합 — 빈 결과로 graceful fallback (caller 안전). + if ((status as string) === 'archived') return [] as Note[]; + const VALID: readonly NoteStatus[] = ['active', 'completed', 'trashed']; if (!VALID.includes(status)) return [] as Note[]; return deps.repo.listByStatus(status, opts); } ); - // v0.2.9 Cut B Task 4 — 4 status counts (헤더 4탭 badge). - ipcMain.handle('inbox:counts-by-status', () => ({ - active: deps.repo.countByStatus('active'), - completed: deps.repo.countByStatus('completed'), - archived: deps.repo.countByStatus('archived'), - trashed: deps.repo.countByStatus('trashed') + // v0.2.9 Cut B Task 4 — status counts (헤더 탭 badge). + // v0.4 — archived 제거 (3 status 만 반환). notebookId 옵션 추가. + ipcMain.handle('inbox:counts-by-status', (_e, opts: { notebookId?: string } = {}) => ({ + active: deps.repo.countByStatus('active', opts), + completed: deps.repo.countByStatus('completed', opts), + trashed: deps.repo.countByStatus('trashed', opts) })); // v0.2.9 Cut B Task 8 — status 4분기 직접 전이 (사유 포함). @@ -320,9 +323,10 @@ export function registerInboxApi(deps: InboxIpcDeps): void { }); // v0.2.11 Cut D — FTS5 검색 + 회고 aggregate. + // v0.4 — notebookId 옵션 추가. ipcMain.handle( 'inbox:search', - (_e, query: string, opts: { limit?: number; status?: NoteStatus } = {}) => + (_e, query: string, opts: { limit?: number; status?: NoteStatus; notebookId?: string } = {}) => deps.repo.search(query, opts) ); diff --git a/src/preload/index.ts b/src/preload/index.ts index 8a8cbb8..a09fcde 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -8,7 +8,7 @@ const api: InklingApi = { hide: () => ipcRenderer.send('capture:hide') }, inbox: { - listNotes: (opts) => ipcRenderer.invoke('inbox:list', opts), + listNotes: (opts: { limit: number; cursor?: string; notebookId?: string }) => ipcRenderer.invoke('inbox:list', opts), updateAiFields: (noteId, fields) => ipcRenderer.invoke('inbox:updateAi', { noteId, fields }), setDueDate: (noteId, date) => ipcRenderer.invoke('inbox:setDueDate', { noteId, date }), @@ -71,8 +71,9 @@ const api: InklingApi = { // v0.2.8 Cut A — 첨부 이미지를 OS 기본 뷰어로 열기 (Task 3). openMedia: (relPath: string) => ipcRenderer.invoke('inbox:open-media', relPath), // v0.2.9 Cut B Task 4 — status 별 list + counts. + // v0.4 — notebookId 옵션 pass-through. countsByStatus opts 추가. listByStatus: (status, opts) => ipcRenderer.invoke('inbox:list-by-status', status, opts ?? {}), - countsByStatus: () => ipcRenderer.invoke('inbox:counts-by-status'), + countsByStatus: (opts?: { notebookId?: string }) => ipcRenderer.invoke('inbox:counts-by-status', opts ?? {}), // v0.2.9 Cut B Task 8 — 4분기 status 전이 + AI 자동 분류 추천. setStatus: (id, status, reason) => ipcRenderer.invoke('inbox:set-status', id, status, reason), classifyStatus: (id, reason) => ipcRenderer.invoke('ai:classify-status', id, reason), @@ -88,6 +89,7 @@ const api: InklingApi = { listRevisions: (noteId: string) => ipcRenderer.invoke('inbox:list-revisions', noteId), restoreRevision: (noteId: string, revId: number) => ipcRenderer.invoke('inbox:restore-revision', noteId, revId), // v0.2.11 Cut D — search + 회고 aggregate. + // v0.4 — notebookId 옵션 pass-through. search: (query, opts) => ipcRenderer.invoke('inbox:search', query, opts ?? {}), reviewAggregate: (period) => ipcRenderer.invoke('inbox:review-aggregate', period), // v0.3.0 Cut E — 양방향 sync. diff --git a/src/renderer/inbox/store.ts b/src/renderer/inbox/store.ts index 78ff106..d2fda2e 100644 --- a/src/renderer/inbox/store.ts +++ b/src/renderer/inbox/store.ts @@ -120,7 +120,8 @@ export const useInbox = create((set, get) => ({ inboxApi.countsByStatus(), inboxApi.getSettings() ]); - set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true, loading: false }); + // v0.4 — countsByStatus 응답에서 archived 제거됨 (Task 16 UI 정리 예정). 0 fallback 유지. + set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts: { ...counts, archived: 0 }, ai_enabled: settings.ai_enabled ?? true, loading: false }); } catch (e) { // 첫 launch 의 IPC 실패 (DB migration 실패 / main process 비정상) 시 무한 loading 회피. // 빈 데이터로 진입하면 사용자가 캡처 시도 → 실제 fail 이 표면화 → 재시도 가능. @@ -142,7 +143,8 @@ export const useInbox = create((set, get) => ({ inboxApi.countsByStatus(), inboxApi.getSettings() ]); - set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true }); + // v0.4 — countsByStatus 응답에서 archived 제거됨 (Task 16 UI 정리 예정). 0 fallback 유지. + set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts: { ...counts, archived: 0 }, ai_enabled: settings.ai_enabled ?? true }); } catch (e) { // refreshMeta 는 background poll/event 에서 자주 호출 → fail 무시 (다음 호출에 회복). console.error('[inbox] refreshMeta failed', e); diff --git a/src/shared/types.ts b/src/shared/types.ts index dbe4687..878a312 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -137,7 +137,7 @@ export interface AutostartResponse { } export interface InboxApi { - listNotes(opts: { limit: number; cursor?: string }): Promise; + listNotes(opts: { limit: number; cursor?: string; notebookId?: string }): Promise; updateAiFields( noteId: string, fields: { title?: string; summary?: string; tags?: string[] } @@ -197,8 +197,9 @@ export interface InboxApi { // v0.2.8 Cut A — 첨부 이미지를 OS 기본 뷰어로 열기 (Task 3). openMedia(relPath: string): Promise<{ ok: true } | { ok: false; reason: string }>; // v0.2.9 Cut B Task 4 — status 별 노트 목록 + status 별 count. - listByStatus(status: NoteStatus, opts?: { limit?: number }): Promise; - countsByStatus(): Promise<{ active: number; completed: number; archived: number; trashed: number }>; + // v0.4 — notebookId 옵션 추가. archived 는 counts 응답에서 제거 (Task 16 UI 정리 예정). + listByStatus(status: NoteStatus, opts?: { limit?: number; notebookId?: string }): Promise; + countsByStatus(opts?: { notebookId?: string }): Promise<{ active: number; completed: number; trashed: number }>; // v0.2.9 Cut B Task 8 — 4분기 status 전이 + AI 자동 분류 추천. setStatus( id: string, @@ -229,7 +230,8 @@ export interface InboxApi { listRevisions(noteId: string): Promise; restoreRevision(noteId: string, revId: number): Promise<{ ok: true } | { ok: false; reason: string }>; // v0.2.11 Cut D — FTS5 search + 회고 aggregate. - search(query: string, opts?: { limit?: number; status?: NoteStatus }): Promise; + // v0.4 — notebookId 옵션 추가. + search(query: string, opts?: { limit?: number; status?: NoteStatus; notebookId?: string }): Promise; reviewAggregate(period: ReviewPeriod): Promise; // v0.3.0 Cut E — 양방향 sync. configureSync(url: string | null): Promise<{ ok: true } | { ok: false; reason: string }>; diff --git a/tests/unit/App.test.tsx b/tests/unit/App.test.tsx index 312b2af..cc9bed1 100644 --- a/tests/unit/App.test.tsx +++ b/tests/unit/App.test.tsx @@ -7,7 +7,7 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({ inboxApi: { listNotes: vi.fn(async () => []), listByStatus: vi.fn(async () => []), - countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, archived: 0, trashed: 0 })), + countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, trashed: 0 })), getContinuity: vi.fn(async () => ({ weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null @@ -125,14 +125,16 @@ describe('App header — 4 tabs', () => { }); // loadInitial 이 비동기로 counts 를 덮어씀 — onboarding wizard async gate (Task 12) 도입 // 후 render 가 await 후 발생하므로 mock 의 countsByStatus 가 테스트 기대값을 반환하도록 갱신. - vi.mocked(inboxApi.countsByStatus).mockResolvedValue({ active: 5, completed: 3, archived: 2, trashed: 1 }); + // v0.4 — countsByStatus 응답에서 archived 제거 (store 가 archived:0 fallback 추가). + vi.mocked(inboxApi.countsByStatus).mockResolvedValue({ active: 5, completed: 3, trashed: 1 }); }); it('renders 4 tabs with counts', async () => { render(); expect(await screen.findByRole('tab', { name: /Inbox\(5\)/ })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: /완료\(3\)/ })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: /보관\(2\)/ })).toBeInTheDocument(); + // v0.4 — archived count 는 IPC 응답에서 제거됨 → store 가 0 fallback. 보관 탭은 Task 15 에서 제거 예정. + expect(screen.getByRole('tab', { name: /보관\(0\)/ })).toBeInTheDocument(); expect(screen.getByRole('tab', { name: /휴지통\(1\)/ })).toBeInTheDocument(); });