strategy.md §1 heading and core sentence reframed to '머릿속에서 꺼내 두기' as the load-bearing user action (was '기억 구출'). §4.1 보상 카피 list updated to match shipped copy. Brief Zeigarnik- effect rationale added per F4-E. §7 example updated. Slice spec §5.5 카피 테이블 4개 항목 갱신 — code 와 spec 의 카피 드리프트 차단. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43 KiB
Inkling — Vertical Slice v0.4 설계 문서
작성일: 2026-04-24 (v0.1) · 개정: 2026-04-25 (v0.2, Strategy 통합) · 2026-04-25 (v0.3, gemma4:26b + LAN endpoint) · 2026-04-25 (v0.4, gemma4:e4b 표준 티어로 다운사이즈)
저자: 김태현 (dlsrks0734@gmail.com)
대상 기획서: inkling.md v1.4
참조 문서: docs/superpowers/strategy/strategy.md (심리학 기반 습관화 전략)
문서 성격: Phase 1 MVP의 첫 서브프로젝트 설계 (vertical slice)
0. 개요
이 문서는 inkling.md 기획서의 Phase 1 MVP(§8.1) 중 종단 end-to-end 경로 한 줄만 최소 구현하기 위한 설계다. 목적은 세 가지 핵심 가설을 가장 얇은 경로로 검증하는 것이다.
- "전역 단축키 → 입력 → 저장"이 실제로 3초 내 마찰 없이 이루어지는가. (기획서 §3.2-3)
- 로컬 Ollama(Gemma 4 9B)가 제목·요약·태그를 실용 가능한 품질로 생성하는가. (기획서 §4.2, §6.5)
- Strategy 문서의 핵심 행동 루프(Capture → Clarify → Capitalize)와 회복 친화 스트릭이 실제 dogfood에서 "메모를 던지고 싶은 충동"을 만드는가.
Slice는 dogfood만을 목적으로 하며, 패키징·서명·배포·캘린더 트리거·검색·내보내기는 전부 후속 spec의 범위다.
0.1 제품 원칙 (Strategy §12)
이 원칙들은 모든 UX 결정·카피·기능 우선순위의 상위 기준이다. 충돌 시 원칙이 이긴다.
- 메모의 기준은 완성도가 아니라 존재 여부다.
- 사용자의 첫 책임은 캡처, 정리는 시스템의 책임이다.
- AI는 사고를 대체하지 않고 사고를 다시 꺼내게 한다.
- 스트릭은 죄책감이 아니라 회복을 위한 장치다.
- 개인 메모는 팀 지식의 씨앗이다.
원칙 1·2·3은 본 slice에 직접 반영된다. 원칙 4는 Weekly Continuity와 회복 친화 카피로 반영. 원칙 5는 Confluence 내보내기 spec(후속)에서 본격 다룸.
1. 범위
1.1 In-scope
- 플랫폼: Windows + macOS Electron 빌드. Windows를 dogfood 우선으로 하고 macOS는 빌드 통과 및 기본 동작 수준을 유지.
- Quick Capture: 전역 단축키
Ctrl/Cmd+Shift+J, 단일 입력창. 텍스트 + 클립보드 붙여넣기 이미지 입력. - 저장: 로컬 SQLite 단일 프로필(
default). 미디어는 프로필 디렉터리media/하위 파일로 저장. - AI 파이프라인: 비동기 처리.
LocalOllamaProvider1개 구현체, endpoint는INKLING_OLLAMA_ENDPOINT환경변수 또는 생성자 인자로 주입(미지정 시http://localhost:11434폴백).gemma4:e4b(8.0B Q4_K_M, 효율 변형) 표준 티어 고정 — 공식 9B 체크포인트 부재 시 가장 근접한 대체. 출력은 제목 + 3줄 요약 + 태그 최대 3개. - Inbox: 날짜 내림차순 리스트. 카드에 제목·요약·태그·원문·미디어 썸네일. AI 필드 인라인 편집 및 노트 삭제 지원. 원문은 읽기 전용.
- 즉시 보상 토스트 (Strategy §4.1): Quick Capture 저장 직후 OS 네이티브 알림으로 회전 카피 1개 표시 ("이 생각은 이제 Inkling이 들고 있습니다." 등 4종).
- AI 제안형 라벨 (Strategy §7-원칙4): AI가 생성한 제목·태그 옆에 작은 "AI" 회색 라벨. 사용자가 편집한 필드에서는 라벨이 사라져 "내 메모" 정체성 식별 가능.
- "의미 한 줄" 배너 (Strategy §2.2, §7-원칙2): AI가
done전환 시 카드 상단에 1회 노출. 회전 4종 중 1개 ("내일의 내가 알아야 할 것은?" 등). 입력 시 카드에 💡 배지로 영구 노출, 건너뛰기 시 해당 카드에서는 다시 안 띄움.notes.user_intent·notes.intent_prompted_at컬럼 사용. - Weekly Continuity 스트릭 (Strategy §5): Daily streak 폐기. KST 월~일 주 단위, 목표 7건/주(요일 무관, 같은 날 누적 OK). Inbox 상단 배지: 미달성 "이번 주 N/7", 달성 "이번 주 7/7 ✓ · 연속 K주 완성". 마지막 기록 후 7일 이상 갭 후 첫 기록 시 24시간 동안 "🌱 흐름을 다시 이어갑니다" 토스트.
- 회복 친화 카피 정책 (Strategy §5, §12-원칙4): "실패"·"끊김"·"연속 실패" 단어 금지. 0건 상태도 "이번 주 한 줄이면 시작입니다". 모든 사용자 대면 문구는 §5의 카피 카탈로그 따름.
1.2 Out-of-scope (후속 spec에서 다룸)
기존 항목:
- 음성 입력 / STT
- 관련 메모 링크 / 프로젝트 자동 분류 / 벡터 검색
- 푸시·스케줄러·캘린더 어댑터·모닝/이브닝 프롬프트
- 다중 프로필 / 비밀번호 잠금 / 마스킹 레이어
- 별도
LanOllamaProvider클래스 / 외부 API Provider / BYOK 키 관리 (LAN Ollama는LocalOllamaProviderendpoint 주입 방식으로 slice 범위 내) - Confluence 내보내기 / 프로필 zip 내보내기·재가져오기
- 주간 회고 / 뱃지 / 온보딩 위자드
- 코드 서명·공증·자동 업데이트·배포 파이프라인
- Android 보조 앱
Strategy 문서에서 추가로 명시 이관 (Strategy 절 번호와 함께):
- 실행 의도(if-then) 온보딩 (§3) — 별도 온보딩 위자드 spec
- 상황별 최소 템플릿 (회의/트러블슈팅/요청/의사결정/인수인계, §6) — QuickCapture v2 spec
- 66일 단계별 습관 프로그램 (§9) — 알림 빈도·프롬프트 변화 spec
- 심리학 기반 KPI 측정 + 월 1회 정성 설문 (§10) — 분석 인프라 spec
- A/B 테스트 인프라 (§11) — 실험 플랫폼 spec
- Confluence 공유 후보 추천 + 실패→주의 카피 변환 (§8) — Confluence export spec
- Triggered Capture (회의 종료·퇴근 직전, §3) — 캘린더 어댑터 spec
1.3 종료 조건
- Windows에서 저자 본인이 하루 5건 이상 실제 dogfood를 2주 연속 수행.
- AI 결과 체감 품질 검토: 한국어 제목·요약이 10건 중 7건 이상 사용자가 받아들일 수 있는 수준.
- 앱 재시작 후 데이터 무결성 유지, 수동 사용 기준 크래시 0회.
2. 아키텍처
2.1 프로세스 구조 (Electron 표준 main/renderer)
┌─ Renderer (React + TS) ──────────────────┐
│ - QuickCaptureWindow (frameless popup) │
│ - InboxWindow (main) │
│ - 상태 관리: Zustand │
└──────────────┬───────────────────────────┘
│ IPC (typed, contextBridge)
┌──────────────▼───────────────────────────┐
│ Main process (Node) │
│ ┌──────────────────┐ │
│ │ HotkeyService │ globalShortcut 등록│
│ ├──────────────────┤ │
│ │ CaptureService │ 입력 수신·저장 오케│
│ ├──────────────────┤ │
│ │ NoteRepository │ SQLite CRUD │
│ │ │ (better-sqlite3) │
│ ├──────────────────┤ │
│ │ MediaStore │ 이미지 → 파일 저장 │
│ ├──────────────────┤ │
│ │ AiWorker │ 비동기 큐, 재시도 │
│ │ └─ InferenceProvider ◄── LocalOllama │
│ ├──────────────────┤ │
│ │ StreakService │ SQL 집계 │
│ └──────────────────┘ │
└──────────────────────────────────────────┘
│ HTTP
┌──────────────▼───────────────────────────┐
│ Ollama ({endpoint}, gemma4:e4b) │
└──────────────────────────────────────────┘
2.2 모듈 책임과 경계
- HotkeyService —
Ctrl/Cmd+Shift+J등록, 중복·충돌 감지, 이벤트를QuickCaptureWindow.show()로 연결. OS별 분기는 이 모듈 안에 격리한다. - CaptureService — QuickCapture에서 들어온 페이로드(
{text, images[]})를 받아 단일 트랜잭션으로NoteRepository.create+MediaStore.save를 실행하고, 이어서AiWorker.enqueue(noteId)를 호출한 뒤NotificationService.celebrate(noteId)를 호출한다. 저장 성공 시점에 창 닫힘 응답을 반환한다. - NoteRepository —
notes·note_tags·tags테이블 접근을 전담.user_intent,intent_prompted_at,title_edited_by_user,summary_edited_by_user컬럼 관리도 여기 포함. SQL 문자열이 이 모듈 밖으로 새지 않는다.better-sqlite3(동기 드라이버, 단일 프로세스에 적합)를 사용한다. - MediaStore — 클립보드 PNG 바이트를
{profileDir}/media/{noteId}/{uuid}.png형태로 파일 저장하고, 상대 경로만 DB에 기록한다. - AiWorker — in-memory FIFO 큐 + SQLite
pending_jobs테이블(앱 재시작 시 재적재용). 동시 처리 1개. Ollama 호출 실패/타임아웃을 지수 백오프로 3회 재시도 후 노트를failed로 전이.pending상태의 작업만 재시작 시 자동 재적재되며, 이미failed로 전이된 노트는 slice 범위에서는 수동·자동 어느 쪽으로도 다시 처리되지 않는다. - InferenceProvider 인터페이스 — §4 참조. 본 slice의 유일한 구현체는
LocalOllamaProvider. - StreakService — Weekly Continuity 계산 전담.
notes.created_at을 KST 기준으로 그룹화해 이번 주 건수, 연속 완성 주(consecutive complete weeks), 회복 토스트 노출 여부(showRecoveryToast)를 산출. 순수 읽기 작업. - NotificationService — OS 네이티브 알림(
new Notification(...)) 발사 전담. 회전 카피 4종 중 1개 선택 (noteId 해시 mod 4). 권한 거부 시 무음 폴백. Strategy §4.1 즉시 보상 책임. - IntentService —
setIntent(noteId, text)/dismissIntent(noteId)두 메서드만.notes.user_intent와notes.intent_prompted_at갱신 책임을 격리. UI(IntentBanner)에서 IPC를 통해 호출. 회전 프롬프트 4종 중 noteId 해시 mod 4로 결정론적 선택.
2.3 IPC 계약 (typed preload)
captureApi: {
submit(payload: {text: string; images: ArrayBuffer[]}): Promise<{noteId: string}>;
hide(): void;
}
inboxApi: {
listNotes(opts: {limit: number; cursor?: string}): Promise<Note[]>;
updateAiFields(
noteId: string,
fields: {title?: string; summary?: string; tags?: string[]}
): Promise<void>;
deleteNote(noteId: string): Promise<void>;
getContinuity(): Promise<WeeklyContinuity>;
getPendingCount(): Promise<number>;
getOllamaStatus(): Promise<{ok: boolean; reason?: string}>;
setIntent(noteId: string, text: string): Promise<void>;
dismissIntent(noteId: string): Promise<void>;
onNoteUpdated(cb: (note: Note) => void): () => void;
}
// Continuity / intent types
interface WeeklyContinuity {
weekStart: string; // ISO date, KST 월요일
weekCount: number; // 이번 주 메모 건수
weekTarget: number; // 7
consecutiveCompleteWeeks: number; // 7건 채운 주 연속 횟수
showRecoveryToast: boolean; // 갭≥7일 후 첫 기록인지
}
onNoteUpdated는 AiWorker가 노트 상태를 전이할 때마다 푸시한다. 낙관적 업데이트는 하지 않으며, pending 카드는 "Inkling이 정리하는 중…" 상태로 두었다가 완료 이벤트 수신 시 부분 리렌더로 교체한다. done 전환된 카드 중 intent_prompted_at IS NULL인 것은 IntentBanner를 1회 노출하고, 사용자가 setIntent 또는 dismissIntent 호출 시 intent_prompted_at이 기록되어 다시 노출되지 않는다.
IntentBanner의 회전 4종 프롬프트는 renderer 측에 상수 배열로 보유하고 noteId 해시 mod 4로 결정론적으로 선택한다. IPC 라운드트립이 필요 없는 순수 클라이언트 계산이므로 contract에 노출하지 않는다.
2.4 데이터 디렉터리
- Windows:
%APPDATA%\Inkling\profiles\default\ - macOS:
~/Library/Application Support/Inkling/profiles/default/
Electron의 app.getPath('userData')를 기반으로 경로를 구성한다. 각 프로필 디렉터리는 inkling.sqlite 단일 DB 파일과 media/ 하위 디렉터리를 포함한다.
3. 데이터 모델
3.1 SQLite 스키마 (v1)
-- 메모 본문
CREATE TABLE notes (
id TEXT PRIMARY KEY, -- UUID v7
raw_text TEXT NOT NULL, -- 원본. 불변 (기획서 §4.2)
ai_title TEXT, -- AI 생성, 사용자 편집 가능
ai_summary TEXT, -- AI 생성, 사용자 편집 가능
ai_status TEXT NOT NULL
CHECK (ai_status IN ('pending','done','failed')),
ai_error TEXT, -- 실패 사유 (디버그용)
ai_provider TEXT, -- 'local-ollama/gemma4:e4b'
ai_generated_at TEXT, -- ISO8601
title_edited_by_user INTEGER NOT NULL DEFAULT 0 -- 0/1, AI 라벨 표시 토글
CHECK (title_edited_by_user IN (0,1)),
summary_edited_by_user INTEGER NOT NULL DEFAULT 0
CHECK (summary_edited_by_user IN (0,1)),
user_intent TEXT, -- "의미 한 줄" (Strategy §2.2)
intent_prompted_at TEXT, -- 배너 노출/처리된 시점 (NULL=아직 미노출)
created_at TEXT NOT NULL, -- ISO8601 UTC
updated_at TEXT NOT NULL
);
CREATE INDEX idx_notes_created_at ON notes(created_at DESC);
CREATE INDEX idx_notes_ai_status ON notes(ai_status);
-- 태그 (정규화)
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE
);
CREATE TABLE note_tags (
note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id),
source TEXT NOT NULL CHECK (source IN ('ai','user')),
PRIMARY KEY (note_id, tag_id)
);
-- 미디어 (slice에선 이미지만)
CREATE TABLE media (
id TEXT PRIMARY KEY,
note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK (kind IN ('image')),
rel_path TEXT NOT NULL, -- media/{note_id}/{uuid}.png
mime TEXT NOT NULL,
bytes INTEGER NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX idx_media_note_id ON media(note_id);
-- AI 작업 큐 (재기동 시 재적재)
CREATE TABLE pending_jobs (
note_id TEXT PRIMARY KEY REFERENCES notes(id) ON DELETE CASCADE,
attempts INTEGER NOT NULL DEFAULT 0,
next_run_at TEXT NOT NULL,
last_error TEXT
);
-- 스키마 버전
CREATE TABLE schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
);
3.2 파일시스템 레이아웃
{userData}/Inkling/profiles/default/
├── inkling.sqlite
├── inkling.sqlite-wal # WAL 모드 활성
├── inkling.sqlite-shm
└── media/
└── {note_id}/
└── {uuid}.png
3.3 핵심 불변 조건
raw_text는 저장 이후 수정 쿼리가 존재하지 않는다.NoteRepository에 원문 업데이트 메서드를 만들지 않는 것으로 이 불변을 보장한다.ai_status='pending'행은 반드시pending_jobs에 대응 엔트리가 존재한다.CaptureService.create는 두 insert를 동일 트랜잭션 안에서 수행한다.- 노트 삭제는 DB 트랜잭션 수행 후 미디어 디렉터리 제거 순서로 진행한다. 미디어 삭제 실패로 생긴 고아 파일은 기동 시 GC가 청소한다.
- 태그는
source='ai'와source='user'를 구분해 저장한다. 사용자가 AI 태그를 제거해도tags테이블에서 지우지 않고note_tags행만 삭제한다(태그 이름 재사용 가능). title_edited_by_user/summary_edited_by_user는 사용자가 해당 필드를 한 번이라도 편집하면1로 전이되며, 이후 절대0으로 되돌아가지 않는다. AI가 같은 필드를 재생성하지 않으므로 (slice에는 재처리 기능 없음) 안전하다.intent_prompted_at은setIntent또는dismissIntent호출 시 1회 기록되며, 이후 갱신되지 않는다. NULL 여부가 IntentBanner 노출 게이트.
3.4 마이그레이션
단방향 버전 번호를 사용한다. Slice 출시 시점은 v1. 앱 기동 시 PRAGMA user_version을 읽어 필요한 순차 마이그레이션을 실행한다. 다운그레이드는 지원하지 않는다.
3.5 쿼리 핫스팟
- Inbox 리스트:
SELECT ... FROM notes ORDER BY created_at DESC LIMIT 50 OFFSET ?+ 태그·미디어를 별도 fetch(N+1 허용, 페이지당 50행 기준 실용 문제 없음). - Weekly Continuity:
SELECT created_at FROM notes ORDER BY created_at ASC결과를 KST 월~일 주 단위로 그룹화. 이번 주 카운트와 연속 완성 주(consecutive complete weeks) 계산. 회복 토스트는MAX(created_at)한 행 추가 조회.
4. AI 파이프라인
4.1 Provider 계약
interface InferenceProvider {
readonly name: string; // 'local-ollama/gemma4:e4b'
generate(input: GenerateInput): Promise<GenerateResult>;
healthCheck(): Promise<HealthResult>;
}
interface GenerateInput {
text: string; // 원문. slice에선 이미지 미포함.
}
interface GenerateResult {
title: string; // 한국어, ≤ 60자
summary: string; // 한국어, 3줄, 각 ≤ 120자, "\n" 구분
tags: string[]; // 0~3개, 영문 kebab-case
}
interface HealthResult {
ok: boolean;
model?: string;
reason?: string;
}
4.2 LocalOllamaProvider 구현
- 엔드포인트:
POST {endpoint}/api/generate.endpoint는 생성자 인자({endpoint: string})로 주입하며, 미지정 시http://localhost:11434로 폴백. dogfood에서 LAN Ollama를 쓰기 위해 Task 30 main 엔트리에서process.env.INKLING_OLLAMA_ENDPOINT를 전달. - 요청 본문:
{model: "gemma4:e4b", prompt: <§4.3>, format: "json", stream: false, options: {temperature: 0.2, num_predict: 512}} - 타임아웃: 120초 (AbortController).
- 응답 파싱:
response필드를 JSON.parse한 뒤 zod 스키마로 검증. 실패 시 상위에 throw(AiWorker가 재시도를 결정).
4.3 프롬프트
You organize raw personal notes into structured metadata.
Input note (raw text, may be fragmented, any language):
---
{raw_text}
---
Return a JSON object with EXACTLY these keys:
- "title": concise title in KOREAN (max 60 chars)
- "summary": 3-line summary in KOREAN. Each line max 120 chars. Lines separated by "\n".
- "tags": array of 0 to 3 tags in lowercase kebab-case (English letters and digits only,
e.g., "api-timeout", "weekly-retro"). Empty array if no clear tags.
Rules:
- title and summary MUST be written in Korean regardless of input language.
- tags MUST be English kebab-case (for consistency across notes; easier to search/group).
- Do NOT invent facts not present in the input.
- Do NOT include markdown code fences or preamble.
- Return ONLY the JSON object.
4.4 응답 검증 규칙
title: 한국어 문자 1개 이상 포함(정규식/[가-힣]/). 미포함이면 프롬프트 위반으로 간주하고 재시도 대상으로 삼는다.summary: 줄 수가 정확히 3줄이 아니면 빈 줄을 보정하거나 과잉 줄을 합쳐 3줄로 정규화한다. 0줄이면 실패 처리.tags: 각 태그가^[a-z0-9]+(-[a-z0-9]+)*$를 만족하는지 검사. 위반 태그는 필터링하고, 남은 태그만 저장. 전체 실패는 아니다.
4.5 AiWorker 동작
- 시작 시점: 앱 기동 직후
pending_jobs전체를 큐에 적재. - 동시성: 1건(로컬 Ollama 자원 경합 회피).
- 재시도 백오프: 즉시 / 30초 / 120초. 3회 실패 시
notes.ai_status='failed'로 전이하고pending_jobs에서 삭제. - 성공 트랜잭션:
UPDATE notes SET ai_title, ai_summary, ai_status='done', ai_provider, ai_generated_at=NOW(), updated_at=NOW() WHERE id=?DELETE FROM note_tags WHERE note_id=? AND source='ai'후 새 태그INSERT(멱등).DELETE FROM pending_jobs WHERE note_id=?
- 완료 이벤트: IPC
onNoteUpdated로 갱신된 노트 푸시.
4.6 헬스 체크 (앱 기동 시 1회)
GET /api/tags호출로gemma4:e4b가 설치되어 있는지 확인.- 모델 미설치 → Inbox 상단 배너: "
ollama pull gemma4:e4b실행 후 앱을 재시작하세요." - 엔드포인트 무응답 → 배너: "Ollama가 실행되지 않았습니다.
ollama serve를 실행해주세요." Capture는 계속 허용되며 노트는pending상태로 누적된다. - 자동 pull은 slice에서 제공하지 않는다.
4.7 배제 항목 (후속 spec에서 다룸)
- 이미지 멀티모달 입력 (Gemma 4는 텍스트 전용으로 가정; 비전 모델 도입은 별도 spec).
- 프롬프트 A/B, 사용자 피드백 기반 튜닝.
- 외부 API Provider, LAN Ollama Provider.
- 마스킹 레이어 (로컬 전용이므로 slice에서는 비활성; 기획서 §6.5 마스킹 참조).
5. UX 플로우
5.1 Quick Capture
[Any app/screen]
│ Ctrl/Cmd+Shift+J
▼
[QuickCaptureWindow popup]
- frameless, always-on-top, 640x280
- 화면 중앙, 포커스 자동 획득
- 단일 textarea + 썸네일 스트립 (이미지 있을 때만)
- placeholder: "지금 머릿속에 있는 것 한 줄. 정리는 나중입니다."
- 하단 힌트: "Ctrl+Enter 저장 · Esc 취소 · 이미지 붙여넣기"
│
├─ 사용자 타이핑 / Ctrl+V (이미지)
│ └─ 클립보드 이미지 감지 → 썸네일 추가 (메모리 버퍼)
│
├─ Ctrl+Enter → submit
│ - CaptureService.submit({text, images})
│ - 트랜잭션: notes insert + media 저장 + pending_jobs insert
│ - 성공 시 창 즉시 닫힘 (AI 완료 대기 없음)
│ - 닫힘 직후 NotificationService.celebrate(noteId) 발사 (OS 네이티브 알림)
│ - 실패 시 창 내 빨간 토스트, 내용 유지
│
└─ Esc → 취소 (5자 이상이면 "이 한 줄을 흘려보낼까요?" 1-step 확인)
타이밍 목표
- 단축키 → 창 표시: p95 < 100ms (숨겨진 창을
show/hide토글, 매번 생성하지 않음). - Submit → 창 닫힘: p95 < 300ms (DB 트랜잭션만 동기 대기, AI·알림은 비동기).
저장 직후 즉시 보상 토스트 (Strategy §4.1)
- 발사 시점:
CaptureService.submit트랜잭션 성공 직후. - 채널: OS 네이티브 알림 (
new Notification(title, {body, silent})). Inbox in-app 토스트 아님 — 사용자가 작업 중인 다른 앱 위에 즉시 떠야 보상 즉시성을 살림. - 회전 카피 4종 (noteId 해시 mod 4로 결정론적 선택):
- "이 생각은 이제 Inkling이 들고 있습니다."
- "머릿속에서 꺼내 두었습니다."
- "방금 한 줄 잡아뒀습니다."
- "기록 완료. 이제 잊어도 됩니다."
- 권한: 첫 submit 시 OS 권한 다이얼로그. 거부 시 무음 폴백 (캡처는 정상 동작).
- 알림 본문에는
raw_text나ai_title을 포함하지 않는다. 오직 회전 카피만 표시 (PII 보호와 일관성).
5.2 Inbox
[InboxWindow] — 트레이 아이콘 클릭으로 열림, 상시 실행
┌──────────────────────────────────────────────────┐
│ Inkling 이번 주 4/7 · 연속 2주 완성 │ ← ContinuityBadge
├──────────────────────────────────────────────────┤
│ [🌱 흐름을 다시 이어갑니다] │ ← 회복 토스트 (24h, 갭≥7일 후만)
│ [🟡 Inkling이 정리하는 중: 3건] │ ← pending count (0이면 숨김)
│ [Ollama 상태 경고가 있으면 여기 배너] │
├──────────────────────────────────────────────────┤
│ ┌ 2026-04-24 18:32 ────────────────────────────┐ │
│ │ 회의 A프로젝트 이슈 정리 [AI] │ │ ← 제목 + AI 라벨 (편집 안 됨)
│ │ 첫 줄 요약 │ │
│ │ 둘째 줄 요약 │ │
│ │ 셋째 줄 요약 [AI] │ │
│ │ [api-timeout AI] [meeting AI] │ │ ← 태그 + AI 표시
│ │ 💡 다시 만나면 trace 먼저 확인 │ │ ← user_intent (입력된 경우)
│ │ [썸네일] 🖼 🖼 │ │
│ │ ▸ 원문 보기 (토글, 기본 접힘) │ │
│ │ [🗑 삭제] │ │
│ └──────────────────────────────────────────────┘ │
│ ┌ 2026-04-24 18:10 ────────────────────────────┐ │
│ │ ┌ IntentBanner (1회 노출) ─────────────────┐ │ │ ← intent_prompted_at IS NULL
│ │ │ 💭 내일의 내가 알아야 할 것 한 줄? │ │ │
│ │ │ [한 줄 입력...] [건너뛰기] │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ 트러블슈팅 timeout │ │
│ │ ... │ │
│ └──────────────────────────────────────────────┘ │
│ ┌ 2026-04-24 17:10 ────────────────────────────┐ │
│ │ Inkling이 정리하는 중… │ │ ← ai_status='pending'
│ │ (원문 자동 펼침) │ │
│ └──────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────┤
│ [더 보기] (50개씩 페이지네이션) │
└──────────────────────────────────────────────────┘
상태별 카드 렌더링
pending— 제목 자리에 "Inkling이 정리하는 중…" 스피너. 요약·태그 숨김, 원문 자동 펼침. IntentBanner 미노출.done— 제목·요약·태그 표시, 원문 접힘. AI 라벨은 사용자가 편집하지 않은 필드에만 노출.intent_prompted_at IS NULL이면 카드 상단에 IntentBanner 1회 노출.failed— 제목 자리에 "정리 보류 — 원문은 안전합니다" + 사유 hover tooltip. 원문 자동 펼침. 재시도 버튼은 slice 외. IntentBanner 미노출.
AI 제안형 라벨 (Strategy §7-원칙4)
- 제목 옆 작은 회색 "AI" 배지:
title_edited_by_user=0일 때만 표시. - 요약 카드 우측 하단 "AI" 배지:
summary_edited_by_user=0일 때만 표시. - 태그 옆 "AI" 첨자:
note_tags.source='ai'인 태그만. 사용자 편집/추가 태그는 라벨 없음. - 라벨은 클릭 불가 (장식). hover tooltip: "AI 제안 — 클릭하여 편집".
IntentBanner (Strategy §2.2)
- 노출 조건:
note.aiStatus === 'done' && note.intentPromptedAt === null. - 프롬프트 회전 4종 (noteId 해시 mod 4로 결정론적):
- "내일의 내가 이 메모에서 꼭 알아야 할 것은?"
- "이 메모가 중요한 이유를 한 줄로?"
- "이 문제를 다시 만나면 무엇을 먼저 확인할까요?"
- "동료에게 공유한다면 제목을 뭐라고?"
- 입력 →
setIntent(noteId, text)IPC. 성공 시 배너 사라지고 카드에 "💡 {intent}" 영구 배지로 노출. - 건너뛰기 →
dismissIntent(noteId). 배너 사라지고 다시 안 보임. - 두 경로 모두
intent_prompted_at을 현재 시각으로 기록. - 입력 시 한 줄 ≤ 200자 강제 (오버는 잘림).
- 💡 배지 클릭 시 인라인 편집 가능 (EditableField 재사용).
편집 동작
- 제목·요약 필드는 클릭 시
contentEditable로 전환, blur 시점에 저장(updateAiFieldsIPC). 저장 시 해당 필드의*_edited_by_user가 1로 전이. - 태그 클릭 시 제거(source 무관). 태그 추가 UI는 slice 외.
- 저장 실패 시 이전 값 복구 + 빨간 테두리 1회 플래시.
Continuity 배지 (Strategy §5)
- 0건: "이번 주 한 줄이면 시작입니다"
- 1~6건: "이번 주 N/7"
- 7건 이상: "이번 주 7/7 ✓ · 연속 K주 완성" (K=consecutiveCompleteWeeks)
- 모든 카피에서 "실패", "끊김", "연속 실패" 단어 금지 (회복 친화 카피 정책).
회복 토스트
- 조건: 마지막 메모로부터 ≥7일 갭 후 첫 기록 시.
- 노출: Inbox 상단 in-app 배너 (Continuity 배지와 별도). "🌱 흐름을 다시 이어갑니다".
- 표시 기간: 24시간 또는 사용자가 닫기까지. 닫기 후 같은 회복 사건으로 재노출 안 됨 (
localStorage에 dismiss 시점 저장).
삭제
- 🗑 클릭 → 네이티브 confirm 다이얼로그 ("이 기억을 버릴까요? 되돌릴 수 없습니다."). 확인 시
deleteNote호출 후 카드 제거 및 미디어 파일 삭제.
실시간 업데이트
onNoteUpdated수신 시 해당 카드만 부분 리렌더. 스크롤 위치 유지.
5.3 첫 실행
Slice는 온보딩 위자드를 제공하지 않는다.
- 최초 기동 시
{userData}/Inkling/profiles/default/가 자동 생성된다. - Inbox 창이 열리고 빈 상태 메시지("머릿속에 떠다니는 한 줄을 적어보세요.
Ctrl+Shift+J")가 노출된다. - Ollama 헬스 체크가 실행되고 결과에 따라 배너가 표시된다.
5.4 카피 카탈로그
본 slice의 모든 사용자 대면 문구는 다음 카탈로그를 따른다. 추가 문구가 필요하면 동일 톤(회복 친화·인지 부담 감소·정체성 강화)을 유지한다.
| 위치 | 문구 |
|---|---|
| QuickCapture placeholder | 지금 머릿속에 있는 것 한 줄. 정리는 나중입니다. |
| QuickCapture hint | Ctrl+Enter 저장 · Esc 취소 · 이미지 붙여넣기 |
| QuickCapture 제출 실패 | 저장에 실패했습니다. 다시 시도해주세요. |
| QuickCapture 취소 confirm (5자+) | 이 한 줄을 흘려보낼까요? |
| Inbox 빈 상태 | 머릿속에 떠다니는 한 줄을 적어보세요. Ctrl+Shift+J |
| Inbox 처리 중 카드 | Inkling이 정리하는 중… |
| Inbox AI 실패 카드 | 정리 보류 — 원문은 안전합니다 |
| 트레이 메뉴: Quick Capture | 한 줄 적기 |
| 트레이 메뉴: Inbox 열기 | 보관한 메모 보기 |
| 트레이 메뉴: Quit | 종료 |
| 삭제 confirm | 이 기억을 버릴까요? 되돌릴 수 없습니다. |
| Continuity 0건 | 이번 주 한 줄이면 시작입니다 |
| Continuity 1~6건 | 이번 주 N/7 |
| Continuity 7건+ | 이번 주 7/7 ✓ · 연속 K주 완성 |
| 회복 토스트 | 🌱 흐름을 다시 이어갑니다 |
| 즉시 보상 토스트 (회전 4종) | (§5.1 참조) |
| IntentBanner 프롬프트 (회전 4종) | (§5.2 참조) |
| Ollama 미실행 배너 | Inkling 정리가 잠시 멈췄습니다. Ollama를 실행해주세요. |
| Ollama 모델 미설치 배너 | ollama pull gemma4:e4b 실행 후 앱을 재시작해주세요. |
| 단축키 등록 실패 배너 | 단축키 Ctrl+Shift+J를 다른 앱이 사용 중입니다. 충돌 앱을 닫거나 FAQ를 확인하세요. |
5.5 앱 종료·재시작
- 창 닫기(X)는 트레이로 숨기는 동작이며 앱은 계속 실행된다. 실제 종료는 트레이 메뉴의 "종료" 항목으로 수행한다.
- 재시작 시
pending_jobs를 큐에 재적재하여 AiWorker 처리가 이어진다. - 회복 토스트 dismiss 상태는 renderer
localStorage에 저장되어 재시작 후에도 유지된다.
6. 오류 처리·관찰성·테스트
6.1 실패 모드 카탈로그
| # | 지점 | 실패 | 사용자 영향 | 복구 전략 |
|---|---|---|---|---|
| 1 | 단축키 등록 | 다른 앱이 Ctrl+Shift+J 점유 |
Capture 진입 불가 | 기동 로그 기록 + Inbox 배너 표시. 설정 UI는 slice 외이므로 FAQ로 대체 안내. |
| 2 | Quick Capture submit | DB write 실패(디스크 full, lock) | 메모 소실 위험 | 창 내 빨간 토스트 + 내용 유지. 재시도는 사용자가 Ctrl+Enter 재입력. |
| 3 | 미디어 저장 | 파일 쓰기 실패 | 이미지 누락 | 미디어를 먼저 쓰고 성공 후에만 노트 insert. 미디어 실패 시 노트 저장 자체를 중단. |
| 4 | Ollama 미실행/미설치 | /api/tags 실패 |
노트는 저장되지만 pending 누적 | Inbox 배너 + 큐가 지수 백오프로 계속 시도. |
| 5 | Ollama 타임아웃 (>120s) | AbortError | 3회 실패 시 노트 failed |
ai_error 기록. 수동 재처리는 slice 외. |
| 6 | Ollama 응답 스키마 위반 | zod parse 실패 | 3회 실패 시 failed |
위반 응답 최대 500자를 last_error에 저장. |
| 7 | 앱 비정상 종료 | WAL 미체크포인트 | 재기동 시 자동 복구 | PRAGMA journal_mode=WAL + SQLite 자체 복구. |
| 8 | 미디어 고아 파일 | DB에는 기록 없으나 파일 잔존 | 디스크 낭비 | 앱 기동 시 GC가 media/{note_id}/ 중 미등록 디렉터리 제거. |
| 9 | 마이그레이션 실패 | 스키마 충돌 | 앱 기동 불가 | DB 백업 복사본 자동 생성 후 기동 중단, 에러 다이얼로그 표시. |
| 10 | OS 알림 권한 거부 | 즉시 보상 토스트 미발사 | 보상 신호 없음 | 캡처는 정상 동작. 로그에 1회 기록 후 무음 폴백. 재요청 안 함. |
| 11 | IntentBanner 동시 setIntent | 두 탭/창에서 동시 호출 | (slice는 단일 창이라 미발생) | intent_prompted_at 컬럼이 NOT NULL이면 후속 setIntent는 user_intent만 갱신, 첫 시각 보존. |
6.2 로깅·진단
- 로그 경로:
{userData}/Inkling/logs/main-YYYY-MM-DD.log. 일별 rotate, 7일 보관. - 로그 레벨:
info/warn/error.debug는 환경 변수INKLING_DEBUG=1일 때만 활성. - PII 보호: 로그에
raw_text·ai_title·ai_summary본문을 기록하지 않는다.noteId, 본문 길이, 해시 prefix(첫 8자)만 기록한다. - 진단 명령: 트레이 메뉴에 "Export logs" 항목. 최근 7일 로그를 zip으로 묶어 사용자가 선택한 폴더에 저장.
6.3 테스트 전략
단위 테스트 (Vitest)
NoteRepository: 메모리 SQLite(:memory:)로 CRUD·트랜잭션 원자성·원문 불변 계약 검증.MediaStore: tmp 디렉터리 기반, 경로 충돌 및 바이너리 무결성.AiWorker:InferenceProvider목으로 주입. 성공·타임아웃·파싱 실패·재시도 간격 시나리오.LocalOllamaProvider: HTTP 레이어는undicimock agent로 고정 응답 주입.- 응답 검증기(zod + 한국어 정규식): 영문 제목 반려, kebab-case 위반 태그 필터링.
통합 테스트 (Vitest, Electron main 없이)
- 실제 SQLite 파일 + 실제
LocalOllamaProvider+ 로컬에서 실행 중인 Ollama. - CI에서는 스킵(로컬 커맨드
npm run test:integration으로 수행). - 골든 케이스 10종(한국어 회의 메모, 영문 스택트레이스, 혼합, 단편, 긴 로그 등): 제목·요약 한국어 여부, 태그 kebab-case 형식 검증.
E2E 테스트 (Playwright + Electron)
- Windows · macOS 각각 1회의 smoke run.
- 시나리오: (a) 단축키 → 텍스트 입력 → 저장 → Inbox 노출(pending), (b) AI 완료 대기 → done 상태로 전환 확인, (c) 제목 인라인 편집 후 저장, (d) 삭제 → 카드 제거, (e) 앱 재시작 후 데이터 유지.
수동 체크리스트 (릴리스 전)
- 클립보드 이미지 붙여넣기를 양 OS에서 수동 확인.
- Ollama 중단 상태에서 10건 저장 후 재기동 → 큐 재적재 및 처리 확인.
- 디스크 full 재현(tmpfs quota 등)으로 Failure #2 경로 확인.
6.4 성능 목표
| 항목 | p95 목표 |
|---|---|
| 단축키 → Quick Capture 창 표시 | < 100ms |
| Submit → 창 닫힘 | < 300ms |
| AI 완료 (gemma4:e4b Q4_K_M, LAN, 입력 ≤ 500자) | < 30s 잠정 (서버 GPU·LAN 지연 실측 후 §8에서 확정; 초과해도 failed 아님) |
| Inbox 50건 렌더 | < 200ms |
7. 기술 스택과 버전 정책
7.1 버전 정책
- Node.js: 최신 LTS 고정. 구현 시작 시점 기준 v24.x (Krypton, Latest LTS).
- 라이브러리·프레임워크: 각 의존성의 최신 안정(stable) 메이저를 기본값으로 사용. 의도적 구버전 고정이 필요한 경우(보안 이슈, 알려진 regression 등) 이 문서 하단 §7.3 이슈 로그에 사유 기록.
- pre-release·RC 금지: alpha/beta/rc 태그 의존성은 허용하지 않는다. 단, Electron 내부 Chromium/Node 조합에 한해서는 Electron이 번들한 버전을 그대로 사용한다(별도 pin 불가).
- 버전 고정 방법:
package.json은^프리픽스 없이 정확한 버전으로 pin("electron": "41.3.0"형태). 업그레이드는 PR 단위 명시적 작업으로만 수행. - Node 버전 동기화: 저장소 루트에
.nvmrc(예:24.15.0) 파일을 둔다. CI·로컬 개발자 모두nvm use로 동일 버전 사용.
7.2 초기 pin 후보 (구현 시작 시점에 최신값 재확인)
| 역할 | 패키지 | 메이저 | 비고 |
|---|---|---|---|
| 런타임 | Node.js | 24 LTS | .nvmrc로 minor까지 고정 |
| 데스크톱 셸 | electron |
41 | Chromium + Node 번들 |
| UI | react, react-dom |
19 | Concurrent features 사용 자유 |
| 언어 | typescript |
6 | strict: true |
| 상태 관리 | zustand |
5 | |
| DB | better-sqlite3 |
12 | 동기 드라이버 |
| 스키마 검증 | zod |
4 | AI 응답 파싱 |
| 단위/통합 테스트 | vitest |
4 | |
| E2E | @playwright/test |
1.59 | Electron support 포함 |
| HTTP mock (테스트) | undici |
8 | |
| 패키징 | electron-builder 또는 electron-forge |
미정 | §8 오픈 이슈 |
실제
npm install시점에npm view <pkg> version으로 최신값을 다시 확인하여package.json에 기록한다. 위 표의 메이저 번호는 2026-04-24 기준 참고값이며, 구현 착수일에 더 높은 stable 메이저가 있으면 그것을 채택한다.
7.3 업그레이드 관리
- 의존성 업그레이드는 메이저 단위로 일괄이 아닌 패키지 단위 독립 PR로 수행한다.
- Electron 메이저 업그레이드는 Chromium·Node 동반 상승을 수반하므로 별도 smoke 체크리스트 재수행을 강제한다.
npm outdated를 월 1회 리뷰하며, 보안 경고(npm audit)는 발견 즉시 처리.
7.4 개발 환경 전제
- Windows 개발 머신에 Volta(권장) 또는 nvm-windows로 Node 24 LTS 설치. macOS/Linux는 nvm 사용.
- AI Provider 엔드포인트:
INKLING_OLLAMA_ENDPOINT환경변수로 Ollama 서버 URL 지정 (예:http://192.168.0.47:11434). 미지정 시http://localhost:11434폴백. LAN 서버 사용 시 User 환경변수에 등록 후 앱 실행. - 저장소 최초 체크아웃 흐름:
volta install node@24.15.0또는nvm install→.nvmrc기반 설치·활성화.npm ci→ 락파일에 고정된 정확한 트리 복원.- 구현 착수 시점의 실제 버전이 §7.2 표와 어긋나면 이 문서를 PR로 갱신한다.
8. 오픈 이슈 / 차기 spec으로 이관
기존 항목:
- 단축키 충돌 시 변경 UI: 설정 화면이 slice에 없으므로 FAQ/환경 변수로 임시 대응. 후속 spec에서 정식 설정 UI로 해결.
- AI 결과 수동 재처리 버튼: dogfood 중 프롬프트 튜닝 편의가 필요하면 개발자 전용 debug 메뉴로 먼저 도입 검토.
- Gemma 4 모델 가용성 (확인 완료 2026-04-25): 공식
gemma4:9b체크포인트 부재 확인. LAN 서버(192.168.0.47:11434)에 설치된gemma4:e4b(8.0B Q4_K_M, 효율 변형)로 표준 티어 운용 — 9B 표준 티어에 가장 근접. e variant는 동일 파라미터 클래스의 효율 변형으로, 한국어 출력 품질·kebab-case 태그 형식 준수가 dogfood 1주차 검증 대상. 품질 부족 확인 시gemma4:26b(고품질, 25.8B Q4_K_M)로 상향, 속도 우선 시gemma4:e2b(경량, 5.1B)로 하향. 기획서(inkling.md) §6.5의 "표준=9b" 권장 문구는 9B 공식 체크포인트 출시 여부 재확인 후 별도 조정. - §6.4 AI 완료 p95 실측 (신규): 26B Q4_K_M + LAN 조합의 실측 레이턴시 확보 후 p95 목표 확정. 잠정 < 60s. dogfood 첫 주 중 측정 후 본 문서에 기록.
- Electron 패키징 옵션 결정:
electron-buildervselectron-forge. Slice 바이너리는 dev 빌드로 충분하므로 후속 배포 spec에서 결정. - Node 24 LTS → 이후 LTS 전환: Node 24 Maintenance 진입 시점(로드맵상 2027년대)에 다음 짝수 메이저로 이관. 시점 도래 시 별도 spec.
Strategy v0.2 추가 이관 (구현·실험 시점이 slice 이후로 미뤄진 항목):
- 실행 의도(if-then) 온보딩 위자드 (Strategy §3) — 첫 실행 3분 위자드, 2개 트리거 선택, Capture → Clarify → Capitalize 행동 모델 노출.
- 상황별 최소 템플릿 (Strategy §6) — 회의/트러블슈팅/요청/의사결정/인수인계 5종, 2~4칸 입력 카드. QuickCapture v2.
- 66일 단계별 습관 프로그램 (Strategy §9) — Week 0/1-2/3-4/5-8/9+ 단계별 알림 빈도·프롬프트 자동 변화. 사용자별 진행 상태 저장 필요.
- 심리학 기반 KPI 측정 (Strategy §10) — Time to First Note, MVN Rate, Triggered Capture Rate, Reuse Rate, Confluence Candidate Acceptance + 월 1회 정성 설문 (인지 오프로딩, 자기효능감, 정체성 변화).
- A/B 테스트 인프라 (Strategy §11) — 4종 실험 (빈창 vs 템플릿 / 스트릭형 vs 가치형 피드백 / 의미 질문 유무 / 매일 vs 주 7회).
- Confluence 공유 후보 추천 (Strategy §8) — AI가 문제·해결·근거·재발 방지 패턴을 가진 메모를 후보로 분류. 실패 단어 → "주의할 점" 카피 변환.
- Triggered Capture (Strategy §3, 회의형/디버깅형/요청 관리형/퇴근 회고형/학습형) — 캘린더 어댑터 + 시간 트리거 필요.
- Streak Freeze 자동 보호 (Strategy §5) — 휴가·주말·공휴일 자동 감지. 캘린더 권한 필요.
- 알림 강도 사용자 조절 + 빈도 자동 감소 (Strategy §9 Week 9+) — 사용자 행동 분석 기반 알림 개인화.
본 문서는 dogfood 결과와 후속 spec 작성 과정에서 조정될 수 있다.