From 4c39a38ed5d8168878e198507ba9c068090b00c0 Mon Sep 17 00:00:00 2001
From: th-kim0823
Date: Fri, 15 May 2026 10:03:49 +0900
Subject: [PATCH] =?UTF-8?q?feat(notes):=20notebook=5Fid=20=ED=95=84?=
=?UTF-8?q?=EB=93=9C=20+=20create/upsert=20=EC=8B=9C=20default=20notebook?=
=?UTF-8?q?=20=EB=B3=B4=EC=9E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Note.notebookId 필드 추가(required), NoteRepository.create/importNote/upsertFromSync INSERT 에
notebook_id 컬럼 포함 — 미지정 시 getDefaultNotebookId() 로 가장 오래된 notebook 자동 할당.
hydrate 에 notebookId 반환 추가. 관련 test fixture 5곳 notebookId 보강.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
src/main/repository/NoteRepository.ts | 40 ++++++++++++++++++++-------
src/shared/types.ts | 2 ++
tests/unit/NoteCard.test.tsx | 3 +-
tests/unit/NoteRepository.test.ts | 33 ++++++++++++++++++++++
tests/unit/store.expired.test.ts | 2 +-
tests/unit/store.recall.test.ts | 3 +-
tests/unit/store.tagFilter.test.ts | 3 +-
tests/unit/store.trash.test.ts | 2 +-
8 files changed, 73 insertions(+), 15 deletions(-)
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)', () => {