Files
life-helper/docs/design/204-flutter-bootstrap/fn-seed-importer.md
joungmin b8e563176b [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>
2026-06-11 16:57:31 +09:00

5.7 KiB

함수 설계서: SeedImporter.importIfNeeded (#204)

부모 설계서: README.md · 상태: Draft 작성: [AI] Architect · 구현: lib/data/seed/seed_importer.dart::SeedImporter.importIfNeeded (TBD) 테스트: test/data/seed/seed_importer_test.dart (TBD)

1. 시그니처

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 (150180 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 보장

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, 04-migrations.md.
  • 시드 SoT: 4 마크다운 (huberman-protocols.md, habit-todo-methodologies.md, habit-breaking-protocols.md, nutrition/diet-protocols.md).