# v0.2.9 — Cut B Design (status 4분기 + 사유 + Ollama-less)

**작성일:** 2026-05-09
**선행 문서:**

- `docs/superpowers/specs/2026-04-25-dogfood-feedback.md` (F17, F18, F23)
- `docs/superpowers/strategy/v028plus-roadmap.md` Cut B

**Cut 라벨:** v0.2.9 — semver 엄밀히 minor (새 status enum + onboarding wizard) 이지만 v0.2.x feature lane 관습 유지.

---

## 1. Cut 정체성

데이터 모델 정비 cut. 메모의 의미 분기 (active / completed / archived / trashed) + 이동 시 사유 입력 + Ollama-less 모드 onboarding. 세 항목이 같은 schema 영역 (notes 테이블 + ai_status enum) 영향이라 한 cut.

---

## 2. 범위

| 항목 | 결정 |
|---|---|
| **F17** | status 4분기 (`active`/`completed`/`archived`/`trashed`) + AI 자동 분류 버튼 (사유 입력 후 클릭 → AI 가 reason+raw_text 분석 → status 추천 → 사용자 confirm/dismiss) |
| **F18** | 자유 텍스트 사유 (preset X — friction 최소). notes.move_reason 컬럼 또는 별도 trash_log 테이블 |
| **F23** | 첫 launch wizard (Y/N) + Ollama 최적화 안내 + 설치 가이드 페이지 링크. ai_status='disabled' 신규 enum + capture skip + raw-only NoteCard fallback |

---

## 3. F17 디테일

### 3-1. Schema 마이그레이션 (m004)

```sql
ALTER TABLE notes ADD COLUMN status TEXT NOT NULL DEFAULT 'active'
  CHECK (status IN ('active', 'completed', 'archived', 'trashed'));
ALTER TABLE notes ADD COLUMN status_changed_at TEXT;
ALTER TABLE notes ADD COLUMN move_reason TEXT;

-- 기존 deleted_at != NULL 노트 → status='trashed' migrate
UPDATE notes SET status='trashed', status_changed_at=deleted_at
  WHERE deleted_at IS NOT NULL;
```

`deleted_at` 컬럼 — backward compat 위해 잔류 (status='trashed' 와 동기화). 향후 cut 에서 제거 가능.

### 3-2. 인터페이스

```ts
type NoteStatus = 'active' | 'completed' | 'archived' | 'trashed';

interface Note {
  // ... 기존 필드
  status: NoteStatus;
  statusChangedAt: string | null;
  moveReason: string | null;
}
```

NoteRepository 메서드:

- `setStatus(id: string, status: NoteStatus, reason: string | null): void`
- `listByStatus(status: NoteStatus, limit?: number): Note[]`
- 기존 `restoreNote()` → `setStatus(id, 'active', null)` 으로 재구현

### 3-3. UI — inbox 헤더 탭 4개

기존 Inbox / 휴지통 2탭 → **Inbox / 완료 / 보관 / 휴지통** 4탭. 헤더 폭 좁아질 수 있어 short label + count badge.

```tsx
<button>Inbox(N)</button> <button>완료(N)</button> <button>보관(N)</button> <button>휴지통(N)</button>
```

`useInbox` store 의 `view: 'inbox' | 'completed' | 'archived' | 'trash' | 'settings'` enum 확장 (기존 `showTrash` boolean + `showSettings` boolean → enum 통합 권장 — 또는 boolean 3개 유지). enum 통합이 깔끔.

### 3-4. NoteCard 액션 메뉴

기존 휴지통 버튼 1개 → 메뉴 (설정 페이지 내부의 dropdown 또는 inline 버튼 group):

- "완료로 이동"
- "보관함으로 이동"
- "휴지통으로 이동"

각 클릭 → 사유 입력 modal (한 줄 textarea, 빈 값 허용) → 확인 → `setStatus(id, target, reason)`.

### 3-5. AI 자동 분류 버튼

사유 입력 modal 안 옵션:

```
사유: [____________________________]
[ AI 자동 분류 ]   [ 완료 ] [ 보관 ] [ 휴지통 ] [ 취소 ]
```

"AI 자동 분류" 클릭 → main 의 `ai:classify-status` IPC → AiWorker 가 prompt:

```
다음 메모와 사용자 사유를 보고 어디로 이동해야 할지 판단:
- 메모 본문: <raw_text>
- 메모 요약: <summary>
- 사용자 사유: <reason>
- 가능한 status: completed (작업 끝), archived (장기 보관, 회수 가능), trashed (불필요)
JSON 출력: { "recommended": "completed|archived|trashed", "rationale": "..." }
```

응답 → 사용자에게 추천 + rationale 표시:

```
AI 추천: 완료
이유: "처리됨" 표현 + 사용자 사유 "결재 끝" 일치
[ 확정 ] [ 다른 status 선택 ]
```

확정 → setStatus 적용. 다른 선택 → preset 버튼 노출.

### 3-6. IPC

```ts
// 신규
'inbox:set-status': (id: string, status: NoteStatus, reason: string | null) => Promise<{ ok: true }>
'ai:classify-status': (id: string, reason: string) => Promise<{ recommended: NoteStatus; rationale: string }>
```

---

## 4. F18 디테일

자유 텍스트 사유 입력 — F17 의 modal 안에 그대로 포함. 별도 컬럼 `notes.move_reason TEXT` (가장 마지막 사유 보존). 변경 이력 보존 시 별도 테이블 (`note_status_log`) 가능 but YAGNI — 마지막 사유만으로 충분.

빈 값 허용 (preset X 정책 따라). 검색/필터 — 향후 cut (F19 search) 에서 검색 인덱스 포함 가능.

---

## 5. F23 디테일

### 5-1. Schema

ai_status enum 확장: `pending | processing | complete | failed | disabled`. 마이그레이션 m004 동일 commit:

```sql
-- ai_status 가 enum text 라 그대로 새 값 INSERT 가능. CHECK constraint 갱신:
-- (SQLite 는 CHECK ALTER 직접 안 됨 → table 재생성 또는 trigger 추가)
```

SQLite CHECK 갱신 어려움 — 옵션:

- (a) 기존 CHECK 제거 + 새 CHECK 추가 (table 재생성)
- (b) CHECK 부재 + application-level 검증

추천: (b) — application 검증 (zod schema). 마이그레이션 비용 ↓.

### 5-2. Onboarding wizard

첫 launch (settings.json 의 `onboarding_completed` flag 부재) 시 모달:

```
Inkling 사용 시작

Inkling 은 로컬 LLM (Ollama) 으로 메모를 자동 정리합니다.
Ollama 가 설치되어 있고 한국어 지원 모델 (gemma3, gemma2 등) 이 pull 되어 있어야 최적의 경험이 가능합니다.

설치 가이드: https://ollama.com/download

[ AI 자동 처리 사용 (Ollama 필요) ]
[ 원문만 저장 (AI 처리 안 함) ]
[ 나중에 설정 ]
```

3 옵션:

- (1) AI 사용 → settings.ai_enabled=true + onboarding_completed=true
- (2) 원문만 → ai_enabled=false + onboarding_completed=true
- (3) 나중에 → onboarding_completed=false (다음 launch 다시 prompt — 하지만 capture 가능, ai_enabled=null=undefined → default true)

추천: 3 옵션 모두 onboarding_completed=true 로 설정 + 사용자가 설정 페이지에서 언제든 변경. (3) 만 다시 prompt 면 friction.

수정: 3 옵션 모두 close 시 onboarding_completed=true. (3) 은 default ai_enabled=true (LAN Ollama 가정 본인 dogfood).

### 5-3. AI off 시 capture path

CaptureService.create():

```ts
const aiEnabled = await settingsService.get('ai_enabled', true);
if (!aiEnabled) {
  // notes INSERT with ai_status='disabled' + skip pending_jobs enqueue
  this.repo.insert({ ...input, ai_status: 'disabled' });
  return { id, ... };
}
// 기존 path
```

### 5-4. NoteCard fallback

ai_status='disabled' 노트 → title fallback = raw_text 첫 60자 (또는 첫 줄).

```tsx
const displayTitle = note.title?.trim() || note.rawText.split('\n')[0].slice(0, 60) || '(빈 메모)';
```

summary/tags hide. raw_text 그대로.

### 5-5. Banner 비활성

ai_enabled=false → OllamaBanner / FailedBanner 자동 비활성 (state 가 false 면 render skip). HealthChecker 도 ai_enabled=false 시 polling 중단.

### 5-6. 설정 페이지 토글

AI 제공자 섹션 상단 추가:

```
[ ] AI 자동 처리 사용

   ↳ AI 처리를 사용하면 메모의 제목/요약/태그가 자동 생성됩니다.
     Ollama 로컬 LLM 이 필요합니다. 설치 가이드: ollama.com/download
```

토글 OFF → ON 전환 시: onboarding wizard 와 동일 prompt 재노출 (간소화 — 그냥 endpoint 검증 후 결과 표시).

### 5-7. 옛 노트 처리 (ON ↔ OFF 전환)

**B1 정책 채택** (roadmap):

- ON → OFF: 기존 pending 잔재 그대로 (드레인 후 enqueue stop)
- OFF → ON: 기존 disabled 잔류 (사용자 명시 trigger 만 처리)

설정 페이지 안 "기존 disabled 메모 N건 — 지금 모두 처리" 버튼 (ON 전환 후 disabled count > 0 시 노출).

---

## 6. 테스트 전략

| 영역 | 단위 | 수동 |
|---|---|---|
| m004 마이그레이션 | mock db → status 컬럼 + 기존 deleted_at != NULL → trashed | - |
| `setStatus` repo | 4 status 전환 + reason 저장 + statusChangedAt | - |
| `listByStatus` | 각 status filter | - |
| 4탭 UI 렌더 | view enum 4값 분기 + count badge | - |
| 사유 입력 modal | 자유 텍스트 입력 + 빈 값 허용 + 4 status 버튼 | - |
| `ai:classify-status` IPC | mock provider 응답 → recommended + rationale 반환 | - |
| AI 자동 분류 UI | recommended 표시 + 확정 클릭 시 setStatus 호출 | - |
| ai_status='disabled' enum | application zod 검증 + capture path skip pending_jobs | - |
| Onboarding wizard | 첫 launch 시 표시 (settings 부재) + 3 옵션 결과 | 첫 launch 시 표시 확인 |
| AI off 시 NoteCard | title fallback (raw 첫 줄), summary/tags hide | - |
| AI off 시 Banner | render skip 회귀 | - |

**목표**: 단위 467 → 약 490 (+23), typecheck 0.

---

## 7. Risk

| Risk | 대응 |
|---|---|
| AI 자동 분류 정확도 낮음 | 추천만 표시, 사용자 confirm 강제. fallback = preset 4 status 버튼 |
| status 4분기 + tag + reason layer 가 사용자 정신 부담 | dogfood 1주 측정. 사용 빈도 낮은 status 는 v0.2.10+ 에서 hide 옵션 |
| Onboarding wizard 가 첫 launch 흐름 차단 | "나중에 설정" 옵션 제공 + close 가능 |
| ai_enabled false 시 회귀 (기존 pending → 영원히 잔류) | 설정 페이지의 "기존 disabled 메모 N건 처리" 버튼 |

---

## 8. v0.2.9 후

**Cut C** (v0.2.10) — F20 raw_text revision history. AI 재실행 input = current raw_text (latest revision).

**dogfood verify**:

1. 4탭 사용 빈도 (active/completed/archived/trashed) — 사용 안 되는 status 발견 시 cut B+1 에서 hide
2. AI 자동 분류 정확도 (사용자 confirm 비율)
3. Onboarding wizard 의 3 옵션 비율
