feat(store): selectedNotebookId 변경 시 list/counts 자동 refresh

- selectNotebook: set 후 loadByView(view) + refreshMeta() 자동 호출 (inbox/completed/trash view 한정 list 갱신, 모든 view 에서 counts 갱신)
- loadInitial / loadByView / refreshMeta: selectedNotebookId 를 listByStatus / countsByStatus 에 notebookId 옵션으로 전달
- tests: selectNotebook→loadByView+refreshMeta 호출 검증, notebookId 전달 검증, review view 에선 listByStatus 미호출 검증 (4케이스 추가)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-15 11:10:14 +09:00
parent 343624dceb
commit 862da4f15a
2 changed files with 56 additions and 7 deletions

View File

@@ -136,10 +136,11 @@ export const useInbox = create<InboxState>((set, get) => ({
// v0.3.8 — IPC 실패 시 loading=true 영구 stuck 방지. catch 로 reset.
set({ loading: true });
try {
const notebookId = get().selectedNotebookId ?? undefined;
const [notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, settings] = await Promise.all([
// inbox 탭은 status='active' 만 표시 — loadByView('inbox') 와 동일 path 로 일관성 확보.
// listNotes 는 deleted_at IS NULL 만 필터 (= active+completed+archived 혼재) 이라 부정확.
inboxApi.listByStatus('active', { limit: 50 }),
inboxApi.listByStatus('active', { limit: 50, notebookId }),
inboxApi.getContinuity(),
inboxApi.getPendingCount(),
inboxApi.getOllamaStatus(),
@@ -148,7 +149,7 @@ export const useInbox = create<InboxState>((set, get) => ({
inboxApi.listExpired(),
inboxApi.getFailedCount(),
inboxApi.listRecallCandidate(),
inboxApi.countsByStatus(),
inboxApi.countsByStatus({ notebookId }),
inboxApi.getSettings()
]);
set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true, loading: false });
@@ -161,6 +162,7 @@ export const useInbox = create<InboxState>((set, get) => ({
},
async refreshMeta() {
try {
const notebookId = get().selectedNotebookId ?? undefined;
const [continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, settings] = await Promise.all([
inboxApi.getContinuity(),
inboxApi.getPendingCount(),
@@ -170,7 +172,7 @@ export const useInbox = create<InboxState>((set, get) => ({
inboxApi.listExpired(),
inboxApi.getFailedCount(),
inboxApi.listRecallCandidate(),
inboxApi.countsByStatus(),
inboxApi.countsByStatus({ notebookId }),
inboxApi.getSettings()
]);
set({ continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true });
@@ -287,8 +289,9 @@ export const useInbox = create<InboxState>((set, get) => ({
// fail 시 빈 배열로 reset 해서 사용자에게 "비어있음" 으로 표시 (혼동 < stale).
const status =
view === 'trash' ? 'trashed' : view === 'inbox' ? 'active' : view;
const notebookId = get().selectedNotebookId ?? undefined;
try {
const notes = await inboxApi.listByStatus(status, { limit: 200 });
const notes = await inboxApi.listByStatus(status, { limit: 200, notebookId });
if (view === 'trash') {
set({ trashNotes: notes, trashCount: notes.length });
} else {
@@ -432,6 +435,12 @@ export const useInbox = create<InboxState>((set, get) => ({
},
selectNotebook(id) {
set({ selectedNotebookId: id });
// v0.4 Task 19 — notebook 전환 시 list + counts 자동 갱신.
const v = get().view;
if (v === 'inbox' || v === 'completed' || v === 'trash') {
void get().loadByView(v);
}
void get().refreshMeta();
},
async createNotebook(name, color) {
const r = await notebookApi.create({ name, color });

View File

@@ -1,5 +1,10 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
const { mockListByStatus, mockCountsByStatus } = vi.hoisted(() => ({
mockListByStatus: vi.fn(async () => []),
mockCountsByStatus: vi.fn(async () => ({ active: 0, completed: 0, archived: 0, trashed: 0 }))
}));
vi.mock('../../src/renderer/inbox/api.js', () => ({
inboxApi: {
listNotes: vi.fn(async () => []),
@@ -12,9 +17,9 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({
listExpired: vi.fn(async () => []),
getFailedCount: vi.fn(async () => 0),
listRecallCandidate: vi.fn(async () => null),
countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, archived: 0, trashed: 0 })),
countsByStatus: mockCountsByStatus,
getSettings: vi.fn(async () => ({ ai_enabled: true })),
listByStatus: vi.fn(async () => [])
listByStatus: mockListByStatus
},
notebookApi: {
list: vi.fn(async () => [
@@ -33,7 +38,9 @@ import { useInbox } from '../../src/renderer/inbox/store.js';
describe('store notebooks', () => {
beforeEach(() => {
useInbox.setState({ notebooks: [], selectedNotebookId: null, sidebarVisible: false, sidebarWidth: 240 } as never);
mockListByStatus.mockClear();
mockCountsByStatus.mockClear();
useInbox.setState({ notebooks: [], selectedNotebookId: null, sidebarVisible: false, sidebarWidth: 240, view: 'inbox' } as never);
});
it('loadNotebooks 가 notebookApi.list 결과 반영', async () => {
@@ -74,4 +81,37 @@ describe('store notebooks', () => {
expect(useInbox.getState().notebooks.map((n) => n.id)).toEqual(['nb-1']);
expect(useInbox.getState().selectedNotebookId).toBe('nb-1');
});
it('selectNotebook 가 loadByView + refreshMeta 호출', async () => {
// inbox view 에서 notebook 전환 → listByStatus + countsByStatus 호출됨.
useInbox.setState({ view: 'inbox', selectedNotebookId: null } as never);
useInbox.getState().selectNotebook('nb-X');
expect(useInbox.getState().selectedNotebookId).toBe('nb-X');
// 비동기 완료 대기
await new Promise((r) => setTimeout(r, 0));
expect(mockListByStatus).toHaveBeenCalled();
expect(mockCountsByStatus).toHaveBeenCalled();
});
it('loadByView 가 selectedNotebookId 를 inboxApi.listByStatus 에 전달', async () => {
useInbox.setState({ selectedNotebookId: 'nb-X', view: 'inbox' } as never);
await useInbox.getState().loadByView('inbox');
expect(mockListByStatus).toHaveBeenCalledWith('active', expect.objectContaining({ notebookId: 'nb-X' }));
});
it('refreshMeta 가 selectedNotebookId 를 inboxApi.countsByStatus 에 전달', async () => {
useInbox.setState({ selectedNotebookId: 'nb-X' } as never);
await useInbox.getState().refreshMeta();
expect(mockCountsByStatus).toHaveBeenCalledWith(expect.objectContaining({ notebookId: 'nb-X' }));
});
it('selectNotebook: review view 에선 loadByView 를 호출하지 않음', async () => {
useInbox.setState({ view: 'review-weekly', selectedNotebookId: null } as never);
useInbox.getState().selectNotebook('nb-X');
await new Promise((r) => setTimeout(r, 0));
// listByStatus 는 호출되지 않아야 함 (review view 는 list 대상 아님)
expect(mockListByStatus).not.toHaveBeenCalled();
// countsByStatus 는 refreshMeta 경로로 호출됨
expect(mockCountsByStatus).toHaveBeenCalled();
});
});