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

15 KiB
Raw Blame History

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)

-- 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

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

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-statusnotebookId? 옵션 추가 (없으면 모든 노트북)
  • inbox:counts-by-statusnotebookId? 옵션 + archived 필드 제거

9. search 통합

  • 기본 scope: selectedNotebookId 안 검색
  • search box 옆 dropdown: "이 노트북" / "모든 노트북"
  • inbox:searchnotebookId? 옵션 추가
  • 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 무시 가능.

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-opsMLX Ops, user-managementUser 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 근거 명시