알림함 항목 생성 · 알림 이동 · 브리핑 항목 승인·상세 · 푸시 토픽을 가입 팬덤 전체로

This commit is contained in:
Sn-Kinos
2026-10-05 22:48:10 +09:00
parent fbf9a2c3d7
commit cf608a1d99
27 changed files with 714 additions and 69 deletions
+5
View File
@@ -247,6 +247,7 @@ class BriefingItem {
required this.sourceUrl,
required this.articleUrl,
required this.aiSummary,
this.sourceKind,
});
/// 구획 안에서의 순서. 구획마다 1 부터 시작한다.
@@ -271,6 +272,10 @@ class BriefingItem {
/// AI 요약 본문.
final String aiSummary;
/// 출처 구분 — `official` · `fandom`(PRD-06 「공식/팬덤 출처 구분 표시」). 수신 규격에서
/// 선택 필드라 없으면 표시하지 않는다.
final String? sourceKind;
}
/// HOT ARCHIVE — 진행 중인 캠페인 한 건.
@@ -290,6 +290,10 @@ class FirestoreHomeRepository implements HomeRepository {
sourceUrl: sourceUrl,
articleUrl: articleUrl,
aiSummary: aiSummary,
sourceKind: switch (data.strOr('sourceKind')) {
final k when k == 'official' || k == 'fandom' => k,
_ => null,
},
);
}
@@ -12,11 +12,9 @@ import '../../../core/i18n/app_locale.dart';
/// *섹션 숨김, 모두 0 건이면 전체 Briefing 블록을 숨김*. 구획이 없으면 「섹션만
/// 숨김」이라는 판정 자체가 성립하지 않는다. 블록 전체 숨김은 홈 화면이 맡는다.
///
/// **한 줄은 제목만 띄운다.** 출처명·게시시각·AI 요약을 어디에 배치할지는 발주사가
/// HOME 기준으로 지정한 Figma 「REFINE v5A · HOME · Default」 확인 후 정한다.
/// 데이터는 [BriefingItem] 에 5 요소가 전부 들어와 있으므로 배치만 남는다.
///
/// 줄을 누르면 원문보기로 나간다(B-75 5 요소 중 「원문보기」).
/// **한 줄은 Figma 대로 번호 + 제목만 띄운다.** 정책 원장 B-75(확정) 「각 항목에 출처명 ·
/// 게시/발행시각 · 원본 URL · 원문보기 · AI 요약 표시」는 Figma 에 배치가 없어, 줄을 누르면
/// 여는 상세 시트([showBriefingDetail])에 모두 싣는다. 시트의 「원문 보기」가 원문으로 나간다.
class BriefingList extends StatelessWidget {
const BriefingList({super.key, required this.items, this.onOpen});
@@ -68,7 +66,14 @@ class BriefingList extends StatelessWidget {
),
for (final item in group.rows) ...[
const SizedBox(height: AppSpace.s5),
_Row(item: item, onOpen: () => _open(context, item)),
_Row(
item: item,
onOpen: () => showBriefingDetail(
context,
item,
onOriginal: () => _open(context, item),
),
),
],
],
],
@@ -145,3 +150,78 @@ class _Row extends StatelessWidget {
);
}
}
/// 브리핑 항목 상세 — B-75 5 요소(출처명 · 게시시각 · 원본 URL · 원문보기 · AI 요약 표시)와
/// 공식/팬덤 출처 구분을 한 시트에 싣는다.
Future<void> showBriefingDetail(
BuildContext context,
BriefingItem item, {
required Future<void> Function() onOriginal,
}) {
const muted = TextStyle(
fontFamily: AppFonts.body,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.4,
color: AppColors.onCosmicMuted,
);
final l10n = context.l10n;
final kind = switch (item.sourceKind) {
'official' => l10n.briefingSourceOfficial,
'fandom' => l10n.briefingSourceFandom,
_ => null,
};
String two(int v) => v.toString().padLeft(2, '0');
final at = item.sourceAt.toLocal();
final stamp =
'${at.year}.${two(at.month)}.${two(at.day)} ${two(at.hour)}:${two(at.minute)}';
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: AppColors.cosmicSurfaceContainer,
builder: (sheet) => SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppSpace.s5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
item.headline,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.4,
color: AppColors.onCosmic,
),
),
const SizedBox(height: AppSpace.s2),
Text([?kind, item.sourceName, stamp].join(' · '), style: muted),
const SizedBox(height: AppSpace.s4),
Text(l10n.briefingAiSummary, style: muted),
const SizedBox(height: AppSpace.s1),
Text(
item.aiSummary,
style: const TextStyle(
fontFamily: AppFonts.body,
fontSize: 15,
height: 1.5,
color: AppColors.onCosmicVariant,
),
),
const SizedBox(height: AppSpace.s3),
Text(item.sourceUrl, style: muted),
const SizedBox(height: AppSpace.s4),
FilledButton(
onPressed: () async {
Navigator.of(sheet).pop();
await onOriginal();
},
child: Text(l10n.briefingViewOriginal),
),
],
),
),
),
);
}