feat(v0211): store — search + reviewData state + actions + view enum 확장
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Note, WeeklyContinuity } from '@shared/types';
|
||||
import type { Note, ReviewAggregate, WeeklyContinuity } from '@shared/types';
|
||||
import { inboxApi } from './api.js';
|
||||
import { nextKstMidnightMs } from '@shared/util/kstDate.js';
|
||||
|
||||
@@ -7,7 +7,9 @@ export { selectFilteredNotes } from './selectFilteredNotes.js';
|
||||
|
||||
// v0.2.9 Cut B Task 4 — 4탭 view enum + settings.
|
||||
// 'inbox' = active, 'completed'/'archived' = NoteStatus 그대로, 'trash' = trashed (mirror), 'settings' = SettingsPage.
|
||||
export type InboxView = 'inbox' | 'completed' | 'archived' | 'trash' | 'settings';
|
||||
export type InboxView =
|
||||
| 'inbox' | 'completed' | 'archived' | 'trash' | 'settings'
|
||||
| 'review-daily' | 'review-weekly' | 'review-monthly';
|
||||
|
||||
export interface InboxCounts {
|
||||
active: number;
|
||||
@@ -39,6 +41,10 @@ interface InboxState {
|
||||
// v0.2.9 Cut B Task 14 — AI 비활성 모드에서는 OllamaBanner/FailedBanner render skip.
|
||||
// 기본 true (기존 사용자 무영향). loadInitial / refreshMeta 가 settings 로드.
|
||||
ai_enabled: boolean;
|
||||
// v0.2.11 Cut D — FTS5 search + review aggregate state.
|
||||
searchQuery: string;
|
||||
searchResults: Note[] | null; // null = 검색 안 한 상태
|
||||
reviewData: ReviewAggregate | null;
|
||||
loadInitial: () => Promise<void>;
|
||||
refreshMeta: () => Promise<void>;
|
||||
upsertNote: (note: Note) => void;
|
||||
@@ -61,6 +67,11 @@ interface InboxState {
|
||||
openRecall: (id: string) => Promise<void>;
|
||||
dismissRecallNote: (id: string) => Promise<void>;
|
||||
snoozeRecall: () => Promise<void>;
|
||||
// v0.2.11 Cut D — search + review actions.
|
||||
setSearchQuery: (q: string) => void;
|
||||
searchNotes: (q: string) => Promise<void>;
|
||||
clearSearch: () => void;
|
||||
loadReview: (period: 'daily' | 'weekly' | 'monthly') => Promise<void>;
|
||||
}
|
||||
|
||||
const emptyContinuity: WeeklyContinuity = {
|
||||
@@ -88,6 +99,9 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
recallCandidate: null,
|
||||
recallSnoozeUntilMs: null,
|
||||
ai_enabled: true,
|
||||
searchQuery: '',
|
||||
searchResults: null,
|
||||
reviewData: null,
|
||||
async loadInitial() {
|
||||
set({ loading: true });
|
||||
const [notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, settings] = await Promise.all([
|
||||
@@ -178,6 +192,10 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
if (view === 'completed' || view === 'archived' || view === 'trash') {
|
||||
void get().loadByView(view);
|
||||
}
|
||||
// v0.2.11 Cut D — review-* view 진입 시 aggregate 로드.
|
||||
if (view === 'review-daily') void get().loadReview('daily');
|
||||
if (view === 'review-weekly') void get().loadReview('weekly');
|
||||
if (view === 'review-monthly') void get().loadReview('monthly');
|
||||
},
|
||||
async loadByView(view) {
|
||||
const status = view === 'trash' ? 'trashed' : view;
|
||||
@@ -269,5 +287,30 @@ export const useInbox = create<InboxState>((set, get) => ({
|
||||
if (candidate) {
|
||||
await inboxApi.emitRecallSnoozed(candidate.id);
|
||||
}
|
||||
},
|
||||
// v0.2.11 Cut D — FTS5 search + review aggregate actions.
|
||||
setSearchQuery(q) {
|
||||
set({ searchQuery: q });
|
||||
if (q.trim().length === 0) set({ searchResults: null });
|
||||
},
|
||||
async searchNotes(q) {
|
||||
if (q.trim().length === 0) {
|
||||
set({ searchResults: null });
|
||||
return;
|
||||
}
|
||||
const view = get().view;
|
||||
// 회고/설정 view 일 때는 status filter 무의미 → 그대로 전체 검색
|
||||
const status = view === 'completed' || view === 'archived' || view === 'trash'
|
||||
? (view === 'trash' ? 'trashed' : view)
|
||||
: view === 'inbox' ? 'active' : undefined;
|
||||
const r = await inboxApi.search(q, status ? { status } : {});
|
||||
set({ searchResults: r });
|
||||
},
|
||||
clearSearch() {
|
||||
set({ searchQuery: '', searchResults: null });
|
||||
},
|
||||
async loadReview(period) {
|
||||
const data = await inboxApi.reviewAggregate(period);
|
||||
set({ reviewData: data });
|
||||
}
|
||||
}));
|
||||
|
||||
50
tests/unit/store.search.test.ts
Normal file
50
tests/unit/store.search.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('../../src/renderer/inbox/api.js', () => ({
|
||||
inboxApi: {
|
||||
search: vi.fn(),
|
||||
reviewAggregate: vi.fn(),
|
||||
listNotes: vi.fn(() => []),
|
||||
getContinuity: vi.fn(() => ({ weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null })),
|
||||
getPendingCount: vi.fn(() => 0),
|
||||
getOllamaStatus: vi.fn(() => ({ ok: true })),
|
||||
getTodayCount: vi.fn(() => 0),
|
||||
getTrashCount: vi.fn(() => 0),
|
||||
listExpired: vi.fn(() => []),
|
||||
getFailedCount: vi.fn(() => 0),
|
||||
listRecallCandidate: vi.fn(() => null),
|
||||
countsByStatus: vi.fn(() => ({ active: 0, completed: 0, archived: 0, trashed: 0 })),
|
||||
getSettings: vi.fn(() => ({ ai_enabled: true })),
|
||||
listByStatus: vi.fn(() => [])
|
||||
}
|
||||
}));
|
||||
|
||||
import { useInbox } from '../../src/renderer/inbox/store';
|
||||
import { inboxApi } from '../../src/renderer/inbox/api.js';
|
||||
|
||||
describe('store — searchNotes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useInbox.setState({ searchQuery: '', searchResults: null, view: 'inbox' });
|
||||
});
|
||||
|
||||
it('빈 query → searchResults null + IPC 미호출', async () => {
|
||||
await useInbox.getState().searchNotes(' ');
|
||||
expect(useInbox.getState().searchResults).toBeNull();
|
||||
expect(inboxApi.search).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keyword query → IPC 호출 + searchResults set', async () => {
|
||||
(inboxApi.search as ReturnType<typeof vi.fn>).mockResolvedValue([{ id: 'a' }]);
|
||||
await useInbox.getState().searchNotes('회의');
|
||||
expect(inboxApi.search).toHaveBeenCalledWith('회의', { status: 'active' });
|
||||
expect(useInbox.getState().searchResults).toEqual([{ id: 'a' }]);
|
||||
});
|
||||
|
||||
it('clearSearch — query + results 모두 초기화', () => {
|
||||
useInbox.setState({ searchQuery: '회의', searchResults: [{ id: 'a' } as never] });
|
||||
useInbox.getState().clearSearch();
|
||||
expect(useInbox.getState().searchQuery).toBe('');
|
||||
expect(useInbox.getState().searchResults).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user