diff --git a/src/renderer/inbox/components/SearchBox.tsx b/src/renderer/inbox/components/SearchBox.tsx
index 2de58a8..bfebff8 100644
--- a/src/renderer/inbox/components/SearchBox.tsx
+++ b/src/renderer/inbox/components/SearchBox.tsx
@@ -3,32 +3,50 @@ import { useInbox } from '../store.js';
export function SearchBox(): React.ReactElement {
const [draft, setDraft] = useState('');
+ const [scope, setScope] = useState<'current' | 'all'>('current');
+ const selectedNotebookId = useInbox((s) => s.selectedNotebookId);
useEffect(() => {
const handle = setTimeout(() => {
const trimmed = draft.trim();
- if (trimmed.length === 0) useInbox.getState().clearSearch();
- else void useInbox.getState().searchNotes(trimmed);
+ if (trimmed.length === 0) {
+ useInbox.getState().clearSearch();
+ } else {
+ // v0.4 Task 18 — scope='current' 이면 현재 notebook ID 전달, 'all' 이면 미전달(전체 검색).
+ const notebookId = scope === 'current' ? (selectedNotebookId ?? undefined) : undefined;
+ void useInbox.getState().searchNotes(trimmed, { notebookId });
+ }
}, 200);
return () => clearTimeout(handle);
- }, [draft]);
+ }, [draft, scope, selectedNotebookId]);
return (
- setDraft(e.target.value)}
- aria-label="노트 검색"
- style={{
- marginLeft: 12,
- padding: '4px 8px',
- fontSize: 12,
- border: '1px solid #bbb',
- borderRadius: 4,
- width: 200
- }}
- />
+
+ setDraft(e.target.value)}
+ aria-label="노트 검색"
+ style={{
+ marginLeft: 12,
+ padding: '4px 8px',
+ fontSize: 12,
+ border: '1px solid #bbb',
+ borderRadius: 4,
+ width: 200
+ }}
+ />
+
+
);
}
diff --git a/src/renderer/inbox/store.ts b/src/renderer/inbox/store.ts
index 601b230..eb12e60 100644
--- a/src/renderer/inbox/store.ts
+++ b/src/renderer/inbox/store.ts
@@ -1,5 +1,5 @@
import { create } from 'zustand';
-import type { Note, Notebook, PromotionCandidate, ReviewAggregate, WeeklyContinuity } from '@shared/types';
+import type { Note, NoteStatus, Notebook, PromotionCandidate, ReviewAggregate, WeeklyContinuity } from '@shared/types';
import { inboxApi, notebookApi } from './api.js';
import { nextKstMidnightMs } from '@shared/util/kstDate.js';
@@ -79,7 +79,8 @@ interface InboxState {
snoozeRecall: () => Promise;
// v0.2.11 Cut D — search + review actions.
setSearchQuery: (q: string) => void;
- searchNotes: (q: string) => Promise;
+ // v0.4 Task 18 — scope 토글. notebookId 전달 시 해당 notebook 안 검색, undefined 시 전체 검색.
+ searchNotes: (q: string, opts?: { notebookId?: string }) => Promise;
clearSearch: () => void;
loadReview: (period: 'daily' | 'weekly' | 'monthly') => Promise;
// v0.4 — Notebook actions.
@@ -383,7 +384,7 @@ export const useInbox = create((set, get) => ({
set({ searchQuery: q });
if (q.trim().length === 0) set({ searchResults: null });
},
- async searchNotes(q) {
+ async searchNotes(q, opts) {
if (q.trim().length === 0) {
set({ searchResults: null });
return;
@@ -394,7 +395,10 @@ export const useInbox = create((set, get) => ({
? (view === 'trash' ? 'trashed' : view)
: view === 'inbox' ? 'active' : undefined;
try {
- const r = await inboxApi.search(q, status ? { status } : {});
+ // v0.4 Task 18 — opts.notebookId 전달 시 해당 notebook 안 검색, undefined 시 전체 검색.
+ const searchOpts: { status?: NoteStatus; notebookId?: string } = status ? { status: status as NoteStatus } : {};
+ if (opts?.notebookId !== undefined) searchOpts.notebookId = opts.notebookId;
+ const r = await inboxApi.search(q, searchOpts);
set({ searchResults: r });
} catch (e) {
// FTS5 query parse error (special char 미escape) / IPC fail → 빈 결과로.
diff --git a/tests/unit/SearchBox.test.tsx b/tests/unit/SearchBox.test.tsx
index 9292ae7..92b1b9c 100644
--- a/tests/unit/SearchBox.test.tsx
+++ b/tests/unit/SearchBox.test.tsx
@@ -9,10 +9,13 @@ const { mockSearchNotes, mockClearSearch } = vi.hoisted(() => ({
mockClearSearch: vi.fn()
}));
+// selectedNotebookId 를 테스트 간 변경할 수 있도록 ref 로 관리.
+let mockSelectedNotebookId: string | null = 'nb-1';
+
vi.mock('../../src/renderer/inbox/store.js', () => ({
useInbox: Object.assign(
- (selector?: (s: { searchQuery: string }) => unknown) => {
- const state = { searchQuery: '' };
+ (selector?: (s: { searchQuery: string; selectedNotebookId: string | null }) => unknown) => {
+ const state = { searchQuery: '', selectedNotebookId: mockSelectedNotebookId };
return selector ? selector(state) : state;
},
{ getState: () => ({ searchNotes: mockSearchNotes, clearSearch: mockClearSearch }) }
@@ -26,6 +29,7 @@ describe('SearchBox', () => {
vi.clearAllMocks();
cleanup();
vi.useFakeTimers();
+ mockSelectedNotebookId = 'nb-1';
});
it('타이핑 → 200ms debounce 후 searchNotes 호출', () => {
@@ -34,7 +38,7 @@ describe('SearchBox', () => {
fireEvent.change(input, { target: { value: '회의' } });
expect(mockSearchNotes).not.toHaveBeenCalled();
vi.advanceTimersByTime(200);
- expect(mockSearchNotes).toHaveBeenCalledWith('회의');
+ expect(mockSearchNotes).toHaveBeenCalledWith('회의', { notebookId: 'nb-1' });
});
it('빈 값 → clearSearch 호출', () => {
@@ -44,4 +48,32 @@ describe('SearchBox', () => {
vi.advanceTimersByTime(200);
expect(mockClearSearch).toHaveBeenCalled();
});
+
+ it('기본 scope=current — searchNotes 에 selectedNotebookId 전달', () => {
+ render();
+ const input = screen.getByRole('searchbox');
+ fireEvent.change(input, { target: { value: '노트' } });
+ vi.advanceTimersByTime(200);
+ expect(mockSearchNotes).toHaveBeenCalledWith('노트', { notebookId: 'nb-1' });
+ });
+
+ it('scope=all 변경 시 다음 검색에서 notebookId 미전달 (undefined)', () => {
+ render();
+ const input = screen.getByRole('searchbox');
+ const scopeSelect = screen.getByRole('combobox', { name: '검색 범위' });
+
+ fireEvent.change(scopeSelect, { target: { value: 'all' } });
+ fireEvent.change(input, { target: { value: '리뷰' } });
+ vi.advanceTimersByTime(200);
+ expect(mockSearchNotes).toHaveBeenCalledWith('리뷰', { notebookId: undefined });
+ });
+
+ it('selectedNotebookId=null 이면 scope=current 에서 notebookId=undefined 전달', () => {
+ mockSelectedNotebookId = null;
+ render();
+ const input = screen.getByRole('searchbox');
+ fireEvent.change(input, { target: { value: '테스트' } });
+ vi.advanceTimersByTime(200);
+ expect(mockSearchNotes).toHaveBeenCalledWith('테스트', { notebookId: undefined });
+ });
});