[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:
137
docs/design/204-flutter-bootstrap/fn-seed-importer.md
Normal file
137
docs/design/204-flutter-bootstrap/fn-seed-importer.md
Normal 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`).
|
||||
Reference in New Issue
Block a user