diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts index e50bc2d..782bc86 100644 --- a/src/main/repository/NoteRepository.ts +++ b/src/main/repository/NoteRepository.ts @@ -11,6 +11,10 @@ export interface CreateNoteInput { * 미지정 시 기존 'pending' default + pending_jobs enqueue (backward compat). */ aiStatus?: AiStatus; + /** + * v0.4 — 미지정 시 가장 오래된 notebook (default notebook) 의 id 자동 사용. + */ + notebookId?: string; } export interface NewMediaRow { @@ -83,11 +87,12 @@ export class NoteRepository { const id = uuidv7(); const ts = now.toISOString(); const aiStatus: AiStatus = input.aiStatus ?? 'pending'; + const notebookId = input.notebookId ?? this.getDefaultNotebookId(); const tx = this.db.transaction(() => { this.db - .prepare(`INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) - VALUES (?, ?, ?, ?, ?)`) - .run(id, input.rawText, aiStatus, ts, ts); + .prepare(`INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at, notebook_id) + VALUES (?, ?, ?, ?, ?, ?)`) + .run(id, input.rawText, aiStatus, ts, ts, notebookId); this.db .prepare(`INSERT INTO note_revisions (note_id, raw_text, edited_at, edited_by) VALUES (?, ?, ?, 'capture')`) @@ -104,6 +109,16 @@ export class NoteRepository { return { id }; } + private getDefaultNotebookId(): string { + const r = this.db + .prepare(`SELECT id FROM notebooks ORDER BY created_at ASC LIMIT 1`) + .get() as { id: string } | undefined; + if (!r) { + throw new Error('No default notebook found — m008 migration may not have run'); + } + return r.id; + } + insertMedia(rows: NewMediaRow[]): void { if (rows.length === 0) return; const now = new Date().toISOString(); @@ -897,14 +912,15 @@ export class NoteRepository { finalId = uuidv7(); status = 'forked'; } + const notebookId = this.getDefaultNotebookId(); const tx = this.db.transaction(() => { this.db .prepare( `INSERT INTO notes (id, raw_text, ai_title, ai_summary, ai_status, ai_provider, ai_generated_at, title_edited_by_user, summary_edited_by_user, - user_intent, intent_prompted_at, deleted_at, created_at, updated_at) - VALUES (?, ?, ?, ?, 'done', ?, ?, ?, ?, ?, ?, ?, ?, ?)` + user_intent, intent_prompted_at, deleted_at, created_at, updated_at, notebook_id) + VALUES (?, ?, ?, ?, 'done', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) .run( finalId, @@ -919,7 +935,8 @@ export class NoteRepository { input.intentPromptedAt, input.deletedAt ?? null, input.createdAt, - input.updatedAt + input.updatedAt, + notebookId ); this.db .prepare( @@ -970,6 +987,7 @@ export class NoteRepository { if (!existing) { // INSERT path + const notebookId = this.getDefaultNotebookId(); const tx = this.db.transaction(() => { this.db .prepare( @@ -979,8 +997,8 @@ export class NoteRepository { user_intent, intent_prompted_at, created_at, updated_at, due_date, due_date_edited_by_user, - status, status_changed_at, move_reason) - VALUES (?, ?, ?, ?, 'done', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + status, status_changed_at, move_reason, notebook_id) + VALUES (?, ?, ?, ?, 'done', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) .run( input.id, @@ -999,7 +1017,8 @@ export class NoteRepository { input.dueDateEditedByUser ? 1 : 0, input.status, input.statusChangedAt, - input.moveReason + input.moveReason, + notebookId ); this.db .prepare( @@ -1223,7 +1242,8 @@ export class NoteRepository { createdAt: row.created_at as string, updatedAt: row.updated_at as string, tags: tags as NoteTag[], - media + media, + notebookId: row.notebook_id as string }; } } diff --git a/src/shared/types.ts b/src/shared/types.ts index 1ad2a02..7a09150 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -92,6 +92,8 @@ export interface Note { updatedAt: string; tags: NoteTag[]; media: NoteMedia[]; + // v0.4 — m008 마이그레이션 보장 (notebook_id NOT NULL, default notebook 자동 할당). + notebookId: string; } // v0.4 — Notebook: 노트 묶음 단위. noteCount = status='active' 노트 수. diff --git a/tests/unit/NoteCard.test.tsx b/tests/unit/NoteCard.test.tsx index 106877c..f8d8f15 100644 --- a/tests/unit/NoteCard.test.tsx +++ b/tests/unit/NoteCard.test.tsx @@ -69,7 +69,8 @@ const baseNote: Note = { media: [ { id: 'm1', kind: 'image', relPath: 'media/n1/img1.png', mime: 'image/png', bytes: 100 }, { id: 'm2', kind: 'image', relPath: 'media/n1/img2.jpg', mime: 'image/jpeg', bytes: 200 } - ] + ], + notebookId: 'nb-default' }; describe('NoteCard — image rendering', () => { diff --git a/tests/unit/NoteRepository.test.ts b/tests/unit/NoteRepository.test.ts index 05ad736..f5289a6 100644 --- a/tests/unit/NoteRepository.test.ts +++ b/tests/unit/NoteRepository.test.ts @@ -1222,3 +1222,36 @@ describe('NoteRepository — notes_fts tags sync (v0.2.11 Cut D)', () => { expect(row.tags).toBe('결재'); }); }); + +describe('NoteRepository.create with notebook', () => { + let db: Database.Database; + let repo: NoteRepository; + let defaultId: string; + + beforeEach(() => { + db = new Database(':memory:'); + db.pragma('foreign_keys = ON'); + runMigrations(db); + repo = new NoteRepository(db); + defaultId = (db.prepare(`SELECT id FROM notebooks`).get() as { id: string }).id; + }); + + it('notebook_id 미지정 시 default notebook 으로 들어감', () => { + const { id } = repo.create({ rawText: 'hello' }); + const r = repo.findById(id); + expect(r?.notebookId).toBe(defaultId); + }); + + it('notebook_id 지정 시 그 값 보존', () => { + db.prepare(`INSERT INTO notebooks(id,name,created_at,updated_at) VALUES('nb-other','회사','2026-05-14','2026-05-14')`).run(); + const { id } = repo.create({ rawText: 'hi', notebookId: 'nb-other' }); + expect(repo.findById(id)?.notebookId).toBe('nb-other'); + }); + + it('hydrate 가 notebookId 필드 반환', () => { + const { id } = repo.create({ rawText: 'hi' }); + const r = repo.findById(id); + expect(typeof r?.notebookId).toBe('string'); + expect(r?.notebookId).toBe(defaultId); + }); +}); diff --git a/tests/unit/store.expired.test.ts b/tests/unit/store.expired.test.ts index 7f2406d..d6169e6 100644 --- a/tests/unit/store.expired.test.ts +++ b/tests/unit/store.expired.test.ts @@ -34,7 +34,7 @@ const noteStub = (id: string): Note => ({ deletedAt: null, lastRecalledAt: null, recallDismissedAt: null, status: 'active', statusChangedAt: null, moveReason: null, createdAt: '2026-05-01T00:00:00Z', updatedAt: '2026-05-01T00:00:00Z', - tags: [], media: [] + tags: [], media: [], notebookId: 'nb-default' }); describe('useInbox — expired state (v0.2.3 #5)', () => { diff --git a/tests/unit/store.recall.test.ts b/tests/unit/store.recall.test.ts index 43e636a..8102358 100644 --- a/tests/unit/store.recall.test.ts +++ b/tests/unit/store.recall.test.ts @@ -38,7 +38,8 @@ const note = (id: string): Note => ({ userIntent: null, intentPromptedAt: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), deletedAt: null, lastRecalledAt: null, recallDismissedAt: null, - status: 'active', statusChangedAt: null, moveReason: null + status: 'active', statusChangedAt: null, moveReason: null, + notebookId: 'nb-default' }); describe('store recall actions', () => { diff --git a/tests/unit/store.tagFilter.test.ts b/tests/unit/store.tagFilter.test.ts index 19d1329..4d5350f 100644 --- a/tests/unit/store.tagFilter.test.ts +++ b/tests/unit/store.tagFilter.test.ts @@ -27,7 +27,8 @@ function sample(id: string, tags: string[]): Note { createdAt: '2026-04-26T00:00:00Z', updatedAt: '2026-04-26T00:00:00Z', tags: tags.map((name) => ({ name, source: 'ai' as const })), - media: [] + media: [], + notebookId: 'nb-default' }; } diff --git a/tests/unit/store.trash.test.ts b/tests/unit/store.trash.test.ts index e124365..e52b7dd 100644 --- a/tests/unit/store.trash.test.ts +++ b/tests/unit/store.trash.test.ts @@ -32,7 +32,7 @@ const noteStub = (id: string, deletedAt: string | null = null): Note => ({ deletedAt, lastRecalledAt: null, recallDismissedAt: null, status: deletedAt ? 'trashed' : 'active', statusChangedAt: deletedAt, moveReason: null, createdAt: '2026-05-01T00:00:00Z', updatedAt: '2026-05-01T00:00:00Z', - tags: [], media: [] + tags: [], media: [], notebookId: 'nb-default' }); describe('useInbox — trash state (v0.2.3 #4)', () => {