Add vertical slice design spec for Inkling MVP
Initial design document covering the first end-to-end slice of Inkling: Quick Capture, SQLite storage, Local Ollama AI pipeline (gemma4:9b), and Inbox with inline editing. Scoped for dogfood on Windows/macOS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
# Inkling — Vertical Slice v0.1 설계 문서
|
||||
|
||||
**작성일:** 2026-04-24
|
||||
**저자:** 김태현 (dlsrks0734@gmail.com)
|
||||
**대상 기획서:** `inkling.md` v1.4
|
||||
**문서 성격:** Phase 1 MVP의 첫 서브프로젝트 설계 (vertical slice)
|
||||
|
||||
---
|
||||
|
||||
## 0. 개요
|
||||
|
||||
이 문서는 `inkling.md` 기획서의 Phase 1 MVP(§8.1) 중 **종단 end-to-end 경로** 한 줄만 최소 구현하기 위한 설계다. 목적은 두 가지 핵심 가설을 가장 얇은 경로로 검증하는 것이다.
|
||||
|
||||
1. "전역 단축키 → 입력 → 저장"이 실제로 3초 내 마찰 없이 이루어지는가. (§3.2-3)
|
||||
2. 로컬 Ollama(Gemma 4 9B)가 제목·요약·태그를 실용 가능한 품질로 생성하는가. (§4.2, §6.5)
|
||||
|
||||
Slice는 dogfood만을 목적으로 하며, 패키징·서명·배포·알림·검색·내보내기는 전부 후속 spec의 범위다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 범위
|
||||
|
||||
### 1.1 In-scope
|
||||
|
||||
- **플랫폼:** Windows + macOS Electron 빌드. Windows를 dogfood 우선으로 하고 macOS는 빌드 통과 및 기본 동작 수준을 유지.
|
||||
- **Quick Capture:** 전역 단축키 `Ctrl/Cmd+Shift+J`, 단일 입력창. 텍스트 + 클립보드 붙여넣기 이미지 입력.
|
||||
- **저장:** 로컬 SQLite 단일 프로필(`default`). 미디어는 프로필 디렉터리 `media/` 하위 파일로 저장.
|
||||
- **AI 파이프라인:** 비동기 처리. `LocalOllamaProvider` 1개 구현체. `gemma4:9b` 표준 티어 고정. 출력은 제목 + 3줄 요약 + 태그 최대 3개.
|
||||
- **Inbox:** 날짜 내림차순 리스트. 카드에 제목·요약·태그·원문·미디어 썸네일. AI 필드 인라인 편집 및 노트 삭제 지원. 원문은 읽기 전용.
|
||||
- **스트릭:** 연속 기록 일수를 Inbox 상단 배지에 표시. SQL 집계 기반.
|
||||
|
||||
### 1.2 Out-of-scope (후속 spec에서 다룸)
|
||||
|
||||
- 음성 입력 / STT
|
||||
- 관련 메모 링크 / 프로젝트 자동 분류 / 벡터 검색
|
||||
- 알림·푸시·스케줄러·캘린더 어댑터·모닝/이브닝 프롬프트
|
||||
- 다중 프로필 / 비밀번호 잠금 / 마스킹 레이어
|
||||
- LAN Ollama Provider / 외부 API Provider / BYOK 키 관리
|
||||
- Confluence 내보내기 / 프로필 zip 내보내기·재가져오기
|
||||
- 주간 회고 / 뱃지 / 온보딩 위자드
|
||||
- 코드 서명·공증·자동 업데이트·배포 파이프라인
|
||||
- Android 보조 앱
|
||||
|
||||
### 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 (localhost:11434, gemma4:9b) │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 모듈 책임과 경계
|
||||
|
||||
- **HotkeyService** — `Ctrl/Cmd+Shift+J` 등록, 중복·충돌 감지, 이벤트를 `QuickCaptureWindow.show()`로 연결. OS별 분기는 이 모듈 안에 격리한다.
|
||||
- **CaptureService** — QuickCapture에서 들어온 페이로드(`{text, images[]}`)를 받아 단일 트랜잭션으로 `NoteRepository.create` + `MediaStore.save`를 실행하고, 이어서 `AiWorker.enqueue(noteId)`를 호출한다. 저장 성공 시점에 창 닫힘 응답을 반환한다.
|
||||
- **NoteRepository** — `notes`·`note_tags`·`tags` 테이블 접근을 전담. 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** — `SELECT DISTINCT DATE(created_at, 'localtime') FROM notes` 결과로 연속 일수를 계산한다. 순수 읽기 작업.
|
||||
|
||||
### 2.3 IPC 계약 (typed preload)
|
||||
|
||||
```ts
|
||||
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>;
|
||||
getStreak(): Promise<{current: number; longest: number}>;
|
||||
getPendingCount(): Promise<number>;
|
||||
onNoteUpdated(cb: (note: Note) => void): () => void;
|
||||
}
|
||||
```
|
||||
|
||||
`onNoteUpdated`는 AiWorker가 노트 상태를 전이할 때마다 푸시한다. 낙관적 업데이트는 하지 않으며, pending 카드는 "처리 중…" 상태로 두었다가 완료 이벤트 수신 시 부분 리렌더로 교체한다.
|
||||
|
||||
### 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)
|
||||
|
||||
```sql
|
||||
-- 메모 본문
|
||||
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:9b'
|
||||
ai_generated_at TEXT, -- ISO8601
|
||||
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` 행만 삭제한다(태그 이름 재사용 가능).
|
||||
|
||||
### 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행 기준 실용 문제 없음).
|
||||
- 스트릭: `SELECT DISTINCT DATE(created_at, 'localtime') FROM notes`를 로드해 애플리케이션 레이어에서 연속 일수를 계산한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. AI 파이프라인
|
||||
|
||||
### 4.1 Provider 계약
|
||||
|
||||
```ts
|
||||
interface InferenceProvider {
|
||||
readonly name: string; // 'local-ollama/gemma4:9b'
|
||||
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 http://localhost:11434/api/generate`
|
||||
- 요청 본문: `{model: "gemma4:9b", 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`에서 삭제.
|
||||
- **성공 트랜잭션:**
|
||||
1. `UPDATE notes SET ai_title, ai_summary, ai_status='done', ai_provider, ai_generated_at=NOW(), updated_at=NOW() WHERE id=?`
|
||||
2. `DELETE FROM note_tags WHERE note_id=? AND source='ai'` 후 새 태그 `INSERT` (멱등).
|
||||
3. `DELETE FROM pending_jobs WHERE note_id=?`
|
||||
- **완료 이벤트:** IPC `onNoteUpdated`로 갱신된 노트 푸시.
|
||||
|
||||
### 4.6 헬스 체크 (앱 기동 시 1회)
|
||||
|
||||
- `GET /api/tags` 호출로 `gemma4:9b`가 설치되어 있는지 확인.
|
||||
- 모델 미설치 → Inbox 상단 배너: "`ollama pull gemma4:9b` 실행 후 앱을 재시작하세요."
|
||||
- 엔드포인트 무응답 → 배너: "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 + 썸네일 스트립 (이미지 있을 때만)
|
||||
- 하단 힌트: "Ctrl+Enter 저장 · Esc 취소 · 이미지 붙여넣기 가능"
|
||||
│
|
||||
├─ 사용자 타이핑 / Ctrl+V (이미지)
|
||||
│ └─ 클립보드 이미지 감지 → 썸네일 추가 (메모리 버퍼)
|
||||
│
|
||||
├─ Ctrl+Enter → submit
|
||||
│ - CaptureService.submit({text, images})
|
||||
│ - 트랜잭션: notes insert + media 저장 + pending_jobs insert
|
||||
│ - 성공 시 창 즉시 닫힘 (AI 완료 대기 없음)
|
||||
│ - 실패 시 창 내 빨간 토스트, 내용 유지
|
||||
│
|
||||
└─ Esc → 취소 (5자 이상이면 "정말 버릴까요?" 1-step 확인)
|
||||
```
|
||||
|
||||
**타이밍 목표**
|
||||
|
||||
- 단축키 → 창 표시: p95 < 100ms (숨겨진 창을 `show/hide` 토글, 매번 생성하지 않음).
|
||||
- Submit → 창 닫힘: p95 < 300ms (DB 트랜잭션만 동기 대기, AI는 비동기).
|
||||
|
||||
### 5.2 Inbox
|
||||
|
||||
```
|
||||
[InboxWindow] — 트레이 아이콘 클릭으로 열림, 상시 실행
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Inkling 🔥 연속 4일 │ ← StreakBadge
|
||||
├─────────────────────────────────────────┤
|
||||
│ [🟡 Ollama 처리 중: 3건] │ ← pending count (0이면 숨김)
|
||||
│ [Ollama 상태 경고가 있으면 여기 배너] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ ┌ 2026-04-24 18:32 ─────────────────┐ │
|
||||
│ │ [제목 AI] 회의 A프로젝트 이슈 정리 │ │ ← 인라인 편집 가능
|
||||
│ │ [요약] 3줄... │ │
|
||||
│ │ [태그] api-timeout meeting │ │ ← 클릭 제거
|
||||
│ │ [썸네일] 🖼 🖼 │ │
|
||||
│ │ ▸ 원문 보기 (토글, 기본 접힘) │ │
|
||||
│ │ [🗑 삭제] │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ ┌ 2026-04-24 17:10 ─────────────────┐ │
|
||||
│ │ [제목] 처리 중… │ │ ← ai_status='pending'
|
||||
│ │ (원문 자동 펼침) │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ [더 보기] (50개씩 페이지네이션) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**상태별 카드 렌더링**
|
||||
|
||||
- `pending` — 제목 자리에 스피너와 "처리 중…". 요약·태그 숨김, 원문 자동 펼침.
|
||||
- `done` — 제목·요약·태그 전부 표시, 원문 접힘.
|
||||
- `failed` — 제목 자리에 "AI 처리 실패" + 사유 hover tooltip. 원문 자동 펼침. 재시도 버튼은 slice 외.
|
||||
|
||||
**편집 동작**
|
||||
|
||||
- 제목·요약 필드는 클릭 시 `contentEditable`로 전환, blur 시점에 저장(`updateAiFields` IPC).
|
||||
- 태그 클릭 시 제거(source 무관). 태그 추가 UI는 slice 외.
|
||||
- 저장 실패 시 이전 값 복구 + 빨간 테두리 1회 플래시.
|
||||
|
||||
**삭제**
|
||||
|
||||
- 🗑 클릭 → 네이티브 confirm 다이얼로그 1회. 확인 시 `deleteNote` 호출 후 카드 제거 및 미디어 파일 삭제.
|
||||
|
||||
**실시간 업데이트**
|
||||
|
||||
- `onNoteUpdated` 수신 시 해당 카드만 부분 리렌더. 스크롤 위치 유지.
|
||||
|
||||
### 5.3 첫 실행
|
||||
|
||||
Slice는 온보딩 위자드를 제공하지 않는다.
|
||||
|
||||
1. 최초 기동 시 `{userData}/Inkling/profiles/default/`가 자동 생성된다.
|
||||
2. Inbox 창이 열리고 빈 상태 메시지("`Ctrl+Shift+J`로 첫 메모를 남겨보세요.")가 노출된다.
|
||||
3. Ollama 헬스 체크가 실행되고 결과에 따라 배너가 표시된다.
|
||||
|
||||
### 5.4 앱 종료·재시작
|
||||
|
||||
- 창 닫기(X)는 트레이로 숨기는 동작이며 앱은 계속 실행된다. 실제 종료는 트레이 메뉴의 "Quit" 항목으로 수행한다.
|
||||
- 재시작 시 `pending_jobs`를 큐에 재적재하여 AiWorker 처리가 이어진다.
|
||||
|
||||
---
|
||||
|
||||
## 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 백업 복사본 자동 생성 후 기동 중단, 에러 다이얼로그 표시. |
|
||||
|
||||
### 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 레이어는 `undici` mock 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:9b, 로컬, 입력 ≤ 500자, RTX 3060 12GB 기준) | < 30s (초과해도 failed 아님) |
|
||||
| Inbox 50건 렌더 | < 200ms |
|
||||
|
||||
---
|
||||
|
||||
## 7. 오픈 이슈 / 차기 spec으로 이관
|
||||
|
||||
- **단축키 충돌 시 변경 UI**: 설정 화면이 slice에 없으므로 FAQ/환경 변수로 임시 대응. 후속 spec에서 정식 설정 UI로 해결.
|
||||
- **AI 결과 수동 재처리 버튼**: dogfood 중 프롬프트 튜닝 편의가 필요하면 개발자 전용 debug 메뉴로 먼저 도입 검토.
|
||||
- **Gemma 4 모델 가용성**: 설계 시점(2026-04) 공식 체크포인트 상태를 별도 확인. 미출시면 `gemma3:9b` 등 동세대 대체 모델로 임시 운용하고 스펙을 업데이트.
|
||||
- **Electron 패키징 옵션 결정**: `electron-builder` vs `electron-forge`. Slice 바이너리는 dev 빌드로 충분하므로 후속 배포 spec에서 결정.
|
||||
|
||||
---
|
||||
|
||||
*본 문서는 dogfood 결과와 후속 spec 작성 과정에서 조정될 수 있다.*
|
||||
Reference in New Issue
Block a user