feat(ipc): inboxApi list/search/counts 에 notebookId 옵션 + counts archived 제거

- 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) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-15 10:28:49 +09:00
parent a0e6bc53b2
commit b9fec25b9d
5 changed files with 33 additions and 21 deletions

View File

@@ -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)
);

View File

@@ -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.

View File

@@ -120,7 +120,8 @@ export const useInbox = create<InboxState>((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<InboxState>((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);

View File

@@ -137,7 +137,7 @@ export interface AutostartResponse {
}
export interface InboxApi {
listNotes(opts: { limit: number; cursor?: string }): Promise<Note[]>;
listNotes(opts: { limit: number; cursor?: string; notebookId?: string }): Promise<Note[]>;
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<Note[]>;
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<Note[]>;
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<NoteRevision[]>;
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<Note[]>;
// v0.4 — notebookId 옵션 추가.
search(query: string, opts?: { limit?: number; status?: NoteStatus; notebookId?: string }): Promise<Note[]>;
reviewAggregate(period: ReviewPeriod): Promise<ReviewAggregate>;
// v0.3.0 Cut E — 양방향 sync.
configureSync(url: string | null): Promise<{ ok: true } | { ok: false; reason: string }>;

View File

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