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();
});