HOME · GALAXY · 행성·성운 상세 · 팬덤 라운지 · 이클립스 화면
This commit is contained in:
@@ -0,0 +1,529 @@
|
||||
/// 홈 화면이 그리는 값의 형태.
|
||||
///
|
||||
/// **화면과 데이터를 갈라 둔 이유** — 서버가 아직 없다. Firestore 연동은 5주차부터이고
|
||||
/// 그때 [HomeRepository] 구현체만 갈아 끼우면 화면은 손대지 않는다. 지금 화면 안에
|
||||
/// 숫자를 박아 두면 그 시점에 화면을 다시 짜야 한다.
|
||||
///
|
||||
/// 값의 근거는 Figma `03_Wireframe` 「홈」(`252:159354`)이다. 시연용 표본이며
|
||||
/// 정책값이 아니다 — 정책은 `정책 설계서 최종본 v1.0` 이 정한다.
|
||||
library;
|
||||
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
|
||||
/// 팬덤 행성의 성장 상태. 정책 근거는 정책 설계서 §P-08.
|
||||
class PlanetGrowth {
|
||||
const PlanetGrowth({
|
||||
this.stage,
|
||||
this.currentSfpi,
|
||||
this.requiredSfpi,
|
||||
this.members,
|
||||
this.requiredMembers,
|
||||
this.nebulaGoalPassed = false,
|
||||
this.isFounder = false,
|
||||
required this.fandomName,
|
||||
required this.contributorName,
|
||||
required this.contributedPop,
|
||||
required this.growthPercent,
|
||||
required this.currentPop,
|
||||
required this.requiredPop,
|
||||
});
|
||||
|
||||
/// 서버 팬덤 Stage 0~6. 누락된 단계는 추정하지 않는다.
|
||||
final int? stage;
|
||||
final String fandomName;
|
||||
|
||||
/// 직전 기여자. 「BTS_Hong 님의 1,230 POP 기여로 …」 문구에 쓴다.
|
||||
final String contributorName;
|
||||
final int contributedPop;
|
||||
|
||||
bool get hasRecentContribution =>
|
||||
contributedPop > 0 && contributorName.trim().isNotEmpty;
|
||||
|
||||
/// 0~100 POP 진행률. 전체 성장 조건의 충족률은 아니다.
|
||||
final int? growthPercent;
|
||||
|
||||
/// 다음 Stage 진입까지의 누적 진행도. 정책 설계서 §P-08 은 POP 과 sFPI 동시 충족이라
|
||||
/// 정한다. 조건 누락은 null이며 개인 기여량으로 대체하지 않는다.
|
||||
final num? currentPop;
|
||||
final int? requiredPop;
|
||||
final num? currentSfpi;
|
||||
final num? requiredSfpi;
|
||||
|
||||
/// 성운(Stage0)의 창립 멤버 수와 전환 기준 인원. 정책 원장 B-10 — 성운은 **10명 이상 +
|
||||
/// 1,000 POP 이상**을 동시에 채워야 정식 행성(Stage1)이 된다. 기준값이 서버에서 오지
|
||||
/// 않으면 null 이며 정책 수치로 채우지 않는다.
|
||||
final int? members;
|
||||
final int? requiredMembers;
|
||||
|
||||
/// 성운의 목표 기간(기본 7일)이 지났지만 전환은 여전히 열려 있는가 — 해석 확인 요청 Q-PN-02
|
||||
/// 권고 A안. 남은 일수 대신 「목표 기간이 지났어요 · 조건을 채우면 …」를 보인다.
|
||||
final bool nebulaGoalPassed;
|
||||
|
||||
/// 내가 이 성운을 만든 창립자(`fandoms.founderUid`)인가 — 해석 확인 요청 Q-PN-01 A안.
|
||||
/// 탄생(형태 · 컬러 선택)은 창립자만 한다. 서버(`fn-planet-create`)도 같은 검사를 한다.
|
||||
final bool isFounder;
|
||||
|
||||
/// 성운 단계인가(B-08 Stage0 / Pre-Planet).
|
||||
bool get isNebula => stage == 0;
|
||||
|
||||
/// 성운 → 행성 전환 조건을 둘 다 채웠는가. 하나라도 모르면 채우지 않은 것으로 본다.
|
||||
bool get nebulaReady =>
|
||||
isNebula &&
|
||||
_met(currentPop, requiredPop) == true &&
|
||||
_met(members, requiredMembers) == true;
|
||||
|
||||
/// 조건 미조회와 실제 0을 구분한다. 승급 자체는 서버 확정으로만 처리한다.
|
||||
String get popConditionLabel => _condition('POP', currentPop, requiredPop);
|
||||
String get sfpiConditionLabel =>
|
||||
_condition('sFPI', currentSfpi, requiredSfpi);
|
||||
String get memberConditionLabel =>
|
||||
_condition(appL10n.growthFounders, members, requiredMembers);
|
||||
String get growthStatus => stage == 6
|
||||
? appL10n.growthFinalStage
|
||||
: _met(currentPop, requiredPop) == true &&
|
||||
_met(currentSfpi, requiredSfpi) == true
|
||||
? appL10n.growthPromotionPending
|
||||
: appL10n.growthCheckConditions;
|
||||
|
||||
double? get popRatio =>
|
||||
currentPop == null || requiredPop == null || requiredPop! <= 0
|
||||
? null
|
||||
: (currentPop! / requiredPop!).clamp(0.0, 1.0);
|
||||
|
||||
/// 조건 충족 여부. 값을 모르면 null.
|
||||
static bool? _met(num? current, num? required) {
|
||||
if (current == null ||
|
||||
required == null ||
|
||||
required < 0 ||
|
||||
!current.isFinite ||
|
||||
!required.isFinite)
|
||||
return null;
|
||||
return current >= required;
|
||||
}
|
||||
|
||||
static String _condition(String name, num? current, num? required) =>
|
||||
switch (_met(current, required)) {
|
||||
null => appL10n.growthConditionChecking(name),
|
||||
true => appL10n.growthConditionMet(name),
|
||||
false => appL10n.growthConditionUnmet(name),
|
||||
};
|
||||
}
|
||||
|
||||
/// 오늘의 퀘스트 한 줄.
|
||||
enum QuestState {
|
||||
/// 조건을 채웠고 보상을 아직 받지 않았다.
|
||||
claimable,
|
||||
|
||||
/// 진행 중.
|
||||
inProgress,
|
||||
|
||||
/// 보상까지 받았다.
|
||||
done,
|
||||
}
|
||||
|
||||
class QuestSummary {
|
||||
const QuestSummary({
|
||||
required this.title,
|
||||
required this.rewardPop,
|
||||
required this.state,
|
||||
this.progressLabel,
|
||||
this.id = '',
|
||||
this.endsAt,
|
||||
});
|
||||
|
||||
final String title;
|
||||
|
||||
/// 보상 POP — 서버 지급액과 같은 원천. 못 읽으면 null.
|
||||
final int? rewardPop;
|
||||
final QuestState state;
|
||||
|
||||
/// 「0/1,500 걸음」 같은 진행 표기. 없으면 표시하지 않는다.
|
||||
final String? progressLabel;
|
||||
|
||||
/// `quests` 문서 ID. 타임어택 카드가 상세로 바로 들어갈 때 쓴다.
|
||||
final String id;
|
||||
|
||||
/// 타임어택 마감(`quests.activeTo`). 있으면 카드가 남은 시간을 센다.
|
||||
final DateTime? endsAt;
|
||||
}
|
||||
|
||||
/// AI Daily Briefing 구획 — 정책 원장 B-47 이 News 와 SNS 둘로 확정했다.
|
||||
///
|
||||
/// *News/SNS 각각 0 건이면 해당 섹션 숨김, 모두 0 건이면 전체 Briefing 블록을 숨김*
|
||||
enum BriefingSection {
|
||||
news('News', 'NEWS'),
|
||||
sns('SNS', 'SNS');
|
||||
|
||||
const BriefingSection(this.key, this.label);
|
||||
|
||||
/// 서버 `briefings.section` 값. 대소문자까지 서버와 같아야 한다.
|
||||
final String key;
|
||||
|
||||
/// 구획 제목.
|
||||
final String label;
|
||||
|
||||
/// 서버 값에서 구획을 찾는다. 모르는 값은 null 이고 화면이 그 줄을 버린다 —
|
||||
/// 정책이 정한 둘 밖의 섹션을 임의로 그리면 근거 없는 구획이 생긴다.
|
||||
static BriefingSection? of(String key) {
|
||||
for (final section in values) {
|
||||
if (section.key == key) return section;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// AI Daily Briefing 한 줄 — 정책 원장 B-47 · B-48 · B-75.
|
||||
///
|
||||
/// **문안은 발주사 Agent 엔진이 만든다.** B-48 이 「외부 Agent 엔진은 발주사 영역,
|
||||
/// 저희 범위는 Candidate/Draft 수신 · Admin 승인 · 노출」로 확정했다.
|
||||
///
|
||||
/// **5 요소가 전부 필수다.** B-75 — *각 항목에 출처명·게시/발행시각·원본 URL·원문보기·*
|
||||
/// *AI 요약 표시를 제공합니다. 출처 확인 불가 AI 내용은 게시하지 않습니다.* 필드를
|
||||
/// 선택값으로 두면 메타가 빈 카드가 화면까지 내려오고, 그것이 곧 B-75 위반이다.
|
||||
/// 서버도 같은 이유로 수신 단계에서 거부한다(`fn-briefing-ingest`).
|
||||
///
|
||||
/// 화면 표시는 지금 제목 한 줄이다. 출처명·게시시각·AI 요약을 어디에 배치할지는
|
||||
/// 발주사가 HOME 기준으로 지정한 Figma 「REFINE v5A · HOME · Default」 확인 후
|
||||
/// 정한다 — 임의로 보조행을 넣지 않는다.
|
||||
class BriefingItem {
|
||||
const BriefingItem({
|
||||
required this.rank,
|
||||
required this.headline,
|
||||
required this.section,
|
||||
required this.sourceName,
|
||||
required this.sourceAt,
|
||||
required this.sourceUrl,
|
||||
required this.articleUrl,
|
||||
required this.aiSummary,
|
||||
});
|
||||
|
||||
/// 구획 안에서의 순서. 구획마다 1 부터 시작한다.
|
||||
final int rank;
|
||||
|
||||
/// 카드에 뜨는 제목.
|
||||
final String headline;
|
||||
|
||||
final BriefingSection section;
|
||||
|
||||
/// 출처명 — 언론사·계정명.
|
||||
final String sourceName;
|
||||
|
||||
/// 게시·발행 시각.
|
||||
final DateTime sourceAt;
|
||||
|
||||
/// 원본 URL.
|
||||
final String sourceUrl;
|
||||
|
||||
/// 원문보기 링크. 원본과 같을 수 있다.
|
||||
final String articleUrl;
|
||||
|
||||
/// AI 요약 본문.
|
||||
final String aiSummary;
|
||||
}
|
||||
|
||||
/// HOT ARCHIVE — 진행 중인 캠페인 한 건.
|
||||
class CampaignHighlight {
|
||||
const CampaignHighlight({
|
||||
required this.authorName,
|
||||
required this.achievedPercent,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.supporters,
|
||||
required this.comments,
|
||||
required this.likes,
|
||||
});
|
||||
|
||||
final String authorName;
|
||||
final int achievedPercent;
|
||||
final String title;
|
||||
final String description;
|
||||
final int supporters;
|
||||
final int comments;
|
||||
final int likes;
|
||||
}
|
||||
|
||||
/// 인기 K-INSIDE 한 건.
|
||||
class InsideDigest {
|
||||
const InsideDigest({
|
||||
required this.authorName,
|
||||
required this.timeAgo,
|
||||
required this.bountyPop,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.answers,
|
||||
});
|
||||
|
||||
final String authorName;
|
||||
final String timeAgo;
|
||||
|
||||
/// 현상금. 정책 설계서 §P-11 — 채택 기한은 아직 미확정이다.
|
||||
final int bountyPop;
|
||||
final String title;
|
||||
final String body;
|
||||
final int answers;
|
||||
}
|
||||
|
||||
/// 홈 한 화면이 필요로 하는 것 전부.
|
||||
class HomeSnapshot {
|
||||
const HomeSnapshot({
|
||||
required this.popBalance,
|
||||
required this.liveTicker,
|
||||
this.fandomId = '',
|
||||
required this.growth,
|
||||
required this.quests,
|
||||
required this.briefings,
|
||||
required this.campaign,
|
||||
required this.insides,
|
||||
this.items = const [],
|
||||
});
|
||||
|
||||
/// 활동 POP 잔액. 정책 설계서 §P-02 — 활동/지원 원장은 분리되어 있고
|
||||
/// 홈 상단에는 활동 POP 을 띄운다.
|
||||
final int popBalance;
|
||||
|
||||
/// 내 소속 팬덤. 이클립스 경보 배너와 라이브 띠가 **어느 팬덤을 구독할지**를
|
||||
/// 이 값으로 정한다. 비어 있으면 소속이 없다는 뜻이라 둘 다 뜨지 않는다.
|
||||
final String fandomId;
|
||||
|
||||
/// 상단 라이브 띠의 **폴백** 문구. 원천은 `fandoms/{id}/liveEvents` 구독이고
|
||||
/// (별첨 1 ① 시나리오 5) 이 문자열은 라이브 이벤트가 아직 없을 때만 쓴다.
|
||||
/// `fandoms/{id}.liveTicker` 를 쓰는 서버 코드는 시드 스크립트뿐이다.
|
||||
final String? liveTicker;
|
||||
|
||||
final PlanetGrowth growth;
|
||||
final List<QuestSummary> quests;
|
||||
final List<BriefingItem> briefings;
|
||||
final CampaignHighlight? campaign;
|
||||
final List<InsideDigest> insides;
|
||||
|
||||
/// 헤더 지갑 · ZAP 「아이템」 시트의 상품. Admin Catalog(B-84 · B-85)
|
||||
/// `heroGiftProducts` 에서 온다.
|
||||
final List<ItemProduct> items;
|
||||
}
|
||||
|
||||
/// 아이템(Hero Gift) 상품 한 줄 — 사본 `아이템 구입`(`252:160233`) 카드.
|
||||
///
|
||||
/// 구매하면 구매자에게 기여 POP, 팬덤 Drop Pool 에 같은 수량의 일반 POP 이 생긴다
|
||||
/// (원장 B-84). 수치는 Admin 저장값이며 표본은 사본 문구다.
|
||||
class ItemProduct {
|
||||
const ItemProduct({
|
||||
this.productId = '',
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.dropPop,
|
||||
required this.buyerPop,
|
||||
required this.priceWon,
|
||||
required this.owned,
|
||||
});
|
||||
|
||||
/// 스토어 상품 ID(= Catalog 문서 ID). 표본은 비어 있다.
|
||||
final String productId;
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
/// 「N POP 전송」 — 팬덤 Drop Pool 에 생기는 일반 POP.
|
||||
final int dropPop;
|
||||
|
||||
/// 「구매자 +N POP」.
|
||||
final int buyerPop;
|
||||
final int priceWon;
|
||||
|
||||
/// 보유 수량.
|
||||
final int owned;
|
||||
}
|
||||
|
||||
/// 홈이 값을 얻어 오는 경로.
|
||||
///
|
||||
/// 5주차 Firestore 연동 때 이 인터페이스의 구현체만 바꾼다.
|
||||
abstract interface class HomeRepository {
|
||||
Future<HomeSnapshot> load();
|
||||
Stream<PlanetGrowth> watchGrowth(String fandomId);
|
||||
|
||||
/// 성운(Stage0) 생성 — `fn-nebula-create`(원장 B-08 · B-09). 만든 팬덤 ID 와, 대표 팬덤이
|
||||
/// 바뀌었는지(C-15 30일 대기 중이면 false — 성운만 생기고 대표는 그대로)를 돌려준다.
|
||||
Future<({String fandomId, bool primaryChanged})> createNebula(String name);
|
||||
|
||||
/// 성운 → 정식 행성 탄생 — `fn-planet-create`. 형태 · 컬러 두 값만 보낸다(C-50).
|
||||
Future<void> createPlanet({
|
||||
required String fandomId,
|
||||
required String planetType,
|
||||
required String colorKey,
|
||||
});
|
||||
|
||||
/// 성운 전환 목표 — 서버(`fn-nebula-create` · `fn-planet-create`)와 같은 `config/policy`
|
||||
/// 키(`nebulaMinPop` · `nebulaMinMembers` · `nebulaDays`). 읽지 못하면 [defaultNebulaTargets].
|
||||
Future<NebulaTargets> nebulaTargets();
|
||||
}
|
||||
|
||||
/// 성운 전환 목표 — POP · 창립 인원 · 목표 기간(일).
|
||||
typedef NebulaTargets = ({int pop, int members, int days});
|
||||
|
||||
/// 정책 원장 B-10 확정값. `config/policy` 가 없을 때의 출발점이다.
|
||||
const NebulaTargets defaultNebulaTargets = (pop: 1000, members: 10, days: 7);
|
||||
|
||||
/// `config/policy` 문서에서 성운 목표를 읽는다. 키가 없거나 잘못된 값이면 기본값.
|
||||
NebulaTargets nebulaTargetsFrom(Map<String, dynamic>? policy) {
|
||||
int read(String key, int fallback) => switch (policy?[key]) {
|
||||
num n when n.isFinite && n > 0 => n.toInt(),
|
||||
_ => fallback,
|
||||
};
|
||||
return (
|
||||
pop: read('nebulaMinPop', defaultNebulaTargets.pop),
|
||||
members: read('nebulaMinMembers', defaultNebulaTargets.members),
|
||||
days: read('nebulaDays', defaultNebulaTargets.days),
|
||||
);
|
||||
}
|
||||
|
||||
/// 서버가 붙기 전까지 쓰는 표본.
|
||||
///
|
||||
/// **정책값이 아니다.** 화면이 어떤 모양으로 서는지 확인하기 위한 것이며,
|
||||
/// 실제 수치는 수치표 v1.4 와 정책 설계서가 정한다.
|
||||
class SampleHomeRepository implements HomeRepository {
|
||||
const SampleHomeRepository();
|
||||
|
||||
@override
|
||||
Stream<PlanetGrowth> watchGrowth(String fandomId) => const Stream.empty();
|
||||
|
||||
@override
|
||||
Future<NebulaTargets> nebulaTargets() async => defaultNebulaTargets;
|
||||
|
||||
@override
|
||||
Future<({String fandomId, bool primaryChanged})> createNebula(
|
||||
String name,
|
||||
) async => (fandomId: 'bts', primaryChanged: true);
|
||||
|
||||
@override
|
||||
Future<void> createPlanet({
|
||||
required String fandomId,
|
||||
required String planetType,
|
||||
required String colorKey,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
// 브리핑 표본이 `sourceAt` 에 DateTime 을 담아 const 로 둘 수 없다.
|
||||
Future<HomeSnapshot> load() async => HomeSnapshot(
|
||||
popBalance: 12540,
|
||||
fandomId: 'bts',
|
||||
liveTicker: '[LIVE] 아미보이님의 아이템 사용으로 버프가 발동되었습니다!',
|
||||
growth: PlanetGrowth(
|
||||
fandomName: 'BTS',
|
||||
contributorName: 'BTS_Hong',
|
||||
contributedPop: 1230,
|
||||
growthPercent: 67,
|
||||
currentPop: 6270,
|
||||
requiredPop: 10000,
|
||||
),
|
||||
quests: [
|
||||
QuestSummary(
|
||||
title: '오늘의 출석체크',
|
||||
// 서버 `config/policy.attendPop` 기본값과 맞춘다. Figma 의 50 이 아니다.
|
||||
rewardPop: 10,
|
||||
state: QuestState.claimable,
|
||||
),
|
||||
QuestSummary(
|
||||
title: '만보기',
|
||||
rewardPop: 150,
|
||||
state: QuestState.inProgress,
|
||||
progressLabel: '0/1,500 걸음',
|
||||
),
|
||||
],
|
||||
briefings: _sampleBriefings,
|
||||
campaign: CampaignHighlight(
|
||||
authorName: '다이브짱',
|
||||
achievedPercent: 92,
|
||||
title: '강남역 초대형 LED 광고',
|
||||
description: 'BTS 데뷔 기념 팬 연합의 초대형 프로젝트입니다. 도심을 BTS의 색으로 물들여주세요.',
|
||||
supporters: 48,
|
||||
comments: 24,
|
||||
likes: 82,
|
||||
),
|
||||
// 사본 `아이템 구입` 표본(Admin 값 아님).
|
||||
items: const [
|
||||
ItemProduct(
|
||||
name: '베이직 드랍',
|
||||
description: '소소하지만 확실한 팬덤 기여',
|
||||
dropPop: 500,
|
||||
buyerPop: 100,
|
||||
priceWon: 1200,
|
||||
owned: 11,
|
||||
),
|
||||
ItemProduct(
|
||||
name: '메가 드랍',
|
||||
description: '팬덤의 열기를 달구는 강력한 화력',
|
||||
dropPop: 2000,
|
||||
buyerPop: 500,
|
||||
priceWon: 3900,
|
||||
owned: 10,
|
||||
),
|
||||
ItemProduct(
|
||||
name: '에픽 드랍',
|
||||
description: '전설적인 영웅의 등장, 압도적 물량',
|
||||
dropPop: 5000,
|
||||
buyerPop: 1500,
|
||||
priceWon: 8900,
|
||||
owned: 10,
|
||||
),
|
||||
],
|
||||
insides: [
|
||||
InsideDigest(
|
||||
authorName: '아미보이',
|
||||
timeAgo: '10분 전',
|
||||
bountyPop: 500,
|
||||
title: '화양연화 세계관 해석해주실 분?',
|
||||
body: '이번 뮤비랑 연결되는 부분이 헷갈리네요. 타임라인 정리해주실 천재 아미 구합니다!',
|
||||
answers: 3,
|
||||
),
|
||||
InsideDigest(
|
||||
authorName: '다이브짱',
|
||||
timeAgo: '1시간 전',
|
||||
bountyPop: 200,
|
||||
title: '공방 신청 꿀팁 공유합니다.',
|
||||
body: '공방 신청할 때 폼 가장 빨리 제출하는 손크로 팁 알려드립! 궁금하신 분 질문 남겨주세요',
|
||||
answers: 8,
|
||||
),
|
||||
InsideDigest(
|
||||
authorName: '네누',
|
||||
timeAgo: '3시간 전',
|
||||
bountyPop: 100,
|
||||
title: 'BTS 이 포즈 뭔가요?',
|
||||
body: '아미 분들 이 포즈 뭔지 알려주실 수 있나요?',
|
||||
answers: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 브리핑 표본 — **정책값이 아니다.** B-75 5 요소를 전건 채워 두어 화면이 무엇을
|
||||
/// 받게 되는지 보인다. News 2 건 · SNS 1 건으로 구획 분기도 함께 확인된다.
|
||||
final List<BriefingItem> _sampleBriefings = [
|
||||
BriefingItem(
|
||||
rank: 1,
|
||||
section: BriefingSection.news,
|
||||
headline: 'NewJeans, 빌보드 ‘Women in Music’ 어워드 올해의 그룹상 수상 확정',
|
||||
sourceName: '연합뉴스',
|
||||
sourceAt: DateTime(2026, 9, 14, 8, 20),
|
||||
sourceUrl: 'https://example.com/news/newjeans-billboard',
|
||||
articleUrl: 'https://example.com/news/newjeans-billboard',
|
||||
aiSummary: '빌보드가 올해의 그룹상 수상자를 확정해 발표했습니다.',
|
||||
),
|
||||
BriefingItem(
|
||||
rank: 2,
|
||||
section: BriefingSection.news,
|
||||
headline: 'NewJeans, ‘Super Shy’, 스포티파이 글로벌 차트 장기 집권 중',
|
||||
sourceName: '스포티파이 차트',
|
||||
sourceAt: DateTime(2026, 9, 14, 7, 5),
|
||||
sourceUrl: 'https://example.com/news/super-shy-chart',
|
||||
articleUrl: 'https://example.com/news/super-shy-chart',
|
||||
aiSummary: '글로벌 차트 상위권을 여러 주 유지하고 있습니다.',
|
||||
),
|
||||
BriefingItem(
|
||||
rank: 1,
|
||||
section: BriefingSection.sns,
|
||||
headline: '버니즈를 위한 도쿄돔 팬미팅 비하인드 독점 공개 예정',
|
||||
sourceName: '공식 X 계정',
|
||||
sourceAt: DateTime(2026, 9, 14, 6, 40),
|
||||
sourceUrl: 'https://example.com/sns/tokyo-dome',
|
||||
articleUrl: 'https://example.com/sns/tokyo-dome',
|
||||
aiSummary: '팬미팅 비하인드 영상 공개 일정이 예고되었습니다.',
|
||||
),
|
||||
];
|
||||
Reference in New Issue
Block a user