[02-Architect] #226 design spec + ADR-0004

- docs/design/226-catalog-gallery/README.md — 12 changed files, DisplayCategory enum, sealed CatalogItem, 5 scenarios
- docs/design/226-catalog-gallery/fn-catalog_repository.md — 3-source unification algorithm + helpers
- docs/design/226-catalog-gallery/fn-migration_v1_to_v2.md — first schema migration (DROP + reseed pattern)
- docs/adr/0004-catalog-recategorization-and-first-migration.md — Catalog 재분류 + 첫 마이그레이션 정책

Refs #226
This commit is contained in:
2026-06-12 16:59:31 +09:00
parent 25be18063e
commit 4665f06a94
4 changed files with 828 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# 함수 설계서: `migrateV1ToV2` (#226)
> **부모 설계서**: ./README.md · **상태**: Draft
> **작성**: [AI] Architect (2026-06-12) · **구현**: `app/lib/data/db/app_database.dart` 의 `migration.onUpgrade` 인라인 또는 file-private top-level · **테스트**: `app/test/data/db/migration_v1_to_v2_test.dart`
## 1. 시그니처
```dart
Future<void> _migrateV1ToV2(Migrator m, AppDatabase db) async { ... }
```
`MigrationStrategy.onUpgrade` 에서 dispatch:
```dart
onUpgrade: (m, from, to) async {
if (from == 1 && to >= 2) {
await _migrateV1ToV2(m, this); // this = AppDatabase
}
// future:
// if (from <= 2 && to >= 3) await _migrateV2ToV3(m, this);
if (from > to || to > schemaVersion) {
assert(false, 'Unknown upgrade path: from=$from to=$to (schemaVersion=$schemaVersion)');
}
},
```
## 2. 책임 (단일 책임, 1줄)
v1 DB 의 `protocols` 테이블을 v2 CHECK 제약으로 교체하고 시드 flag 클리어 — read-only catalog 의 첫 마이그레이션 패턴.
## 3. 입력
| 파라미터 | 타입 | 제약 | 설명 |
|---|---|---|---|
| `m` | `Migrator` | drift 의 schema migrator | DDL API |
| `db` | `AppDatabase` | non-null | metaKv 클리어용 |
## 4. 출력
- **반환**: `Future<void>`.
- **부수효과**:
- `protocols` 테이블 DROP + CREATE (CHECK 제약 7 카테고리로) + 인덱스 `IDX_protocols_category` 재생성.
- `meta_kv` 에서 `kSeededV1Flag` row DELETE.
- 다음 부팅 시 `SeedImporter.importIfNeeded()` 가 재시드 트리거.
- **user 테이블 (Habits, Phases, TrackerEntries, ...) 무변화**.
## 5. 동작 / 알고리즘
```
1. await m.deleteTable(db.protocols);
# SQLite: DROP TABLE protocols
# 인덱스도 자동 cascade drop.
2. await m.createTable(db.protocols);
# v2 schema 로 CREATE TABLE protocols (
# id TEXT PRIMARY KEY,
# category TEXT CHECK (category IN (
# 'light_circadian','sleep','movement','nutrition',
# 'focus_cognition','recovery_stress','emotion_relationship'
# )) NOT NULL,
# title TEXT NOT NULL,
# ...
# );
3. await m.createIndex(Index(
'IDX_protocols_category',
'CREATE INDEX IDX_protocols_category ON protocols(category)',
));
# drop 시 자동 cascade 됐어도 명시적 재생성.
4. await (db.delete(db.metaKv)
..where((t) => t.key.equals(kSeededV1Flag)))
.go();
# 시드 flag 1 row 삭제. 다음 부팅이 importIfNeeded() 호출 → 새 JSON 으로 reseed.
```
## 6. 에러 & 실패 모드
| 조건 | 처리 | 반환/예외 |
|---|---|---|
| DROP 실패 (이론상 없음, 사용자 락) | drift 가 transaction 롤백 → 부팅 실패. 사용자에겐 명시적 에러. | SqliteException 전파 |
| CREATE 실패 (이론상 없음) | 동상 | SqliteException 전파 |
| metaKv 삭제 실패 (이론상 없음) | 동상 — but **여기까지 도달 시 protocols 테이블은 v2 형태**. 다음 부팅 시 flag 가 'true' 인 채라 reseed 안 함 → protocols 빈 상태. **위험.** 그래서 metaKv 삭제는 트랜잭션 내 마지막 단계가 아니라 순서가 중요. | drift onUpgrade 전체가 트랜잭션 — drop/create 와 metaKv 삭제 같이 묶임. |
| 다음 부팅 시 seed JSON 손상 | SeedImporter 가 CHECK 위배 throw → 부팅 실패. dev 단말 1대 영향이라 수용. | FormatException / SqliteException |
## 7. 엣지케이스
- **신규 설치 (`onCreate`)** — 본 함수 호출 0. createAll 이 v2 schema 그대로 적용 후 seed 가 v2 JSON 로드. 정상.
- **v1 → v3+ 점프 (이론상 없음, 현재 schemaVersion=2)** — `from=1, to=3` 이면 v1→v2 → v2→v3 순차 실행 가정. `_migrateV2ToV3` 가 아직 없어 dispatch 가 발견 못 함 → assert false. v3 도입 시점에 명시.
- **트랜잭션 중단** — drift 의 onUpgrade 는 db.transaction 안에서 실행. 부분 실패 시 자동 롤백 → 사용자 DB 는 v1 그대로. 다음 시도에서 재실행.
- **사용자 데이터 보호** — 본 함수는 Protocols 만 건드림. Habits/TrackerEntries 등 user 테이블 0 영향. `migration_v1_to_v2_test.dart` 가 명시적 검증.
- **인덱스 재생성 누락 시** — query latency ↓ 만 영향 (정상 동작). 본 함수가 명시적으로 createIndex 호출하므로 보호.
## 8. 복잡도 / 성능
- 시간: O(1) — DDL 4건.
- 실측: < 50ms (dev 단말, drift 의 transaction overhead 포함).
- 호출 빈도: **dev 단말 평생 1회** (v1 → v2 한 번). 사용자 신규 설치는 호출 0.
## 9. 의존성
- drift `Migrator` API (deleteTable, createTable, createIndex).
- `kSeededV1Flag` 상수 (`core/constants.dart`).
- AppDatabase 의 `protocols` getter (스키마 가져오기).
## 10. 테스트 케이스
- [ ] **smoke**: in-memory DB 를 v1 schema 로 raw SQL 로 생성 → `_migrateV1ToV2(m, db)` 호출 → protocols 테이블의 CHECK 제약이 v2 7 카테고리인지 검증 (PRAGMA / sqlite_master 조회)
- [ ] **flag 클리어**: 사전에 metaKv 에 `seeded_v1='true'` insert → migrate → metaKv 조회 시 row 없음
- [ ] **user 데이터 보호**: 사전에 Habits / Phases / TrackerEntries 에 row insert → migrate → 모두 그대로
- [ ] **v2 CHECK 위배 negative**: migrate 후 `INSERT INTO protocols (..., category='health', ...)` 시도 → SqliteException
- [ ] **v2 CHECK 통과 positive**: `category='light_circadian'` insert → 성공
- [ ] **인덱스 존재**: migrate 후 `sqlite_master` 에서 `IDX_protocols_category` 발견
- [ ] **이중 호출 안전성**: 동일 DB 에 migrate 2회 호출 → 두 번째도 성공 (idempotent 가정. drift `deleteTable` 이 미존재 테이블에 graceful 인지 확인 필요 — OQ)
- [ ] **integration with onUpgrade**: schemaVersion=2 인 AppDatabase 로 v1 DB 열기 → onUpgrade 자동 호출 → 정상 동작
## 11. 추적성
- 인수조건: #226 AC-4 (8 카테고리 재분류 — DB CHECK 갱신 부분), AC-8 (user 테이블 무변화), AC-9 (마이그레이션 unit test).
- 관련 ADR: **ADR-0004** (본 이슈에서 발행 — Catalog re-categorization + first schema migration policy).