[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:
105
app/lib/ui/widgets/catalog_card.dart
Normal file
105
app/lib/ui/widgets/catalog_card.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../domain/catalog/catalog_item.dart';
|
||||
|
||||
class CatalogCard extends StatelessWidget {
|
||||
const CatalogCard({super.key, required this.item, required this.onTap});
|
||||
|
||||
final CatalogItem item;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dc = item.displayCategory;
|
||||
return Semantics(
|
||||
label: '${dc.label} 카테고리. ${item.title}. ${item.summary}',
|
||||
button: true,
|
||||
child: Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(dc.icon, size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (item.titleEn != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
item.titleEn!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.summary,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (item.evidenceStrength != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
_EvidenceBadge(strength: item.evidenceStrength!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EvidenceBadge extends StatelessWidget {
|
||||
const _EvidenceBadge({required this.strength});
|
||||
|
||||
final String strength;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (label, color) = switch (strength) {
|
||||
'strong_rct' || 'strong' => ('근거 강함', Colors.green),
|
||||
'meta_analysis' => ('메타분석', Colors.teal),
|
||||
'moderate' => ('근거 중간', Colors.blue),
|
||||
'observational' => ('관찰연구', Colors.blueGrey),
|
||||
'mechanistic' => ('기전', Colors.orange),
|
||||
'expert_opinion' => ('전문가 의견', Colors.brown),
|
||||
'mixed' => ('근거 혼재', Colors.amber),
|
||||
'weak' => ('근거 약함', Colors.grey),
|
||||
_ => (strength, Colors.grey),
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.15),
|
||||
border: Border.all(color: color),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 11, color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user