퀘스트 제목 다국어 · HOME 다시 읽기 · 내 퀘스트 검토 중·반려됨 구분

This commit is contained in:
Sn-Kinos
2026-10-06 01:22:28 +09:00
parent cf3dd06774
commit 83d9c1e8d4
10 changed files with 139 additions and 26 deletions
@@ -9,7 +9,7 @@ import 'package:flutter/painting.dart' show Color;
import 'home_data.dart';
import '../inside/inside_repository_firestore.dart' show insideQuestionFrom;
import '../quest/quest_data.dart' show QuestType;
import '../quest/quest_repository_firestore.dart' show openTimeAttack, questToday;
import '../quest/quest_repository_firestore.dart' show openTimeAttack, questTitle, questToday;
import '../../core/i18n/app_locale.dart';
/// 홈의 실 서버 구현 — 화면 대조 원장 F1.
@@ -426,7 +426,7 @@ class FirestoreHomeRepository implements HomeRepository {
);
final started = count > 0;
return QuestSummary(
title: quest.strOr('title'),
title: questTitle(quest),
type: QuestType.parse(quest.strOr('type')),
category: QuestType.parse(quest.strOr('type')).label,
rewardPop: openTimeAttack(quest)?.rewardPop ??
+26 -2
View File
@@ -57,6 +57,19 @@ import 'widgets/briefing_share.dart';
///
/// 값은 [HomeRepository] 가 준다. 지금은 [SampleHomeRepository] 라 서버 없이 뜨고,
/// 5주차 Firestore 연동 때 구현체만 바꾼다.
/// HOME 을 다시 읽으라는 신호 — 포그라운드 Push 수신 · 위에 띄운 화면이 닫힘 · HOME 탭 복귀.
/// HOME 은 진입 때 한 번 읽어서 타임어택 발동 Push · 후원 뒤에도 카드가 낡아 있었다(10/6 실기기).
/// ponytail: 어느 화면이 닫혀도 HOME 을 한 번 다시 읽는다. 읽기 비용이 문제면 HOME 이 보일 때만.
final ValueNotifier<int> homeRefreshSignal = ValueNotifier(0);
/// 페이지(시트 · 대화상자 제외)가 닫히면 [homeRefreshSignal] 을 울린다. 루트 · HOME 탭 내비게이터에 하나씩.
class HomeRefreshObserver extends NavigatorObserver {
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route is PageRoute) homeRefreshSignal.value++;
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({
super.key,
@@ -97,7 +110,7 @@ class HomeScreen extends StatefulWidget {
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
late Future<HomeSnapshot> _snapshot;
Future<GalaxySnapshot>? _galaxy;
ZapController? _zap;
@@ -113,13 +126,22 @@ class _HomeScreenState extends State<HomeScreen> {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
homeRefreshSignal.removeListener(_refresh);
_zap?.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) _refresh();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
homeRefreshSignal.addListener(_refresh);
_snapshot = widget.repository.load();
if (widget.galaxyRepository case final repository?) {
_galaxy = repository.load();
@@ -156,6 +178,7 @@ class _HomeScreenState extends State<HomeScreen> {
}
Future<void> _refresh() async {
if (!mounted) return;
final next = widget.repository.load();
setState(() => _snapshot = next);
await next;
@@ -176,7 +199,8 @@ class _HomeScreenState extends State<HomeScreen> {
body: FutureBuilder<HomeSnapshot>(
future: _snapshot,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
// 다시 읽는 동안은 앞 데이터를 그대로 둔다 — 화면이 스피너로 깜빡이지 않게.
if (snap.connectionState != ConnectionState.done && !snap.hasData) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
+6
View File
@@ -332,12 +332,17 @@ class QuestCard {
this.adMultiplier = 2,
this.ugcAdPop = 10,
this.endsAt,
this.publishState = '',
});
final String id;
final String title;
final String categoryName;
/// 내가 만든 Quest 의 공개 상태(`quests.status`) — `review` · `published` · `rejected` 등.
/// 「내 퀘스트」 카드가 검수 중 · 반려를 공개된 것과 구분해 보인다(10/6 실기기 4-2).
final String publishState;
/// 검증 경로를 고르는 유형. 표본 데이터는 지정하지 않아 [QuestType.unknown] 이다.
final QuestType type;
final QuestAuthor author;
@@ -393,6 +398,7 @@ class QuestCard {
adMultiplier: adMultiplier,
ugcAdPop: ugcAdPop,
endsAt: endsAt,
publishState: publishState,
);
}
@@ -178,7 +178,7 @@ class FirestoreQuestRepository implements QuestRepository {
return QuestCard(
id: id,
title: _title(quest),
title: questTitle(quest),
categoryName: type.label,
type: type,
author: authorUid.isEmpty ? QuestAuthor.admin : QuestAuthor.user,
@@ -203,22 +203,11 @@ class FirestoreQuestRepository implements QuestRepository {
ugcAdPop: policy.intOr('adRewardPop', 10),
// 열린 TIME ATTACK 회차의 마감. 있으면 카드 · 상세가 남은 시간을 세고 참여는 회차로 간다.
endsAt: openTimeAttack(quest)?.endsAt,
publishState: authorUid == uid ? quest.strOr('status') : '',
);
}
/// 다국어 제목 — 설계서 §2.8 은 `title`(다국어)이라고만 적었다.
/// 문자열과 지도(map) 둘 다 들어올 수 있어 둘 다 받는다.
String _title(Map<String, dynamic> quest) {
final raw = quest['title'];
if (raw is String) return raw;
if (raw is Map) {
final map = Map<String, dynamic>.from(raw);
return map.strOr('ko', map.strOr('en'));
}
return '';
}
Future<Map<String, dynamic>> _policy() async {
final snap = await _backend.db.doc('config/policy').get();
return snap.data() ?? const <String, dynamic>{};
@@ -635,7 +624,7 @@ class FirestoreQuestRepository implements QuestRepository {
}
return MusicShakeSetup(
videoId: videoId,
title: link.strOr('title', _title(quest)),
title: link.strOr('title', questTitle(quest)),
bpm: bpm.toDouble(),
segments: segments,
windows: ShakeWindows.from(await _policy()),
@@ -792,7 +781,14 @@ List<int> _stepTiers(Map<String, dynamic> policy) {
/// 열린 TIME ATTACK 회차 — 원 Quest 문서의 `timeAttack`(10/3 통합회신: 별도 유형이 아니라
/// 기존 Quest 에 붙는 제한시간 모드). 창(`activeFrom` ~ `activeTo`, 서버 시각) 밖이면 null.
/// 화면 표시용이다 — 참여 수락은 서버(`fn-quest-timeattack`)가 서버 시각으로 다시 판정한다.
({String roundId, DateTime endsAt, int rewardPop})? openTimeAttack(
/// 카드 · 상세에 보일 Quest 제목. 열린 TIME ATTACK 회차면 회차 제목(`timeAttack.title`)이 먼저다.
/// 운영 Quest 제목은 언어별 지도라 [SnapshotReader.textOr] 로 읽는다(설계서 §2.8 `title` 다국어).
String questTitle(Map<String, dynamic> quest) {
final round = openTimeAttack(quest)?.title ?? '';
return round.isNotEmpty ? round : quest.textOr('title');
}
({String roundId, DateTime endsAt, int rewardPop, String title})? openTimeAttack(
Map<String, dynamic> quest,
) {
final raw = quest['timeAttack'];
@@ -807,5 +803,6 @@ List<int> _stepTiers(Map<String, dynamic> policy) {
roundId: round.strOr('roundId'),
endsAt: to.toDate(),
rewardPop: round.intOr('rewardPop'),
title: round.textOr('title'),
);
}
@@ -232,7 +232,14 @@ class QuestCardTile extends StatelessWidget {
height: 1,
),
),
child: Text(card.stage.buttonLabel),
// 내가 만든 Quest 가 검수 중 · 반려면 「자세히」 자리에 상태를 적는다(누르면 상세).
child: Text(switch (card) {
QuestCard(stage: QuestStage.mine, publishState: 'review') =>
context.l10n.questReviewButton,
QuestCard(stage: QuestStage.mine, publishState: 'rejected') =>
context.l10n.questRejectedButton,
_ => card.stage.buttonLabel,
}),
),
],
],