- 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
7.5 KiB
7.5 KiB
함수 설계서: CatalogRepository.all (#226)
부모 설계서: ./README.md · 상태: Draft 작성: [AI] Architect (2026-06-12) · 구현:
app/lib/data/catalog/catalog_repository.dart· 테스트:app/test/data/catalog/catalog_repository_test.dart
1. 시그니처
class CatalogRepository {
CatalogRepository(this._db);
final AppDatabase _db;
Future<List<CatalogItem>> all();
Future<CatalogItem?> byId(String id);
Future<List<ReferenceRow>> referencesByIds(List<String> ids);
}
본 fn-*.md 는 all() 의 알고리즘만 다룬다. byId / referencesByIds 는 단순 lookup 이므로 README §7 표 한 줄로 충분.
2. 책임 (단일 책임, 1줄)
3 source (Protocols / BreakProtocols / DietPatterns) 를 단일 List<CatalogItem> 으로 통합 — 본 이슈의 핵심 변환 한 점.
3. 입력
| 파라미터 | 타입 | 제약 | 설명 |
|---|---|---|---|
(인스턴스 필드) _db |
AppDatabase |
non-null | seed 가 끝난 DB (시드 안 끝났으면 호출자가 보장) |
4. 출력
- 반환:
List<CatalogItem>— 총 47 항목 (protocols 34 + break 8 + diet 5).- 각 항목은
ProtocolCatalogItem/BreakCatalogItem/DietCatalogItem중 하나 (sealed). - 정렬:
displayCategory.index→id알파벳 순.
- 각 항목은
- 부수효과: DB 3회 read. write 0.
5. 동작 / 알고리즘
1. final protocolRows = await _db.select(_db.protocols).get(); // 34
2. final breakRows = await _db.select(_db.breakProtocols).get(); // 8
3. final dietRows = await _db.select(_db.dietPatterns).get(); // 5
4. final items = <CatalogItem>[];
5. for each p in protocolRows:
final dc = DisplayCategory.fromProtocolCategory(p.category);
if (dc == null) {
throw StateError(
'unknown protocol category "${p.category}" for id=${p.id}'
);
}
items.add(ProtocolCatalogItem(
id: p.id,
title: p.title,
titleEn: p.titleEn,
summary: _summary(p.what, fallback: p.title),
displayCategory: dc,
evidenceStrength: p.evidenceStrength,
referenceIds: _decodeIds(p.referenceIdsJson),
what: p.what,
whenText: p.whenText,
dose: p.dose,
why: p.why,
how: _decodeList(p.howJson),
checkText: p.checkText,
caution: p.caution,
defaultAnchor: _decodeAnchor(p.defaultAnchorJson),
minDoseForStart: p.minDoseForStart,
sourceDoc: p.sourceDoc,
));
6. for each b in breakRows:
items.add(BreakCatalogItem(
id: b.id,
title: b.title,
titleEn: null,
summary: b.hubermanSummary, // 이미 1줄 요약 형태
displayCategory: DisplayCategory.breakHabit,
evidenceStrength: null, // BreakProtocol 스키마에 없음
referenceIds: _decodeIds(b.referenceIdsJson),
breakCategory: b.category, // 'alcohol' / 'nicotine' / ...
hubermanSummary: b.hubermanSummary,
phases: _decodeList(b.phasesJson),
defaultCommonFrames: _decodeList(b.defaultCommonFramesJson),
tools: _decodeList(b.toolsJson),
medicalWarning: b.medicalWarning,
));
7. for each d in dietRows:
items.add(DietCatalogItem(
id: d.id,
title: d.name,
titleEn: null,
summary: d.core,
displayCategory: DisplayCategory.nutrition,
evidenceStrength: d.evidenceStrength,
referenceIds: _decodeIds(d.referenceIdsJson),
name: d.name,
core: d.core,
strengths: _decodeList(d.strengthsJson),
weaknesses: _decodeList(d.weaknessesJson),
koreanContextFit: d.koreanContextFit,
starterLevers: _decodeList(d.starterLeversJson),
medicalWarning: d.medicalWarning,
linkedProtocolIds: _decodeIds(d.linkedProtocolIdsJson),
));
8. items.sort((a, b) {
final c = a.displayCategory.index - b.displayCategory.index;
return c != 0 ? c : a.id.compareTo(b.id);
});
9. return items;
헬퍼 (file-private, 모두 pure)
String _summary(String what, {required String fallback, int max = 60}) {
final firstSentence = what.split(RegExp(r'[.!?。!?]')).first.trim();
final s = firstSentence.isEmpty ? fallback : firstSentence;
return s.length <= max ? s : '${s.substring(0, max - 1)}…';
}
List<String> _decodeIds(String? json) {
if (json == null) return const [];
final decoded = jsonDecode(json);
return decoded is List ? decoded.cast<String>() : const [];
}
List<String> _decodeList(String? json) {
if (json == null) return const [];
final decoded = jsonDecode(json);
return decoded is List ? decoded.map((e) => e.toString()).toList() : const [];
}
Map<String, dynamic>? _decodeAnchor(String? json) {
if (json == null) return null;
final decoded = jsonDecode(json);
return decoded is Map<String, dynamic> ? decoded : null;
}
6. 에러 & 실패 모드
| 조건 | 처리 | 반환/예외 |
|---|---|---|
protocol.category 가 DisplayCategory 매핑 0 |
부팅 직후 첫 호출에서 throw → AsyncValue.error → SnackBar "카탈로그 손상" | StateError('unknown protocol category "x" for id=y') |
howJson / referenceIdsJson 파싱 실패 |
빈 리스트 반환 (graceful) | — |
| DB 미시드 (kSeededV1Flag 없음) | 호출자가 seedInProgressProvider 로 막아야 함. 본 함수는 raw 결과 (0 row) 반환 — UI 가 "준비 중" 표시. |
— |
| 3 source 중 한 source 가 부분 손상 (예: protocols 0 row, break 정상) | partial 결과 반환 — 사용자가 break 만 봄. | — |
_summary 가 빈 문자열 |
fallback (title) 사용. | — |
7. 엣지케이스
- 47 ≠ 실제 row 수: 시드 갱신 후 row 수 변동 가능. 본 함수는 row 수 비검증 —
catalog_repository_test.dart가 일관성 검증. - DisplayCategory 추가: enum 에만 추가하고 매핑은 staging JSON 으로 들어가는 새 protocol 만 채움. 기존 47 항목 매핑 무변화.
- 중복 id: PK 제약상 발생 불가 — drift 가 보장.
- 빈 summary:
what이 punctuation 으로만 시작하면_summary가 빈 문자열 → fallback 적용.
8. 복잡도 / 성능
- 시간: O(N) — N=47, 사용자 화면 진입 1회.
- 공간: O(N) — 47 인스턴스.
- 호출 빈도: 갤러리 진입 시 1회 (Riverpod cache, 화면 사라질 때 dispose).
- 실측 추정 latency: < 5ms on 8GB+ Android. 5초 cold start 영향 0.
9. 의존성
- 호출:
AppDatabase.selectx 3,dart:convert(jsonDecode),DisplayCategory.fromProtocolCategory. - 호출처:
catalogItemsProvider(RiverpodFutureProvider<List<CatalogItem>>).
10. 테스트 케이스
- 정상: seed 가 끝난 in-memory DB → all() 가 47 item + 정렬 (displayCategory.index → id)
- 카테고리 매핑: 모든 ProtocolCatalogItem.displayCategory ≠ null
- Break 단일 카테고리: 모든 BreakCatalogItem.displayCategory == breakHabit
- Diet 단일 카테고리: 모든 DietCatalogItem.displayCategory == nutrition
- summary 길이: 모든 item 의 summary ≤ 60자
- referenceIds 디코딩: 빈 JSON / null / 정상 케이스 3개
- 에러: 손상된 category 값을 직접 DB 에 insert 후 all() → StateError
- 빈 DB: seed 안 한 DB → 빈 리스트 (throw 안 함)
- byId 정상 / 미존재: 2 케이스
- referencesByIds: 일부 매칭 / 전부 미매칭 2 케이스
11. 추적성
- 인수조건: #226 AC-2 (카테고리 칩 표시), AC-3 (카드 그리드), AC-4 (8 카테고리 매핑), AC-7 (reference 매칭).
- 관련 ADR: ADR-0004 (본 이슈에서 발행).