HOME 을 Figma 「홈」 구성으로 — TODAY'S QUEST · 미니맵·확장 맵(프로토타입 v6.2) · 행성 정보 박스 · 인큐베이터 · 아티스트 소식 · HOT ARCHIVE · 인기 K-INSIDE
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
library;
|
||||
|
||||
import '../../core/i18n/app_locale.dart';
|
||||
import '../quest/quest_data.dart' show QuestType;
|
||||
import '../inside/inside_data.dart' show InsideQuestion, SampleInsideRepository;
|
||||
|
||||
/// 팬덤 행성의 성장 상태. 정책 근거는 정책 설계서 §P-08.
|
||||
class PlanetGrowth {
|
||||
@@ -85,6 +87,45 @@ class PlanetGrowth {
|
||||
? appL10n.growthPromotionPending
|
||||
: appL10n.growthCheckConditions;
|
||||
|
||||
/// 「행성 성장률 N%」 · 「현재 / 목표」의 기준 — 두 조건 중 **진행률이 낮은 쪽(병목)**.
|
||||
///
|
||||
/// Figma 「홈」(`2097:16428` `Frame 1984081073`)은 성장률 하나 · 수치 한 쌍이고, 정책은
|
||||
/// 두 조건 동시 충족이다(원장 B-11 · 성운은 POP + 창립 인원 B-10). 다음 진화는 늦은 쪽이
|
||||
/// 채워질 때 오므로 그 축을 보인다. 두 조건 상세는 말풍선을 누르면 연다.
|
||||
/// ponytail: 해석 확인 전 임시 기준(HOME 정보위계 v1.0 §3 은 「단일 백분율로 합치지 않음」).
|
||||
/// 값을 모르는 축은 건너뛰고, 둘 다 모르면 null.
|
||||
({double ratio, num current, num required})? get bottleneck {
|
||||
final axes = [
|
||||
(currentPop, requiredPop),
|
||||
isNebula ? (members, requiredMembers) : (currentSfpi, requiredSfpi),
|
||||
];
|
||||
({double ratio, num current, num required})? low;
|
||||
for (final (cur, req) in axes) {
|
||||
if (cur == null || req == null || req <= 0 || !cur.isFinite) continue;
|
||||
final r = (cur / req).clamp(0.0, 1.0).toDouble();
|
||||
if (low == null || r < low.ratio) {
|
||||
low = (ratio: r, current: cur, required: req);
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
|
||||
/// 말풍선 문구. Figma 문구는 「다음 진화까지 얼마 안남았어요!」 하나다 — 병목 진행률이
|
||||
/// 절반 이상일 때만 그 문구를 쓰고, 그 아래는 「성장 조건 확인」(누르면 두 조건 상세).
|
||||
/// ponytail: 절반 기준은 근거 없는 임시값 — 상태별 문구가 정해지면 바꾼다.
|
||||
String get growthBubble => stage == 6
|
||||
? appL10n.growthFinalStage
|
||||
: _met(currentPop, requiredPop) == true &&
|
||||
_met(
|
||||
isNebula ? members : currentSfpi,
|
||||
isNebula ? requiredMembers : requiredSfpi,
|
||||
) ==
|
||||
true
|
||||
? appL10n.growthPromotionPending
|
||||
: (bottleneck?.ratio ?? 0) >= 0.5
|
||||
? appL10n.heroNextEvolution
|
||||
: appL10n.growthCheckConditions;
|
||||
|
||||
double? get popRatio =>
|
||||
currentPop == null || requiredPop == null || requiredPop! <= 0
|
||||
? null
|
||||
@@ -129,10 +170,19 @@ class QuestSummary {
|
||||
this.progressLabel,
|
||||
this.id = '',
|
||||
this.endsAt,
|
||||
this.type,
|
||||
this.category,
|
||||
});
|
||||
|
||||
final String title;
|
||||
|
||||
/// 보조 줄 — 퀘스트 카테고리(사본 「오늘의 출석체크」 · 「커뮤니티」). 퀘스트 목록의
|
||||
/// 카테고리 이름과 같은 원천(`QuestType.label`).
|
||||
final String? category;
|
||||
|
||||
/// `quests.type`. 줄 왼쪽 아이콘을 고르는 데만 쓴다(없으면 제목으로 고른다).
|
||||
final QuestType? type;
|
||||
|
||||
/// 보상 POP — 서버 지급액과 같은 원천. 못 읽으면 null.
|
||||
final int? rewardPop;
|
||||
final QuestState state;
|
||||
@@ -224,6 +274,7 @@ class BriefingItem {
|
||||
/// HOT ARCHIVE — 진행 중인 캠페인 한 건.
|
||||
class CampaignHighlight {
|
||||
const CampaignHighlight({
|
||||
this.id = '',
|
||||
required this.authorName,
|
||||
required this.achievedPercent,
|
||||
required this.title,
|
||||
@@ -233,6 +284,8 @@ class CampaignHighlight {
|
||||
required this.likes,
|
||||
});
|
||||
|
||||
/// `campaigns/{id}`. 「캠페인 보기」가 상세로 바로 간다(명세 §2.114-6). 비면 목록으로.
|
||||
final String id;
|
||||
final String authorName;
|
||||
final int achievedPercent;
|
||||
final String title;
|
||||
@@ -242,25 +295,22 @@ class CampaignHighlight {
|
||||
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,
|
||||
/// 팬덤행성 인큐베이터(성운 추천) 한 장 — 명세 §2.114-4 「성운명 · 가입 인원수 · 성운
|
||||
/// 이미지 · 기여율% · 기여율 그래프 · 함께 성장하기」(`2097:16403`).
|
||||
class NebulaPick {
|
||||
const NebulaPick({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.members,
|
||||
required this.percent,
|
||||
});
|
||||
|
||||
final String authorName;
|
||||
final String timeAgo;
|
||||
final String id;
|
||||
final String name;
|
||||
final int members;
|
||||
|
||||
/// 현상금. 정책 설계서 §P-11 — 채택 기한은 아직 미확정이다.
|
||||
final int bountyPop;
|
||||
final String title;
|
||||
final String body;
|
||||
final int answers;
|
||||
/// 기여율 — 누적 POP ÷ 성운 전환 목표 POP(`config/policy.nebulaMinPop`, B-10). 0~100.
|
||||
final int percent;
|
||||
}
|
||||
|
||||
/// 홈 한 화면이 필요로 하는 것 전부.
|
||||
@@ -272,9 +322,10 @@ class HomeSnapshot {
|
||||
required this.growth,
|
||||
required this.quests,
|
||||
required this.briefings,
|
||||
required this.campaign,
|
||||
this.campaigns = const [],
|
||||
required this.insides,
|
||||
this.items = const [],
|
||||
this.nebulas = const [],
|
||||
});
|
||||
|
||||
/// 활동 POP 잔액. 정책 설계서 §P-02 — 활동/지원 원장은 분리되어 있고
|
||||
@@ -293,8 +344,13 @@ class HomeSnapshot {
|
||||
final PlanetGrowth growth;
|
||||
final List<QuestSummary> quests;
|
||||
final List<BriefingItem> briefings;
|
||||
final CampaignHighlight? campaign;
|
||||
final List<InsideDigest> insides;
|
||||
|
||||
/// HOT ARCHIVE 캐러셀 — 최대 5건(명세 §2.114-6).
|
||||
final List<CampaignHighlight> campaigns;
|
||||
final List<InsideQuestion> insides;
|
||||
|
||||
/// 팬덤행성 인큐베이터 — 기여율 높은 순 최대 6개(명세 §2.114-4).
|
||||
final List<NebulaPick> nebulas;
|
||||
|
||||
/// 헤더 지갑 · ZAP 「아이템」 시트의 상품. Admin Catalog(B-84 · B-85)
|
||||
/// `heroGiftProducts` 에서 온다.
|
||||
@@ -339,6 +395,11 @@ abstract interface class HomeRepository {
|
||||
Future<HomeSnapshot> load();
|
||||
Stream<PlanetGrowth> watchGrowth(String fandomId);
|
||||
|
||||
/// 보유 활동 POP — `users/{uid}/wallet/main.activityBalance` 구독. 퀘스트 목록 헤더
|
||||
/// (`QuestRepository.popBalance`)와 같은 문서라, 다른 화면에서 생긴 변동(현상금 예치 ·
|
||||
/// 보상)이 HOME 헤더에도 바로 반영된다.
|
||||
Stream<int> watchPopBalance();
|
||||
|
||||
/// 성운(Stage0) 생성 — `fn-nebula-create`(원장 B-08 · B-09). 만든 팬덤 ID 와, 대표 팬덤이
|
||||
/// 바뀌었는지(C-15 30일 대기 중이면 false — 성운만 생기고 대표는 그대로)를 돌려준다.
|
||||
Future<({String fandomId, bool primaryChanged})> createNebula(String name);
|
||||
@@ -384,6 +445,9 @@ class SampleHomeRepository implements HomeRepository {
|
||||
@override
|
||||
Stream<PlanetGrowth> watchGrowth(String fandomId) => const Stream.empty();
|
||||
|
||||
@override
|
||||
Stream<int> watchPopBalance() => const Stream.empty();
|
||||
|
||||
@override
|
||||
Future<NebulaTargets> nebulaTargets() async => defaultNebulaTargets;
|
||||
|
||||
@@ -413,30 +477,51 @@ class SampleHomeRepository implements HomeRepository {
|
||||
currentPop: 6270,
|
||||
requiredPop: 10000,
|
||||
),
|
||||
quests: [
|
||||
// 사본 「홈」 `2097:16428` 표본 문구.
|
||||
quests: const [
|
||||
QuestSummary(
|
||||
title: '오늘의 출석체크',
|
||||
title: '출석체크',
|
||||
category: '오늘의 출석체크',
|
||||
type: QuestType.attend,
|
||||
// 서버 `config/policy.attendPop` 기본값과 맞춘다. Figma 의 50 이 아니다.
|
||||
rewardPop: 10,
|
||||
state: QuestState.claimable,
|
||||
),
|
||||
QuestSummary(
|
||||
title: '만보기',
|
||||
rewardPop: 150,
|
||||
title: '좋아요',
|
||||
category: '커뮤니티',
|
||||
rewardPop: 5,
|
||||
state: QuestState.inProgress,
|
||||
),
|
||||
QuestSummary(
|
||||
title: '댓글',
|
||||
category: '커뮤니티',
|
||||
rewardPop: 5,
|
||||
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,
|
||||
),
|
||||
campaigns: const [
|
||||
CampaignHighlight(
|
||||
authorName: '다이브짱',
|
||||
achievedPercent: 92,
|
||||
title: '강남역 초대형 LED 광고',
|
||||
description: 'BTS 데뷔 기념 팬 연합의 초대형 프로젝트입니다. 도심을 BTS의 색으로 물들여주세요.',
|
||||
supporters: 48,
|
||||
comments: 24,
|
||||
likes: 48,
|
||||
),
|
||||
],
|
||||
nebulas: const [
|
||||
NebulaPick(id: 'nebula-rm', name: 'BTS_RM', members: 1280, percent: 98),
|
||||
NebulaPick(id: 'nebula-jin', name: 'BTS_JIN', members: 940, percent: 81),
|
||||
NebulaPick(
|
||||
id: 'nebula-suga',
|
||||
name: 'BTS_SUGA',
|
||||
members: 610,
|
||||
percent: 64,
|
||||
),
|
||||
],
|
||||
// 사본 `아이템 구입` 표본(Admin 값 아님).
|
||||
items: const [
|
||||
ItemProduct(
|
||||
@@ -464,32 +549,7 @@ class SampleHomeRepository implements HomeRepository {
|
||||
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,
|
||||
),
|
||||
],
|
||||
insides: SampleInsideRepository.samples.take(3).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user