import 'dart:convert'; import '../../domain/catalog/catalog_item.dart'; import '../../domain/catalog/display_category.dart'; import '../db/app_database.dart'; /// 3 source (Protocols / BreakProtocols / DietPatterns) → 단일 `List`. /// /// 본 이슈 (#226) 의 핵심 변환 한 점. 본 함수는 fn-catalog_repository.md 의 알고리즘대로. class CatalogRepository { CatalogRepository(this._db); final AppDatabase _db; /// 47 항목 (protocols 34 + break 8 + diet 5) 을 displayCategory 기준 정렬해 반환. Future> all() async { final protocolRows = await _db.select(_db.protocols).get(); final breakRows = await _db.select(_db.breakProtocols).get(); final dietRows = await _db.select(_db.dietPatterns).get(); final items = []; for (final 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, )); } for (final b in breakRows) { items.add(BreakCatalogItem( id: b.id, title: b.title, titleEn: null, summary: _summary(b.hubermanSummary, fallback: b.title), evidenceStrength: null, referenceIds: _decodeIds(b.referenceIdsJson), breakCategory: b.category, hubermanSummary: b.hubermanSummary, phases: _decodeList(b.phasesJson), defaultCommonFrames: _decodeList(b.defaultCommonFramesJson), tools: _decodeList(b.toolsJson), medicalWarning: b.medicalWarning, )); } for (final d in dietRows) { items.add(DietCatalogItem( id: d.id, title: d.name, titleEn: null, summary: _summary(d.core, fallback: d.name), 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), )); } items.sort((a, b) { final c = a.displayCategory.index - b.displayCategory.index; return c != 0 ? c : a.id.compareTo(b.id); }); return items; } /// 단건 조회. Preview 화면 진입 시. Future byId(String id) async { final all_ = await all(); for (final item in all_) { if (item.id == id) return item; } return null; } /// reference id 리스트 → References 테이블 매칭. 미매칭 항목은 결과에서 누락. Future> referencesByIds(List ids) async { if (ids.isEmpty) return const []; return (_db.select(_db.references)..where((t) => t.id.isIn(ids))).get(); } } /// `what` 의 첫 문장을 추출. 비어있으면 `fallback` 사용. 60자 초과 시 절단. 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 _decodeIds(String? jsonStr) { if (jsonStr == null) return const []; final decoded = jsonDecode(jsonStr); return decoded is List ? decoded.cast() : const []; } List _decodeList(String? jsonStr) { if (jsonStr == null) return const []; final decoded = jsonDecode(jsonStr); return decoded is List ? decoded.map((e) => e.toString()).toList() : const []; } Map? _decodeAnchor(String? jsonStr) { if (jsonStr == null) return null; final decoded = jsonDecode(jsonStr); return decoded is Map ? decoded : null; }