화면 하단 여백 · 로그아웃 진행 · 스낵바 강조색 · TODAY'S QUEST 관리자 퀘스트 3개

This commit is contained in:
Sn-Kinos
2026-10-06 02:29:09 +09:00
parent 82662b9b9f
commit 64825e72b0
17 changed files with 104 additions and 67 deletions
@@ -4,12 +4,14 @@ import 'package:cloud_firestore/cloud_firestore.dart'
import '../../core/data/backend.dart';
import '../../core/data/invite_code.dart';
import '../../core/data/server_clock.dart';
import '../../core/ui/planet_25d.dart' show planetColors, planetColorsFrom, setPlanetColorMaster;
import '../../core/ui/planet_25d.dart'
show planetColors, planetColorsFrom, setPlanetColorMaster;
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, questTitle, questToday;
import '../quest/quest_repository_firestore.dart'
show openTimeAttack, questTitle, questToday;
import '../../core/i18n/app_locale.dart';
/// 홈의 실 서버 구현 — 화면 대조 원장 F1.
@@ -117,23 +119,6 @@ class FirestoreHomeRepository implements HomeRepository {
.where('active', isEqualTo: true)
.limit(30)
.get();
// 긴급 돌발 타임어택(FR-QST-06) — 내 팬덤의 **열린 창**만. 위 목록과 따로 읽어 맨 앞에
// 둔다. 위 질의는 정렬이 없어 5 건 안에 든다는 보장이 없다. 색인 `firestore.indexes.json`.
// 질의는 단말 시계 기준 1시간 여유로 넓게 받고, 마감 판정은 아래에서 서버 시각으로 한다.
// ponytail: 단말 시계가 1시간 넘게 빠르면 열린 창을 놓친다. 그때는 여유를 늘린다.
final timeAttacksF = fandomId.isEmpty
? null
: db
.collection('quests')
.where('timeAttack.fandomId', isEqualTo: fandomId)
.where(
'timeAttack.activeTo',
isGreaterThan: Timestamp.fromDate(
DateTime.now().subtract(const Duration(hours: 1)),
),
)
.limit(10)
.get();
final progressF = db.collection('users/$uid/questProgress').get();
// 출석 보상은 서버가 `config/policy.attendPop` 으로 지급한다. 같은 곳을 읽는다.
final policyF = db.doc('config/policy').get();
@@ -199,24 +184,12 @@ class FirestoreHomeRepository implements HomeRepository {
final fandom = fandomF == null
? const <String, dynamic>{}
: ((await fandomF).data() ?? const <String, dynamic>{});
final timeAttacks = [
for (final doc in (await timeAttacksF)?.docs ?? const [])
if (doc.data()['active'] != false) doc,
];
if (timeAttacks.isNotEmpty) await ServerClock.ensure(_backend);
final open = [
for (final d in timeAttacks)
if (openTimeAttack(d.data()) != null) d,
];
final openIds = {for (final d in open) d.id};
final quests = [
...open,
// 회차가 열린 Quest 는 위에서 맨 앞에 넣었다. 두 번 넣지 않는다.
...adminQuestsLatest([
for (final d in (await questsF).docs)
if (!openIds.contains(d.id)) d,
], (d) => d.data()),
].take(3).toList();
// 설명 프레임 `2097:16397` 그대로 — 관리자 퀘스트 최신순 최대 3개. 열린 타임어택 회차를
// 맨 앞에 끼우던 처리는 발주사 자료에 근거가 없어 뺐다(회차는 Push · 퀘스트 목록으로 안내).
final quests = adminQuestsLatest(
(await questsF).docs,
(d) => d.data(),
).take(3).toList();
final progress = (await progressF).docs;
final briefings = (await briefingsF).docs;
final blocked = {for (final d in (await blocksF).docs) d.id};
@@ -330,7 +303,11 @@ class FirestoreHomeRepository implements HomeRepository {
/// 이번 주 내가 [fandomId] 행성 성장에 보탠 Growth POP — 원장과 같은 트랜잭션에서 쓰는
/// `users/{uid}/growthEvents`(역분개는 음수 행)의 [since] 이후 합. 읽지 못하면 null.
// ponytail: 한 주치 성장 행을 전부 읽어 합한다. 한 사용자가 주에 수천 건을 넘기면 서버 집계 필드로 옮긴다.
Future<num?> _myWeekGrowth(String uid, String fandomId, DateTime since) async {
Future<num?> _myWeekGrowth(
String uid,
String fandomId,
DateTime since,
) async {
try {
final q = await _backend.db
.collection('users/$uid/growthEvents')
@@ -340,7 +317,8 @@ class FirestoreHomeRepository implements HomeRepository {
for (final d in q.docs) {
final data = d.data();
final amount = data['amount'];
if (data['fandomIdAtPayout'] == fandomId && amount is num) sum += amount;
if (data['fandomIdAtPayout'] == fandomId && amount is num)
sum += amount;
}
return sum;
} catch (_) {
@@ -429,7 +407,8 @@ class FirestoreHomeRepository implements HomeRepository {
title: questTitle(quest),
type: QuestType.parse(quest.strOr('type')),
category: QuestType.parse(quest.strOr('type')).label,
rewardPop: openTimeAttack(quest)?.rewardPop ??
rewardPop:
openTimeAttack(quest)?.rewardPop ??
QuestType.parse(quest.strOr('type')).rewardFrom(quest, policy),
state: done
? QuestState.done