사용자 화면 문구·표본 데이터 · 퀘스트 참여 흐름 · HOME 진입 구획 · 미니맵 · 공통 확인 창 배치 수정
This commit is contained in:
@@ -93,10 +93,17 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
for (final doc in docs) {
|
||||
final quest = doc.data();
|
||||
if (!_open(quest, now, uid)) continue;
|
||||
final progress = await db
|
||||
.doc('users/$uid/questProgress/${doc.id}')
|
||||
.get()
|
||||
.then((s) => s.data() ?? const <String, dynamic>{});
|
||||
// 카드 한 장의 부가 조회가 실패해도 목록 전체를 깨지 않는다 — 그 카드만 시작 전으로 둔다.
|
||||
// UGC 퀘스트 하나의 제출 건 조회가 거부되어 **모든 사용자의** 목록이 무한 로딩이었다(2026-10-04).
|
||||
Map<String, dynamic> progress;
|
||||
try {
|
||||
progress = await db
|
||||
.doc('users/$uid/questProgress/${doc.id}')
|
||||
.get()
|
||||
.then((s) => s.data() ?? const <String, dynamic>{});
|
||||
} on Exception {
|
||||
progress = const {};
|
||||
}
|
||||
cards.add(_card(doc.id, quest, progress, uid, policy, await _submission(doc.id, quest)));
|
||||
}
|
||||
return cards;
|
||||
@@ -145,17 +152,9 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
final type = QuestType.parse(quest.strOr('type'));
|
||||
final authorUid = quest.strOr('authorUid');
|
||||
final dailyLimit = quest.intOr('dailyLimit', 1);
|
||||
// 타임어택은 1인 1회라 날짜로 끊지 않는다 — 창이 자정을 넘어도 「보상 완료」로 남는다.
|
||||
final sameDay = type == QuestType.timeAttack ||
|
||||
progress.strOr('dayKey') == _dayKey(DateTime.now());
|
||||
final count = sameDay ? progress.intOr('count') : 0;
|
||||
|
||||
// 걸음은 **구간표**가 목표다 — 정책 원장 **B-26**(1 천·5 천·1 만·1.5 만보). 오늘
|
||||
// 받은 구간 수가 `count` 이므로 다음 문턱이 지금 목표다. 나머지 유형은 일일 한도가
|
||||
// 곧 목표 횟수다 — 설계서 §2.8 이 `quests` 에 목표 수치 필드를 두지 않았다.
|
||||
final (:count, :done) = questToday(type, quest, progress, policy);
|
||||
final tiers = _stepTiers(policy);
|
||||
final steps = type == QuestType.steps;
|
||||
final done = steps ? count >= tiers.length : count >= dailyLimit;
|
||||
final goal = steps
|
||||
? tiers[count.clamp(0, tiers.length - 1)]
|
||||
: dailyLimit;
|
||||
@@ -191,23 +190,6 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
);
|
||||
}
|
||||
|
||||
/// 걸음 구간 문턱들 — `config/policy.stepTiers`(운영값).
|
||||
///
|
||||
/// **화면 표시용이다. 판정은 서버가 다시 한다.** 문서가 아직 없을 때만 예전 단일
|
||||
/// 목표(`stepsGoal`)로 떨어뜨린다 — 앱에 구간값을 박으면 운영값이 두 벌이 된다.
|
||||
List<int> _stepTiers(Map<String, dynamic> policy) {
|
||||
final raw = policy['stepTiers'];
|
||||
if (raw is List && raw.isNotEmpty) {
|
||||
final steps = <int>[];
|
||||
for (final row in raw) {
|
||||
if (row is Map && row['steps'] is num) {
|
||||
steps.add((row['steps'] as num).round());
|
||||
}
|
||||
}
|
||||
if (steps.isNotEmpty) return steps..sort();
|
||||
}
|
||||
return [policy.intOr('stepsGoal', 10000)];
|
||||
}
|
||||
|
||||
/// 다국어 제목 — 설계서 §2.8 은 `title`(다국어)이라고만 적었다.
|
||||
/// 문자열과 지도(map) 둘 다 들어올 수 있어 둘 다 받는다.
|
||||
@@ -282,9 +264,13 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
Future<QuestSubmission?> _submission(String questId, Map<String, dynamic> quest) async {
|
||||
if (quest.strOr('authorUid').isEmpty) return null;
|
||||
final id = '${questId}_${_backend.requireUid}';
|
||||
final snap = await _backend.db.doc('questSubmissions/$id').get();
|
||||
final data = snap.data();
|
||||
return data == null ? null : _submissionOf(id, data);
|
||||
try {
|
||||
final data = (await _backend.db.doc('questSubmissions/$id').get()).data();
|
||||
return data == null ? null : _submissionOf(id, data);
|
||||
} on Exception {
|
||||
// 규칙 배포 전(없는 문서 get 이 permission-denied) 등 — 제출 없음으로 본다.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 생성자의 검토 대기열. 규칙이 생성자 조건을 증명하도록 `questAuthorUid` 를 함께 건다.
|
||||
@@ -441,6 +427,12 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
return result.strOr('questId');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> popBalance() async {
|
||||
final wallet = await _backend.db.doc('users/${_backend.requireUid}/wallet/main').get();
|
||||
return wallet.data()?.intOr('activityBalance');
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<int> progressStream(QuestCard card) {
|
||||
if (card.type != QuestType.steps) return const Stream<int>.empty();
|
||||
@@ -562,3 +554,47 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
/// 단말 시간대로 끊으면 해외 사용자의 화면과 서버 판정이 하루 어긋난다.
|
||||
String _dayKey(DateTime at) =>
|
||||
at.toUtc().add(const Duration(hours: 9)).toIso8601String().substring(0, 10);
|
||||
|
||||
/// 걸음 구간 문턱들 — `config/policy.stepTiers`(운영값).
|
||||
///
|
||||
/// **화면 표시용이다. 판정은 서버가 다시 한다.** 문서가 아직 없을 때만 예전 단일
|
||||
/// 목표(`stepsGoal`)로 떨어뜨린다 — 앱에 구간값을 박으면 운영값이 두 벌이 된다.
|
||||
List<int> _stepTiers(Map<String, dynamic> policy) {
|
||||
final raw = policy['stepTiers'];
|
||||
if (raw is List && raw.isNotEmpty) {
|
||||
final steps = <int>[];
|
||||
for (final row in raw) {
|
||||
if (row is Map && row['steps'] is num) {
|
||||
steps.add((row['steps'] as num).round());
|
||||
}
|
||||
}
|
||||
if (steps.isNotEmpty) return steps..sort();
|
||||
}
|
||||
return [policy.intOr('stepsGoal', 10000)];
|
||||
}
|
||||
|
||||
/// 오늘(KST) 수행 횟수와 완료 여부 — **HOME 과 퀘스트 목록 · 상세가 같이 쓰는 단일 판정.**
|
||||
///
|
||||
/// `users/{uid}/questProgress/{questId}.count` 는 `dayKey` 가 오늘일 때만 오늘 값이다. 날짜를
|
||||
/// 보지 않으면 9/21 출석의 `count: 1` 이 매일 「보상 완료」로 남는다(2026-10-04 실기기 4-1).
|
||||
/// 타임어택은 1인 1회라 날짜로 끊지 않는다 — 창이 자정을 넘어도 「보상 완료」로 남는다.
|
||||
///
|
||||
/// 걸음은 **구간표**가 목표다 — 정책 원장 **B-26**(1 천·5 천·1 만·1.5 만보). 오늘 받은 구간
|
||||
/// 수가 `count` 라 구간을 다 받아야 완료다. 나머지 유형은 일일 한도가 곧 목표 횟수다 —
|
||||
/// 설계서 §2.8 이 `quests` 에 목표 수치 필드를 두지 않았다.
|
||||
({int count, bool done}) questToday(
|
||||
QuestType type,
|
||||
Map<String, dynamic> quest,
|
||||
Map<String, dynamic>? progress,
|
||||
Map<String, dynamic> policy, [
|
||||
DateTime? now,
|
||||
]) {
|
||||
final p = progress ?? const <String, dynamic>{};
|
||||
final sameDay = type == QuestType.timeAttack ||
|
||||
p.strOr('dayKey') == _dayKey(now ?? DateTime.now());
|
||||
final count = sameDay ? p.intOr('count') : 0;
|
||||
final done = type == QuestType.steps
|
||||
? count >= _stepTiers(policy).length
|
||||
: count >= quest.intOr('dailyLimit', 1);
|
||||
return (count: count, done: done);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user