Files
inkling/docs/superpowers/specs/2026-05-14-v04-notebooks-lifecycle-design.md
th-kim0823 b860187b37 docs(spec): m007 → m008 정정 (m007 이 FTS 로 이미 사용중)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:43:31 +09:00

328 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# v0.4 — Notebooks + Lifecycle Simplification Design
**작성일:** 2026-05-14
**선행 문서:**
- `docs/superpowers/specs/2026-04-25-dogfood-feedback.md` F25 (raw idea)
- `docs/superpowers/specs/2026-05-09-v032-cut-g-design.md` (v0.3.2 Cut G — **deprecate**, 본 문서로 승격)
- `docs/superpowers/strategy/v028plus-roadmap.md` Cut G position
---
## 1. 정체성
inbox 분류 layer 도입 + lifecycle 단순화. 단일 cut 으로 묶음 — 두 변경이 같은 UI 영역 (헤더 탭 + status 모델) 을 동시 손대므로 분리 시 중간 상태가 어색.
- **분류 (notebook)**: cross-cutting 컨텍스트. "회사" / "개인" / "학습" 같은 공간 구분. 단일 DB 안 `notebook_id` 컬럼 (옵션 B — Cut G 결정 승계).
- **lifecycle 단순화**: status 4분기 → 3분기. `archived` 제거.
---
## 2. dogfood 근거 (왜 이 시점에 이 cut)
2026-04-25 ~ 2026-05-14 (19일) telemetry + DB:
| 신호 | 수치 | 의미 |
|------|------|------|
| `archived` 노트 | 0건 | 4분기 lifecycle 중 1차원 dead — 사용자가 한 번도 사용 안 함 |
| `active` 노트 | 10건 | 사용 중 |
| `completed` 노트 | 8건 | 적극 사용 |
| `trashed` 노트 | 20건 | 적극 사용 |
| top tag `mlx-ops` | 6건 | 단일 tag 가 사실상 "MLX팀 메모" 컨텍스트 그룹 역할 — tag 의 분류 욕구 명확 |
| tag vocab 적중률 | 32.8% (19/58) | 새 tag 빈번 생성 — 분류 욕구 일관성 부족, grouping UI 필요 |
**archived 는 제거 안전** (마이그레이션 영향 0건). **분류 욕구는 데이터로 확인**되었으나 tag 만으로 충족 어려움 (적중률 낮음).
사용자 우려 ("분류 × lifecycle 햇갈림") 의 대응: archived 제거로 한 차원 줄임. 결과 layer = (notebook × 3분기 × tag) — 3차원이지만 archived 0건 빼서 정신 부담 줄음.
---
## 3. 범위
| 항목 | 결정 |
|------|------|
| **분류 모델** | 옵션 B — `notebook_id` 카테고리, 단일 DB. 옵션 A (다중 profile + 비밀번호 잠금) 는 v0.5+ 후보. |
| **lifecycle 차원** | 3분기 — `active` (inbox) / `completed` (완료) / `trashed` (휴지통). `archived` 제거. |
| **default notebook** | 마이그레이션 시 "기본" 1개 자동 생성. 모든 기존 노트 배치. |
| **사이드바 가시성** | 사용자 토글 + last state (`settings.sidebar_visible`). Cmd+B / Ctrl+B 단축키. |
| **사이드바 내용** | 상단 notebook 목록 + 하단 메모 list (compact view, current notebook + current view 필터). |
| **사이드바 폭** | default 240px, `settings.sidebar_width` 조정 가능 (180-400). |
| **notebook 삭제** | FK RESTRICT — 메모 잔류 시 throw. UI 가 "메모 N건 이동 후 재시도" 안내. |
| **search scope** | 기본 current notebook, 사용자가 dropdown 으로 "모든 노트북" 전환 가능. |
| **새 capture 의 notebook** | current `selectedNotebookId`. 사용자가 다른 notebook 에 있으면 거기 저장. |
---
## 4. Schema 마이그레이션 (m008)
```sql
-- 1. notebooks 테이블 생성
CREATE TABLE notebooks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
color TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE UNIQUE INDEX idx_notebooks_name ON notebooks(name);
-- 2. notes.notebook_id 추가 (FK, NOT NULL — default 통해 보장)
ALTER TABLE notes ADD COLUMN notebook_id TEXT
REFERENCES notebooks(id) ON DELETE RESTRICT;
-- 3. default notebook "기본" 생성
INSERT INTO notebooks (id, name, created_at, updated_at)
VALUES ('<uuidv7>', '기본', '<now>', '<now>');
-- 4. 모든 기존 노트를 default notebook 에 배치
UPDATE notes SET notebook_id = '<default-id>' WHERE notebook_id IS NULL;
-- 5. NOT NULL 제약 추가 (마이그레이션 후)
-- SQLite ALTER 제약: 새 테이블 만들고 복사 패턴 사용 (better-sqlite3 표준)
-- 6. archived → completed 정리 (실제 0건이라 no-op, 안전 위해 명시)
UPDATE notes SET status = 'completed' WHERE status = 'archived';
-- 7. status CHECK constraint 갱신: archived 제거
-- 마찬가지로 새 테이블 만들고 복사 패턴
```
마이그레이션 안전성: 기존 archived 노트 0건 확인 (telemetry + DB query). default notebook 자동 생성으로 기존 사용자 영향 없음.
---
## 5. NotebookRepository
```ts
interface Notebook {
id: string;
name: string;
color: string | null;
createdAt: string;
updatedAt: string;
noteCount: number; // active 노트만 (trashed 제외)
}
class NotebookRepository {
list(): Notebook[]; // count 포함, name ASC
findById(id: string): Notebook | null;
create(input: { name: string; color?: string }): Notebook;
rename(id: string, name: string): void; // UNIQUE name violation throw
setColor(id: string, color: string | null): void;
delete(id: string): { ok: true } | { ok: false; reason: 'has_notes' };
// 메모 잔류 시 FK RESTRICT throw → ok:false 변환
moveNote(noteId: string, notebookId: string): void;
countByNotebook(): Map<string, number>; // 한번 호출로 전체 count
}
```
---
## 6. UI — 사이드바
```
┌───────────────┬─────────────────────────────────────┐
│ [≡] Inkling │ [Inbox(N) 완료(N) 휴지통(N)] [🔍] [⚙] │
├───────────────┼─────────────────────────────────────┤
│ ● 기본 (5) │ │
│ ● 회사 (12) │ NoteCard list (main pane) │
│ ● 학습 (3) │ │
│ + 새 노트북 │ │
│ ───── │ │
│ 메모 빠른 list │ │
│ ・제목1 │ │
│ ・제목2 #tag │ │
│ ・제목3 │ │
└───────────────┴─────────────────────────────────────┘
```
- 좌측, 폭 240px (사용자 조정 가능)
- 헤더 좌측 `≡` 클릭 → 토글. 단축키 `Cmd+B` / `Ctrl+B`
- 상단: notebook 목록 (active 노트 count badge). 클릭 → `selectedNotebookId` 변경
- "+ 새 노트북" 클릭 → NotebookCreateModal (name + optional color)
- 노트북 우클릭 / hover icon → rename / color 변경 / delete
- 하단: 메모 list — `selectedNotebookId` + `selectedView` (status 탭) 필터 결과의 compact view. 클릭 → main pane 의 noteCard 로 scroll.
---
## 7. UI — lifecycle 단순화
| 변경 | 내용 |
|------|------|
| 헤더 탭 | "Inbox / 완료 / 보관 / 휴지통" → **"Inbox / 완료 / 휴지통"** (3탭) |
| MoveStatusModal | "보관" 옵션 제거. 사용자 선택지: "완료" / "휴지통" (또는 "복원" — trash mode 에서) |
| countsByStatus IPC | `archived` 필드 제거. 기존 호출자 (헤더 badge) 적용 |
| NoteRepository.setStatus | `archived` 인자 받으면 throw (defensive). 마이그레이션 후 호출자 없음 |
| 데이터 호환성 | 기존 archived 노트 0건이라 마이그레이션 단계에서 `completed` 로 일괄 이동 |
---
## 8. store / IPC
```ts
interface InboxStore {
notebooks: Notebook[];
selectedNotebookId: string | null; // null = "모든 노트북" 검색 결과 등 특수 모드
sidebarVisible: boolean;
sidebarWidth: number;
loadNotebooks: () => Promise<void>;
selectNotebook: (id: string) => void;
createNotebook: (name: string, color?: string) => Promise<{ ok: boolean; reason?: string }>;
renameNotebook: (id: string, name: string) => Promise<{ ok: boolean; reason?: string }>;
setNotebookColor: (id: string, color: string | null) => Promise<void>;
deleteNotebook: (id: string) => Promise<{ ok: boolean; reason?: string }>;
moveNoteToNotebook: (noteId: string, notebookId: string) => Promise<void>;
toggleSidebar: () => void;
}
```
IPC channels (신설):
- `notebook:list` / `notebook:create` / `notebook:rename` / `notebook:set-color` / `notebook:delete`
- `notebook:move-note`
기존 변경:
- `inbox:list` / `inbox:list-by-status``notebookId?` 옵션 추가 (없으면 모든 노트북)
- `inbox:counts-by-status``notebookId?` 옵션 + `archived` 필드 제거
---
## 9. search 통합
- 기본 scope: `selectedNotebookId` 안 검색
- search box 옆 dropdown: "이 노트북" / "모든 노트북"
- `inbox:search``notebookId?` 옵션 추가
- search 결과 NoteCard 가 notebook 표시 (모든 노트북 검색 시) — 작은 색 chip + 이름
---
## 10. sync 영향 (Cut E 이후)
- export 시 frontmatter 에 `notebook` 필드 추가 (이름 — 다기기 간 id 충돌 회피)
- import 시 notebook 이름이 없으면 생성, 있으면 reuse
- 충돌: 같은 이름 + 다른 id 면 import side 의 id 우선 (deterministic)
---
## 11. AI × Notebook 통합
### 11-1. fit 매칭 (매 capture)
- `buildPrompt` 가 현재 notebooks 목록 (이름) 을 prompt 에 포함
- AI 응답 JSON 에 `notebook_match` 필드 (기존 notebook 이름 또는 null) 추가
- schema (Zod) 갱신: `notebook_match: z.string().nullable().optional()`
- 매치 성공 시 자동 배치 (Zero-Effort 가치 우선). NoteCard 의 notebook chip 클릭으로 1-click 변경 가능
- 매치 실패 / null → default "기본" notebook
prompt 추가 라인 예시 (한국어 자연어):
```
사용 가능한 노트북: 기본, 회사, 학습
이 노트가 위 노트북 중 하나에 명확히 속하면 그 이름을 "notebook_match" 에 반환. 그렇지 않으면 null.
새 노트북 이름을 만들지 말 것 — 기존 목록 안에서만 선택.
```
### 11-2. promotion 제안
**trigger** — 같은 tag 가 **active 노트 안 3건 이상** 누적되고 그 노트들이 모두 default "기본" notebook 에 있을 때 (`active` + `notebook_id = default` + 동일 tag).
분석 timing — inbox 열릴 때 lazy. 단순 SQL aggregation 이라 cost 무시 가능.
```sql
SELECT t.name, COUNT(DISTINCT n.id) AS cnt
FROM tags t
JOIN note_tags nt ON nt.tag_id = t.id
JOIN notes n ON n.id = nt.note_id
WHERE n.status = 'active'
AND n.notebook_id = '<default-id>'
GROUP BY t.id
HAVING cnt >= 3
```
매치 발생 시 InboxStore 에 `promotionCandidate: { tag, noteIds, suggestedName }` 채움. banner 가 표시.
**v0.4 first release 는 rule only** — LLM 의 semantic cluster 분석은 cost 큼. dogfood 결과 보고 v0.5+ 검토.
### 11-3. UI — PromotionBanner
- Inbox 상단, RecallBanner / ExpiryBanner 와 같은 위치
- 문구 예: "💡 `mlx-ops` 관련 노트 6개가 모였어요. 새 노트북 **MLX Ops** 로 분리할까요?"
- 액션: `[수락]` `[나중에]` `[숨기기]`
- 수락 → 작은 modal 띄움 (이름 inline 수정 + color 선택) → notebook 생성 + 해당 noteIds 일괄 이동 + 사이드바 자동 열어 새 notebook 가시화
- 나중에 → 24h snooze (RecallBanner 패턴)
- 숨기기 → 해당 tag 영구 dismiss (`settings.promotion_dismissed_tags` array)
### 11-4. notebook 이름 generation
- default: tag 이름을 Title Case 변환 (예: `mlx-ops``MLX Ops`, `user-management``User Management`)
- 사용자가 수락 modal 에서 inline 수정 가능
### 11-5. promotion 후 status 보존
- 이동된 노트의 `status` 그대로 유지 (active → active, completed → completed)
- `notebook_id` 만 변경
### 11-6. 새 노트의 자동 fit 매칭이 promoted notebook 도 인식
- promotion 후 notebooks 목록에 새 notebook 포함 → 다음 capture 의 prompt 에 자동 반영
- 그 후 같은 주제 노트는 자동으로 새 notebook 에 들어감
---
## 12. 테스트 전략
| 영역 | 케이스 |
|------|--------|
| NotebookRepository | CRUD + count + delete RESTRICT |
| migration m008 | default notebook 생성 + 기존 노트 마이그레이션 + archived → completed |
| store actions | 토글 + 선택 변경 + create/rename/delete 흐름 |
| UI 사이드바 | notebook 목록 render + 클릭 selectedNotebookId 변경 + count badge |
| 메모 list 필터 | notebook + view 필터 combination |
| search scope | current / all 옵션별 결과 |
| MoveStatusModal | archived 옵션 부재 |
| 헤더 탭 | 3탭 + count |
| AI prompt notebook_match | prompt 에 notebooks 목록 포함 + AI 응답 schema 의 notebook_match 필드 |
| capture 자동 fit | AI 응답 notebook_match 매치 시 자동 배치, 미매치 시 default |
| promotion query | tag 3건 이상 + default notebook 클러스터 검출 |
| PromotionBanner | 수락 → notebook 생성 + 일괄 이동 / 나중에 → 24h snooze / 숨기기 → tag dismiss 영구 저장 |
---
## 13. risks
| risk | 대응 |
|------|------|
| migration 의 status check constraint 갱신 실패 | SQLite 새 테이블 복사 패턴 + 검증 query 후 commit |
| 좁은 화면 (1280×720) 에서 사이드바 부담 | settings.sidebar_visible default false (사용자가 켜야 보임) |
| notebook 삭제 시 RESTRICT error UX | "메모 N건 이동 후 다시 시도" + 이동 dialog 제공 |
| sync (Cut E) 와 결합 시 notebook 정합성 | frontmatter notebook 이름 기반 — 다기기 간 ID 충돌 회피 |
| tag 와 notebook 의미 혼동 | UI text 명시: "노트북 = 컨텍스트 (회사 / 개인)", "태그 = 주제 키워드 (mlx-ops, keycloak)" — 설정 페이지 도움말에 안내 |
| 사용자가 다중 notebook 실제 안 만들면 default 1개 + 사이드바 = noise | default sidebar hidden + 사용자가 2번째 notebook 만들 때 자동 reveal hint |
| AI 가 notebook_match 에 새 이름 ad-hoc 생성 시도 | prompt 에 "기존 목록 안에서만 선택, 새 이름 만들지 말 것" 명시 + schema 검증 단계에서 notebooks 목록과 매치 안 되는 값은 null 로 coerce |
| 같은 tag 의 promotion 이 반복 노출되어 사용자 피로 | "숨기기" → 영구 dismiss (settings 의 array). RecallBanner 의 24h snooze 와 별개 영구 저장소 |
---
## 14. 게이트
- 단위 테스트 — notebook CRUD + migration + UI 사이드바 + 메모 list 필터 + status 3분기 회귀 + AI fit/promotion 분기
- 마이그레이션 verify — fresh DB + 기존 DB 두 시나리오에서 default notebook 생성 + archived 정리
- dogfood — 2-3 일 후 (1) 사이드바 열려있는 비율, (2) notebook 갯수 (수동 vs promoted), (3) notebook 간 메모 이동 빈도, (4) promotion 수락률 측정
---
## 15. 작업 분해 (writing-plans 입력)
1. **m008 마이그레이션** — notebooks 테이블 + notes.notebook_id + archived 정리 + status enum
2. **NotebookRepository** + IPC 핸들러
3. **NoteRepository** 변경 — notebook_id 필터, list/search/counts 옵션 확장
4. **store** — notebooks state + actions + promotionCandidate state
5. **AI prompt 변경** — buildPrompt 에 notebooks 목록 주입, schema 의 notebook_match 필드, AiWorker 가 응답 매치하여 자동 배치 (또는 default 로 coerce)
6. **promotion 분석** — NoteRepository.findPromotionCandidates 쿼리 + store action (inbox open 시 lazy)
7. **PromotionBanner UI** — Inbox 상단, 수락 modal, 24h snooze, 영구 dismiss
8. **사이드바 UI** — Sidebar.tsx + NotebookList + NotebookCreateModal
9. **헤더 / MoveStatusModal** — archived 제거
10. **search** — scope 옵션 통합
11. **sync (옵션 deferred)** — frontmatter notebook 필드. v0.4 본체 머지 후 별도 작업으로 가능
12. **CHANGELOG + release notes** — dogfood 근거 명시