;
+}): React.ReactElement {
+ const [open, setOpen] = useState(false);
+ return (
+
+
+ {open && (
+
+ {notebooks.filter((nb) => nb.id !== current.id).map((nb) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore, onPermanentDelete }: Props): React.ReactElement {
const isTrash = mode === 'trash';
// v0.2.9 Cut B Task 13 — ai_status='disabled' 노트는 raw_text 가 1차 정보. 원문 펼침 default 켬.
@@ -122,6 +171,10 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore
const [draftRaw, setDraftRaw] = useState('');
const [showRevisions, setShowRevisions] = useState(false);
+ const notebooks = useInbox((s) => s.notebooks);
+ const moveNoteToNotebook = useInbox((s) => s.moveNoteToNotebook);
+ const currentNb = notebooks.find((nb) => nb.id === local.notebookId);
+
React.useEffect(() => { setLocal(note); }, [note]);
const formatted = new Date(note.createdAt).toLocaleString('ko-KR');
@@ -393,6 +446,18 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore
))}
)}
+ {currentNb && (
+
+ {
+ await moveNoteToNotebook(local.id, newId);
+ setLocal({ ...local, notebookId: newId });
+ }}
+ />
+
+ )}
{local.userIntent !== null && (
💡
diff --git a/tests/unit/NoteCard.test.tsx b/tests/unit/NoteCard.test.tsx
index f8d8f15..5b5c19b 100644
--- a/tests/unit/NoteCard.test.tsx
+++ b/tests/unit/NoteCard.test.tsx
@@ -2,16 +2,17 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react';
-import type { Note } from '@shared/types';
+import type { Note, Notebook } from '@shared/types';
-const { mockOpenMedia, mockSetStatus, mockClassify, mockUpdateRawText } = vi.hoisted(() => ({
+const { mockOpenMedia, mockSetStatus, mockClassify, mockUpdateRawText, mockMoveNoteToNotebook } = vi.hoisted(() => ({
mockOpenMedia: vi.fn(async () => ({ ok: true })),
mockSetStatus: vi.fn(async () => ({ ok: true as const })),
mockClassify: vi.fn(async () => ({
recommended: 'archived' as const,
rationale: 'stub'
})),
- mockUpdateRawText: vi.fn(async () => ({ ok: true as const }))
+ mockUpdateRawText: vi.fn(async () => ({ ok: true as const })),
+ mockMoveNoteToNotebook: vi.fn(async () => {})
}));
vi.mock('../../src/renderer/inbox/api.js', () => ({
@@ -33,9 +34,24 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({
}));
const mockRefreshMeta = vi.fn();
+
+// Notebooks used across notebook-chip tests.
+const stubNotebooks: Notebook[] = [
+ { id: 'nb-1', name: '회사', color: '#4a90d9', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', noteCount: 1 },
+ { id: 'nb-2', name: '개인', color: '#e67e22', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', noteCount: 0 }
+];
+
vi.mock('../../src/renderer/inbox/store.js', () => ({
useInbox: Object.assign(
- () => ({}),
+ // Selector-aware: if selector is a function, call it with the mock state.
+ (selector?: (s: unknown) => unknown) => {
+ const state = {
+ notebooks: stubNotebooks,
+ moveNoteToNotebook: mockMoveNoteToNotebook
+ };
+ if (typeof selector === 'function') return selector(state);
+ return state;
+ },
{ getState: () => ({ setTagFilter: vi.fn(), refreshMeta: mockRefreshMeta }) }
)
}));
@@ -190,3 +206,33 @@ describe('NoteCard — raw_text editing', () => {
expect(last.rawText).toBe('new');
});
});
+
+describe('NoteCard — notebook chip (Task 17)', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ cleanup();
+ });
+
+ it('현재 notebook 이름 chip 렌더링', () => {
+ render();
+ expect(screen.getByTitle('다른 노트북으로 이동')).toBeInTheDocument();
+ expect(screen.getByTitle('다른 노트북으로 이동').textContent).toContain('회사');
+ });
+
+ it('chip 클릭 → 다른 notebook 목록 dropdown', () => {
+ render();
+ fireEvent.click(screen.getByTitle('다른 노트북으로 이동'));
+ // 현재 nb-1('회사') 는 제외, nb-2('개인') 만 보임.
+ expect(screen.getByText('개인')).toBeInTheDocument();
+ expect(screen.queryAllByText('회사').length).toBe(1); // chip 버튼 안에만 존재
+ });
+
+ it('dropdown 의 notebook 클릭 → store.moveNoteToNotebook 호출', async () => {
+ render();
+ fireEvent.click(screen.getByTitle('다른 노트북으로 이동'));
+ fireEvent.click(screen.getByText('개인'));
+ await waitFor(() => {
+ expect(mockMoveNoteToNotebook).toHaveBeenCalledWith('n1', 'nb-2');
+ });
+ });
+});