번역 평가 버튼 · 내 퀘스트 검토 버튼 · MY 질문 상세 · MY 완료 퀘스트 수 · 행성/성운 칸 · 라운지 빈 상태 수정

This commit is contained in:
Sn-Kinos
2026-10-05 15:12:33 +09:00
parent 5ad229b971
commit f84d696c21
18 changed files with 666 additions and 365 deletions
@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
@@ -14,7 +16,8 @@ import '../../core/i18n/app_locale.dart';
/// 쓰면 어느 것이 진짜인지 화면에서 알 수 없다.
///
/// 읽는 곳 — **서버가 실제로 쓰는 필드**를 읽는다.
/// - 프로필 `users/{uid}` (nickname · photoUrl · originNo · steps · completedQuests)
/// - 프로필 `users/{uid}` (nickname · photoUrl · originNo · steps)
/// - 완료 퀘스트 `users/{uid}/verifiedCompletions` 표식 수(무효 제외)
/// - 누적 기여 POP `users/{uid}.totalContributedPop`(`fn-pop-spend` 가 갱신, 설계서 §3.6)
/// - 레벨 `users/{uid}.{userExp, userLevel}` · Passport `users/{uid}.passportTier`
/// (C-18 · B-65). 필드가 없는 기존 사용자는 Lv.1 · SILVER 로 둔다.
@@ -46,6 +49,18 @@ class FirestoreMyRepository extends SampleMyRepository {
final userF = db.doc('users/$uid').get();
final walletF = db.doc('users/$uid/wallet/main').get();
// 완료 퀘스트 = 검증 활동 표식 수 − 무효 표식 수. 서버 `verifiedQuestCountOf`(fpi.ts)와 같은 식.
// `users.completedQuests` 는 쓰는 서버 코드가 없어 늘 0 이었다.
final done = db.collection('users/$uid/verifiedCompletions');
// 규칙 배포 전 서버에서는 이 읽기가 거부된다 — MY 전체를 떨어뜨리지 않고 0 으로 둔다.
final completedF =
Future.wait([
done.count().get(),
done.where('voided', isEqualTo: true).count().get(),
]).then(
(c) => math.max(0, (c[0].count ?? 0) - (c[1].count ?? 0)),
onError: (Object _) => 0,
);
final user = (await userF).data() ?? const <String, dynamic>{};
final wallet = (await walletF).data() ?? const <String, dynamic>{};
@@ -83,7 +98,7 @@ class FirestoreMyRepository extends SampleMyRepository {
passportTier: PassportTier.parse(user.strOr('passportTier')),
balancePop: wallet.intOr('activityBalance'),
steps: user.intOr('steps'),
completedQuests: user.intOr('completedQuests'),
completedQuests: await completedF,
planetTiers: tiers,
photoUrl: photoUrl.isEmpty ? null : photoUrl,
originNo: user.containsKey('originNo') ? user.intOr('originNo') : null,
@@ -127,10 +142,9 @@ class FirestoreMyRepository extends SampleMyRepository {
@override
Future<void> updateNickname(String nickname) async {
final uid = _backend.requireUid;
await _backend.db.doc('users/$uid').set(
{'nickname': nickname},
SetOptions(merge: true),
);
await _backend.db.doc('users/$uid').set({
'nickname': nickname,
}, SetOptions(merge: true));
}
/// POP 원장 — S-M-WALLET · 명세 `554:23732`.
@@ -174,22 +188,24 @@ class FirestoreMyRepository extends SampleMyRepository {
final reasonCode = data.strOr('reasonCode');
final label = popReasonLabel(reasonCode);
final at = data.dateOr('createdAt', _epoch);
rows.add(_LedgerRow(
reasonCode: reasonCode,
// Zap 한 행이 몇 번분인가. `fn-eclipse-defend` 가 `count` 를 묶어 받으므로
// 행 하나가 여러 번일 수 있다. 없는 문서는 한 번으로 본다.
count: data.intOr('count', 1).clamp(1, 1 << 30),
entry: PopLedgerEntry(
at: at,
title: label.title,
type: label.type,
kind: PopKind.parse(data.strOr('popType')),
txType: PopTxType.parse(data.strOr('type', 'grant')),
delta: data.intOr('amount'),
balanceAfter: data.intOr('balanceAfter'),
cursor: PopCursor(at: at, id: doc.id),
rows.add(
_LedgerRow(
reasonCode: reasonCode,
// Zap 한 행이 몇 번분인가. `fn-eclipse-defend` 가 `count` 를 묶어 받으므로
// 행 하나가 여러 번일 수 있다. 없는 문서는 한 번으로 본다.
count: data.intOr('count', 1).clamp(1, 1 << 30),
entry: PopLedgerEntry(
at: at,
title: label.title,
type: label.type,
kind: PopKind.parse(data.strOr('popType')),
txType: PopTxType.parse(data.strOr('type', 'grant')),
delta: data.intOr('amount'),
balanceAfter: data.intOr('balanceAfter'),
cursor: PopCursor(at: at, id: doc.id),
),
),
));
);
}
return _foldZap(rows);
}
@@ -262,7 +278,10 @@ class FirestoreMyRepository extends SampleMyRepository {
PaymentEntry _payment(String id, Map<String, dynamic> data) => PaymentEntry(
id: id,
at: data.dateOr('purchasedAt', _epoch),
itemName: data.strOr('itemName', data.strOr('productId', appL10n.payItemFallback)),
itemName: data.strOr(
'itemName',
data.strOr('productId', appL10n.payItemFallback),
),
price: data.intOr('priceAmount'),
method: data.strOr('method', appL10n.payMethodInApp),
state: paymentStateOf(data.strOr('state')),
@@ -416,10 +435,9 @@ class FirestoreMyRepository extends SampleMyRepository {
);
}
final url = await ref.getDownloadURL();
await _backend.db.doc('users/$uid').set(
{'photoUrl': url},
SetOptions(merge: true),
);
await _backend.db.doc('users/$uid').set({
'photoUrl': url,
}, SetOptions(merge: true));
return url;
}
}