[03-Developer] #226 Catalog Gallery 구현

- Drift schema v2: Protocols.category CHECK 6→7 (light_circadian/sleep/movement/
  nutrition/focus_cognition/recovery_stress/emotion_relationship). schemaVersion
  1→2 + onUpgrade migrateV1ToV2 (DROP+CREATE+reseed flag 클리어).
- protocols.json 34 항목 v2 재분류 (1차 효과 기준). emotion_relationship 0 매핑.
- 도메인: DisplayCategory enum (8) + CatalogItem sealed (Protocol/Break/Diet).
- 데이터: CatalogRepository.all/byId/referencesByIds (3 source 통합).
- 상태: catalog_providers.dart (catalogItems / groupedByCategory / refsByIds).
- UI: ProtocolGalleryScreen (카테고리 칩 + 카드 그리드) + ProtocolPreviewScreen
  (모든 필드 + reference 펼치기 + "내 습관으로" disabled placeholder) +
  CatalogCard / CategoryChipRow / ReferenceExpandCard. HabitListScreen 빈
  상태 CTA + AppBar 액션.
- 테스트: migration_v1_to_v2 3건 + display_category 5건 + catalog_repository
  9건 + gallery widget 3건 + preview widget 3건 = 23 신규. 기존 88 회귀 0,
  flutter analyze 0 issues. 110 passed / 1 skipped.

설계서: docs/design/226-catalog-gallery/{README, fn-catalog_repository,
fn-migration_v1_to_v2}.md + ADR-0004.

Refs #226
This commit is contained in:
2026-06-12 17:20:13 +09:00
parent 4665f06a94
commit 321d3af53b
24 changed files with 1814 additions and 146 deletions

View File

@@ -0,0 +1,59 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:life_helper/domain/catalog/display_category.dart';
void main() {
test('fromProtocolCategory maps all 7 protocol categories', () {
expect(DisplayCategory.fromProtocolCategory('light_circadian'),
DisplayCategory.lightCircadian);
expect(DisplayCategory.fromProtocolCategory('sleep'),
DisplayCategory.sleep);
expect(DisplayCategory.fromProtocolCategory('movement'),
DisplayCategory.movement);
expect(DisplayCategory.fromProtocolCategory('nutrition'),
DisplayCategory.nutrition);
expect(DisplayCategory.fromProtocolCategory('focus_cognition'),
DisplayCategory.focusCognition);
expect(DisplayCategory.fromProtocolCategory('recovery_stress'),
DisplayCategory.recoveryStress);
expect(DisplayCategory.fromProtocolCategory('emotion_relationship'),
DisplayCategory.emotionRelationship);
});
test('fromProtocolCategory returns null for unknown / break_habit', () {
expect(DisplayCategory.fromProtocolCategory('break_habit'), isNull);
expect(DisplayCategory.fromProtocolCategory('health'), isNull);
expect(DisplayCategory.fromProtocolCategory(''), isNull);
});
test('breakHabit enum has null protocolKey (별도 source)', () {
expect(DisplayCategory.breakHabit.protocolKey, isNull);
});
test('all 8 enum values have label + icon', () {
for (final c in DisplayCategory.values) {
expect(c.label.isNotEmpty, true, reason: '${c.name} label');
}
expect(DisplayCategory.values.length, 8);
});
test('real seed: 모든 protocols.json id 가 정확히 1 DisplayCategory 에 매핑', () {
// Load from disk (test runs from app/).
final file = File('assets/seed/protocols.json');
final rows = json.decode(file.readAsStringSync()) as List;
expect(rows.length, 34, reason: 'protocols.json count');
final seenCategories = <DisplayCategory>{};
for (final r in rows.cast<Map<String, dynamic>>()) {
final cat = r['category'] as String;
final dc = DisplayCategory.fromProtocolCategory(cat);
expect(dc, isNotNull,
reason: 'id=${r['id']} category=$cat not mapped');
seenCategories.add(dc!);
}
// 매핑 누락은 없어야 함, 단 emotion_relationship 은 0 매핑 OK.
expect(seenCategories.contains(DisplayCategory.breakHabit), false,
reason: 'protocols 는 breakHabit 와 직접 매핑 X');
});
}