[Architect] #204 design spec — Flutter bootstrap + 18 tables

Phase 1 설계서 작성 완료. docs/design/204-flutter-bootstrap/ 13 개 파일:
- README.md (12 섹션 모두 채움, 함수 19 개 명세, AC 16 항)
- 01-project-structure.md (feature-first + layer-first 하이브리드)
- 02-drift-schema-catalog.md (Catalog 7 테이블 Dart 정의)
- 03-drift-schema-user.md (User 11 테이블 + R1~R10 강제 매트릭스)
- 04-migrations.md (schemaVersion v1 + 인덱스 17 개)
- 05-seed-data.md (assets/seed/*.json + first-run import)
- 06-ux-contracts.md (체크인 R8 ≤ 60 초 흐름)
- fn-recommend-variant / fn-compute-streak / fn-weekly-minimum-ratio
- fn-validate-frame-level / fn-active-habit-quota / fn-seed-importer

핵심 결정: dose_variants 는 별도 habit_dose_variant 테이블로 정규화 (FK
무결성 + recommendVariant SQL 단순성). ADR-0002 승격 권장.

Refs #204

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 16:57:31 +09:00
parent bcfc6b2402
commit b8e563176b
13 changed files with 2458 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
# 함수 설계서: `SeedImporter.importIfNeeded` (#204)
> 부모 설계서: [README.md](./README.md) · 상태: Draft
> 작성: [AI] Architect · 구현: `lib/data/seed/seed_importer.dart::SeedImporter.importIfNeeded` (TBD)
> 테스트: `test/data/seed/seed_importer_test.dart` (TBD)
## 1. 시그니처
```dart
class SeedImporter {
SeedImporter(this._db, {this._assetLoader = const RootBundleLoader()});
final AppDatabase _db;
final AssetLoader _assetLoader;
Future<void> importIfNeeded(); // 멱등
}
abstract class AssetLoader {
Future<String> loadString(String path);
}
```
## 2. 책임
앱 첫 실행 시 `assets/seed/*.json` 7 파일을 읽어 카탈로그 테이블 7 개 + `users('u_local_default')` 1 행을 batch insert. 두 번째 실행부터는 noop (멱등성).
## 3. 입력
| 파라미터 | 타입 | 제약/검증 | 설명 |
|----------|------|-----------|------|
| `_db` | `AppDatabase` | not null | open 된 Drift DB |
| `_assetLoader` | `AssetLoader` | testable | prod = `RootBundleLoader`, test = fixture loader |
## 4. 출력
- **반환**: `Future<void>`. 성공/실패는 예외로 전달.
- **부수효과**:
- 카탈로그 7 테이블 insert (~150~180 rows 총합).
- `users` 1 row insert.
- `meta_kv['seeded_v1'] = 'true'` set.
## 5. 동작 / 알고리즘
```
function importIfNeeded():
1. existing = db.metaKvDao.find('seeded_v1')
2. if existing?.value == 'true': return # 멱등 skip
3. plan = [
('references.json', References, _adaptReference),
('protocols.json', Protocols, _adaptProtocol),
('break_protocols.json', BreakProtocols, _adaptBreakProtocol),
('common_frames.json', CommonFrames, _adaptCommonFrame),
('methodologies.json', Methodologies, _adaptMethodology),
('frame_patterns.json', FramePatterns, _adaptFramePattern),
('reward_menu_items.json', RewardMenuItems, _adaptRewardMenuItem),
]
4. await db.transaction(() async:
for (assetName, table, adapter) in plan:
raw = await _assetLoader.loadString('assets/seed/' + assetName)
items = jsonDecode(raw) as List<Map<String,dynamic>>
companions = items.map(adapter).toList()
await db.batch((b) => b.insertAll(table, companions, mode: InsertMode.insertOrReplace))
await _ensureLocalDefaultUser()
await db.metaKvDao.put('seeded_v1', 'true')
5. ) # transaction commit
```
### 5.1 adapter 책임
`_adapt*` 함수는 `Map<String,dynamic>` (JSON) → Drift `Companion`.
- nested object (예: `protocol.default_anchor`) → JSON string column.
- array (예: `procedure[]`) → JSON string column.
- enum string → 그대로 (CHECK 가 검증).
- ULID validation 은 본 import 에선 skip (시드는 의미 식별자 사용, ULID 형식 X).
### 5.2 user 보장
```dart
Future<void> _ensureLocalDefaultUser() async {
await _db.into(_db.users).insertOnConflictUpdate(
UsersCompanion.insert(
id: 'u_local_default',
createdAt: DateTime.now().toIso8601String(),
locale: const Value('ko-KR'),
timezone: const Value('Asia/Seoul'),
),
);
}
```
## 6. 에러 & 실패 모드
| 조건 | 처리 | 결과 |
|------|------|------|
| asset 파일 누락 | rootBundle 예외 → 트랜잭션 rollback | `seeded_v1` flag 미설정. 다음 부팅에 재시도 |
| JSON parse 실패 | `FormatException` → rollback | 동일 |
| CHECK 제약 위반 (시드 데이터 오류) | sqlite 예외 → rollback | dev 가 시드 JSON 수정 + 재빌드 |
| FK 무결성 위반 (예: protocol.reference_ids 가 references 에 없음) | 본 Phase 는 reference_ids 가 JSON 컬럼이라 FK 강제 없음. 단 catalog_dao 가 load 시 broken link 발견 시 warning log | warning only |
| 트랜잭션 중간 device crash | 다음 부팅 시 flag false 상태 → 재시도. sqlite WAL 로 일관성 유지 | OK |
## 7. 엣지케이스
- 두 번째 호출 (이미 seeded) → metaKv read 1 회 + return. < 5 ms.
- 다른 instance 가 동시 호출 (race): Drift 의 transaction 직렬화로 OK. 두 instance 중 하나가 먼저 seeded_v1=true 박으면 다른 instance 가 짧은 시간 안에 read → skip.
- 시드 JSON 의 ID 중복: `InsertMode.insertOrReplace` 가 마지막 값 우선. dev 가 시드 정리 필요.
- 빈 시드 파일 (`[]`): 해당 테이블 0 rows, flag 는 set. AC-2 의 "≥ 0 행" 통과.
## 8. 복잡도 / 성능
- 시간: 1 회 ~ 1 초 (JSON parse + 7 batch insert + 1 commit). 첫 부팅 1 회만.
- 멱등 호출: < 5 ms.
- 메모리: 시드 JSON 전체 RAM 적재 (~수백 KB). OK.
## 9. 의존성
- `AppDatabase` + 모든 카탈로그 테이블 + `meta_kv`.
- `rootBundle` (Flutter asset loader).
- `dart:convert.jsonDecode`.
- adapter 함수들 (`_adaptProtocol`, etc.).
## 10. 테스트 케이스
- [ ] 정상 first-run: fixture 7 파일 → 모든 카탈로그 시드 + users 1 row + flag set
- [ ] 멱등 second-run: 이미 seeded → noop, no DB writes
- [ ] 파일 누락 simulate: protocols.json 없음 → 예외 + 전체 rollback + flag 미설정
- [ ] JSON malformed: protocols.json = "not json" → FormatException + rollback
- [ ] CHECK 위반: protocol.category='invalid' → sqlite 예외 + rollback
- [ ] 빈 배열: protocols.json = "[]" → 0 rows + flag set + 멱등 두 번째 호출 OK
- [ ] users insertOnConflictUpdate: 이미 같은 id 있어도 conflict 없음
- [ ] flag 강제 reset 후 호출 → 재import (시드 변경 시뮬레이션)
## 11. 추적성
- 인수조건: #204 AC-2, AC-3, AC-15.
- 관련 설계서: [05-seed-data.md](./05-seed-data.md), [04-migrations.md](./04-migrations.md).
- 시드 SoT: 4 마크다운 (`huberman-protocols.md`, `habit-todo-methodologies.md`, `habit-breaking-protocols.md`, `nutrition/diet-protocols.md`).