import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../core/theme/tokens.dart'; import '../home_data.dart'; import '../../../core/i18n/app_locale.dart'; /// AI Daily Briefing — Figma 홈의 번호 매긴 헤드라인 줄. /// /// **구획을 News 와 SNS 로 나눈다** — 정책 원장 B-47 *News/SNS 각각 0 건이면 해당* /// *섹션 숨김, 모두 0 건이면 전체 Briefing 블록을 숨김*. 구획이 없으면 「섹션만 /// 숨김」이라는 판정 자체가 성립하지 않는다. 블록 전체 숨김은 홈 화면이 맡는다. /// /// **한 줄은 제목만 띄운다.** 출처명·게시시각·AI 요약을 어디에 배치할지는 발주사가 /// HOME 기준으로 지정한 Figma 「REFINE v5A · HOME · Default」 확인 후 정한다. /// 데이터는 [BriefingItem] 에 5 요소가 전부 들어와 있으므로 배치만 남는다. /// /// 줄을 누르면 원문보기로 나간다(B-75 5 요소 중 「원문보기」). class BriefingList extends StatelessWidget { const BriefingList({super.key, required this.items, this.onOpen}); final List items; /// 원문보기 동작. 기본값은 기본 브라우저로 넘기는 것이고, 테스트가 대신 받는다 — /// 위젯 테스트에서 실제 브라우저를 띄울 수는 없다. final Future Function(BriefingItem item)? onOpen; @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; // 구획 순서는 enum 선언 순서다(News 다음 SNS). 수신 순서로 정하면 날마다 바뀐다. final sections = [ for (final section in BriefingSection.values) ( section: section, rows: items.where((i) => i.section == section).toList() ..sort((a, b) => a.rank.compareTo(b.rank)), ), ].where((g) => g.rows.isNotEmpty).toList(); return Container( padding: const EdgeInsets.symmetric( horizontal: AppSpace.s4, vertical: AppSpace.s2, ), decoration: BoxDecoration( color: AppColors.cosmicSurfaceContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (final (groupIndex, group) in sections.indexed) ...[ if (groupIndex > 0) const Divider(height: 1), Padding( padding: const EdgeInsets.only( top: AppSpace.s3, bottom: AppSpace.s1, ), child: Text( group.section.label, style: t.labelSmall?.copyWith( color: AppColors.cosmicAccent, fontWeight: FontWeight.w600, ), ), ), for (final (i, item) in group.rows.indexed) ...[ if (i > 0) const Divider(height: 1), _Row(item: item, onOpen: () => _open(context, item)), ], ], ], ), ); } Future _open(BuildContext context, BriefingItem item) async { final handler = onOpen; if (handler != null) { await handler(item); return; } final opened = await launchUrl( Uri.parse(item.articleUrl), mode: LaunchMode.externalApplication, ); // 열리지 않으면 알린다. 조용히 아무 일도 없으면 눌린 것인지조차 알 수 없다. if (!opened && context.mounted) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text(context.l10n.briefingOpenFailed), behavior: SnackBarBehavior.floating, )); } } } class _Row extends StatelessWidget { const _Row({required this.item, required this.onOpen}); final BriefingItem item; final VoidCallback onOpen; @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return InkWell( onTap: onOpen, child: Padding( padding: const EdgeInsets.symmetric(vertical: AppSpace.s3), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 20, child: Text( '${item.rank}.', style: t.bodySmall?.copyWith( color: AppColors.cosmicAccent, fontWeight: FontWeight.w600, ), ), ), Expanded(child: Text(item.headline, style: t.bodySmall)), ], ), ), ); } }