- 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
106 lines
3.3 KiB
Dart
106 lines
3.3 KiB
Dart
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),
|
|
),
|
|
);
|
|
}
|
|
}
|