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:
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user