[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:
114
app/test/data/catalog/catalog_repository_test.dart
Normal file
114
app/test/data/catalog/catalog_repository_test.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:life_helper/data/catalog/catalog_repository.dart';
|
||||
import 'package:life_helper/data/db/app_database.dart';
|
||||
import 'package:life_helper/data/seed/seed_importer.dart';
|
||||
import 'package:life_helper/domain/catalog/catalog_item.dart';
|
||||
import 'package:life_helper/domain/catalog/display_category.dart';
|
||||
|
||||
import '../seed/test_seeds.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
late CatalogRepository repo;
|
||||
|
||||
setUp(() async {
|
||||
db = AppDatabase.memory();
|
||||
repo = CatalogRepository(db);
|
||||
});
|
||||
|
||||
tearDown(() => db.close());
|
||||
|
||||
Future<void> seed() async {
|
||||
final importer = SeedImporter(db, loadAsset: testStubLoader);
|
||||
await importer.importIfNeeded();
|
||||
}
|
||||
|
||||
test('빈 DB: all() 가 빈 리스트 (throw 안 함)', () async {
|
||||
final items = await repo.all();
|
||||
expect(items, isEmpty);
|
||||
});
|
||||
|
||||
test('정상: seed 후 all() 반환 + displayCategory 매핑', () async {
|
||||
await seed();
|
||||
final items = await repo.all();
|
||||
// test_seeds.dart 가 정의한 stub: protocols 1 + break 1 + diet 1 = 3.
|
||||
expect(items.length, 3);
|
||||
|
||||
final p = items.whereType<ProtocolCatalogItem>().single;
|
||||
expect(p.displayCategory, DisplayCategory.lightCircadian);
|
||||
|
||||
final b = items.whereType<BreakCatalogItem>().single;
|
||||
expect(b.displayCategory, DisplayCategory.breakHabit);
|
||||
|
||||
final d = items.whereType<DietCatalogItem>().single;
|
||||
expect(d.displayCategory, DisplayCategory.nutrition);
|
||||
});
|
||||
|
||||
test('정렬: displayCategory.index → id', () async {
|
||||
await seed();
|
||||
final items = await repo.all();
|
||||
for (var i = 1; i < items.length; i++) {
|
||||
final a = items[i - 1];
|
||||
final b = items[i];
|
||||
expect(a.displayCategory.index <= b.displayCategory.index, true);
|
||||
if (a.displayCategory.index == b.displayCategory.index) {
|
||||
expect(a.id.compareTo(b.id) <= 0, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('summary 길이 ≤ 60자', () async {
|
||||
await seed();
|
||||
final items = await repo.all();
|
||||
for (final item in items) {
|
||||
expect(item.summary.length, lessThanOrEqualTo(60),
|
||||
reason: 'id=${item.id} summary=${item.summary}');
|
||||
}
|
||||
});
|
||||
|
||||
test('byId: 정상 + 미존재', () async {
|
||||
await seed();
|
||||
final p = await repo.byId('morning_sunlight');
|
||||
expect(p, isNotNull);
|
||||
expect(p!.title, '아침 햇빛');
|
||||
|
||||
final none = await repo.byId('no_such_id');
|
||||
expect(none, isNull);
|
||||
});
|
||||
|
||||
test('referencesByIds: 일부 매칭 + 미매칭', () async {
|
||||
await seed();
|
||||
final ok = await repo.referencesByIds(['ref_x']);
|
||||
expect(ok.length, 1);
|
||||
final none = await repo.referencesByIds(['no_ref']);
|
||||
expect(none, isEmpty);
|
||||
final mixed = await repo.referencesByIds(['ref_x', 'no_ref']);
|
||||
expect(mixed.length, 1);
|
||||
});
|
||||
|
||||
test('referencesByIds: 빈 리스트 → 빈 결과 (DB 호출 안 함)', () async {
|
||||
final empty = await repo.referencesByIds(const []);
|
||||
expect(empty, isEmpty);
|
||||
});
|
||||
|
||||
test('손상된 category → StateError', () async {
|
||||
await seed();
|
||||
// CHECK 우회 위해 raw SQL — 외래로 손상 시뮬레이트.
|
||||
await db.customStatement(
|
||||
"INSERT INTO protocols (id, category, title, what, when_text, dose, why, how_json, check_text) "
|
||||
"VALUES ('bad', 'unknown_cat_xxx', 'x', 'x', 'x', 'x', 'x', '[]', 'x')",
|
||||
[]);
|
||||
// 위 insert 가 CHECK 로 실패하면 손상 시뮬레이션 불가 — skip 처리.
|
||||
// 우리는 v2 CHECK 가 있으므로 어차피 throw 됨. 이 시나리오는 raw injection 시에만 발생 — 이 케이스는 명세화로 충분.
|
||||
}, skip: 'v2 CHECK 가 모든 값 차단 — raw injection 시나리오는 명세 검증만');
|
||||
|
||||
test('groupByCategory: 빈 카테고리 key 미포함', () async {
|
||||
await seed();
|
||||
final items = await repo.all();
|
||||
final grouped = groupByCategory(items);
|
||||
// stub 데이터는 lightCircadian + breakHabit + nutrition 만.
|
||||
expect(grouped.keys.toSet(),
|
||||
{DisplayCategory.lightCircadian, DisplayCategory.breakHabit, DisplayCategory.nutrition});
|
||||
expect(grouped.containsKey(DisplayCategory.emotionRelationship), false);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user