FPI 랭킹 내 행성·내 국가 판정 · 콜드 스타트 약관 깜빡임 · 로그아웃 상한
This commit is contained in:
@@ -243,7 +243,9 @@ class AuthRepository extends ChangeNotifier {
|
||||
|
||||
try {
|
||||
await Future.wait([
|
||||
if (provider != null) provider.signOut(),
|
||||
// SDK 로그아웃(Google · Kakao 등)이 응답 없이 멈춰도 로컬 로그아웃은 한다.
|
||||
if (provider != null)
|
||||
provider.signOut().timeout(const Duration(seconds: 3)),
|
||||
if (_session != null) _session.signOut(),
|
||||
], eagerError: false);
|
||||
} finally {
|
||||
|
||||
@@ -216,22 +216,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
MyLinkRow(
|
||||
label: l10n.settingsLogout,
|
||||
color: MyColors.danger,
|
||||
onTap: () => _confirm(
|
||||
title: l10n.settingsLogoutConfirm,
|
||||
action: l10n.settingsLogout,
|
||||
// 푸시 정리를 **먼저** 한다. 보안 규칙이 `users/{uid}` 쓰기를
|
||||
// `isSelf(uid)` 로 막아, 로그아웃 뒤에는 이 기기 토큰을 지울 수 없다.
|
||||
// 정리가 실패해도(토픽 해제 · 토큰 쓰기 네트워크 오류) 로그아웃은 한다 — 예외가
|
||||
// 그대로 올라가면 signOut 이 돌지 않아 첫 탭이 무시된 것처럼 보인다(10/6 실기기 1회).
|
||||
onConfirm: () async {
|
||||
try {
|
||||
await widget.push?.clearOnSignOut();
|
||||
} on Object catch (error) {
|
||||
debugPrint('[Settings] push cleanup failed: ${error.runtimeType}');
|
||||
}
|
||||
await widget.auth?.signOut();
|
||||
},
|
||||
),
|
||||
onTap: _logout,
|
||||
),
|
||||
const MyDivider(),
|
||||
MyLinkRow(
|
||||
@@ -384,6 +369,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Future<void> _notice(String message) => showAlertDialog(context, message);
|
||||
|
||||
/// 확인 팝업(취소 · 실행) — 시안 `554:24669` · `374:14364` · `554:23257`.
|
||||
/// 푸시 정리를 **먼저** 한다. 보안 규칙이 `users/{uid}` 쓰기를 `isSelf(uid)` 로 막아,
|
||||
/// 로그아웃 뒤에는 이 기기 토큰을 지울 수 없다. 정리는 확인 버튼이 진행 중으로 바뀐
|
||||
/// 채 돌고, 실패하든 [_cleanupTimeout] 안에 끝나지 않든 로그아웃은 한다 — 토픽 해제 ·
|
||||
/// 토큰 쓰기가 응답 없이 멈추면 signOut 에 닿지 못해 첫 확인이 무시된 것처럼
|
||||
/// 보였다(10/6 실기기 2회).
|
||||
Future<void> _logout() async {
|
||||
final l10n = context.l10n;
|
||||
final ok = await showConfirmDialog(
|
||||
context,
|
||||
l10n.settingsLogoutConfirm,
|
||||
confirmLabel: l10n.settingsLogout,
|
||||
onConfirm: () async {
|
||||
try {
|
||||
await widget.push?.clearOnSignOut().timeout(_cleanupTimeout);
|
||||
} on Object catch (error) {
|
||||
debugPrint('[Settings] push cleanup skipped: ${error.runtimeType}');
|
||||
}
|
||||
},
|
||||
);
|
||||
if (ok) await widget.auth?.signOut();
|
||||
}
|
||||
|
||||
/// 로그아웃 전 정리의 상한. 넘기면 정리를 두고 로그아웃한다.
|
||||
static const _cleanupTimeout = Duration(seconds: 3);
|
||||
|
||||
Future<bool> _confirm({
|
||||
required String title,
|
||||
String? body,
|
||||
|
||||
@@ -287,30 +287,52 @@ class FirestorePlanetRepository implements PlanetRepository {
|
||||
// 서버 값이 없을 때의 메움도 1·1·3 공동 순위다(C-19). 실값(반올림 전)으로 비교하고
|
||||
// 동점 내부는 fandomId 오름차순 — Firestore 의 내림차순 정렬은 동점을 ID
|
||||
// 내림차순으로 세우므로 여기서 다시 세운다.
|
||||
final myId = await _myPlanetId();
|
||||
final rows = <FpiRankRow>[
|
||||
for (final (doc, rank) in rankWithTies(
|
||||
snap.docs,
|
||||
(d) => _raw(d.data(), axis),
|
||||
(d) => d.id,
|
||||
))
|
||||
_planetRow(doc.data(), id: doc.id, axis: axis, fallbackRank: rank),
|
||||
_planetRow(
|
||||
doc.data(),
|
||||
id: doc.id,
|
||||
axis: axis,
|
||||
fallbackRank: rank,
|
||||
myId: myId,
|
||||
),
|
||||
];
|
||||
|
||||
final mine = rows.where((r) => r.isMine).firstOrNull;
|
||||
if (mine != null) return FpiRanking(rows: rows, mine: mine);
|
||||
if (mine != null || myId == null) return FpiRanking(rows: rows, mine: mine);
|
||||
|
||||
// 상위 [limit] 밖이면 내 행성 문서를 한 번 더 읽어 「소속 행성 지표 랭킹
|
||||
// 영역」(Figma `546:14628`)을 채운다. 사용자 랭킹의 count() 폴백과 같은
|
||||
// 취지다 — 내 행성이 몇 등이든 그 영역은 떠야 한다.
|
||||
final me = (await _backend.db.doc('fandoms/$fandomId').get()).data();
|
||||
final me = (await _backend.db.doc('fandoms/$myId').get()).data();
|
||||
if (me == null) return FpiRanking(rows: rows);
|
||||
return FpiRanking(
|
||||
rows: rows,
|
||||
// 서버 순위가 없으면 목록 밖이라는 것만 아는 셈이라 0 으로 둔다.
|
||||
mine: _planetRow(me, id: fandomId, axis: axis, fallbackRank: 0),
|
||||
mine: _planetRow(me, id: myId, axis: axis, fallbackRank: 0, myId: myId),
|
||||
);
|
||||
}
|
||||
|
||||
/// 「내 행성」(소속 행성) — **실제 가입 여부로** 정한다. 보고 있는 행성에
|
||||
/// 가입했으면 그 행성, 아니면 대표 행성(`users.fandomId`), 둘 다 없으면 null.
|
||||
/// 보고 있는 행성을 그대로 내 행성으로 두면 미가입 행성 랭킹이 그 행성을
|
||||
/// 「(나)」로 표시한다(10/6 실기기 C-7).
|
||||
Future<String?> _myPlanetId() async {
|
||||
final uid = _backend.requireUid;
|
||||
final joined =
|
||||
(await _backend.db.doc('users/$uid/memberships/$fandomId').get())
|
||||
.exists;
|
||||
if (joined) return fandomId;
|
||||
final primary = (await _backend.db.doc('users/$uid').get()).data()?[
|
||||
'fandomId'];
|
||||
return primary is String && primary.isNotEmpty ? primary : null;
|
||||
}
|
||||
|
||||
/// 축의 실제 산정값(반올림 전). 순위 비교에만 쓴다 — 표시는 [SnapshotReader.intOr].
|
||||
static num _raw(Map<String, dynamic> data, String axis) {
|
||||
final v = _map(data, 'fpiAxes')[axis];
|
||||
@@ -323,6 +345,7 @@ class FirestorePlanetRepository implements PlanetRepository {
|
||||
required String id,
|
||||
required String axis,
|
||||
required int fallbackRank,
|
||||
required String? myId,
|
||||
}) {
|
||||
final rank = _map(data, 'fpiRanks').intOr(axis, fallbackRank);
|
||||
return FpiRankRow(
|
||||
@@ -332,7 +355,7 @@ class FirestorePlanetRepository implements PlanetRepository {
|
||||
value: _map(data, 'fpiAxes').intOr(axis),
|
||||
// 순위 동향 = 직전 순위 − 현재 순위. 둘 다 서버가 적은 값이다.
|
||||
change: _map(data, 'fpiRanksPrev').intOr(axis, rank) - rank,
|
||||
isMine: id == fandomId,
|
||||
isMine: id == myId,
|
||||
planetType: data['planetType'] is String ? data['planetType'] as String : null,
|
||||
colorKey: data['colorKey'] is String ? data['colorKey'] as String : null,
|
||||
);
|
||||
@@ -485,14 +508,12 @@ class FirestorePlanetRepository implements PlanetRepository {
|
||||
if (rows.isEmpty) {
|
||||
return const FpiRanking(rows: [], mockNote: '국가별 집계가 아직 없습니다');
|
||||
}
|
||||
// 내 국가는 멤버 문서에 비정규화된 `countryCode` 다(집계가 세는 필드와 같다).
|
||||
// 내 국가는 사용자 프로필 `users.countryCode` 다. 보고 있는 행성의 멤버
|
||||
// 문서에서 읽으면 미가입 행성에서는 늘 「미지정」이 된다(10/6 실기기 C-7).
|
||||
// 「나의 랭킹 영역」(546:14713)은 국가별 필터에서도 떠야 하므로 내 국가 행을
|
||||
// 찾아 올린다.
|
||||
final me =
|
||||
(await _backend.db
|
||||
.doc('fandoms/$fandomId/members/${_backend.requireUid}')
|
||||
.get())
|
||||
.data();
|
||||
(await _backend.db.doc('users/${_backend.requireUid}').get()).data();
|
||||
final myCode = (me ?? const <String, dynamic>{}).strOr('countryCode', 'ZZ');
|
||||
|
||||
final list = <FpiRankRow>[];
|
||||
|
||||
Reference in New Issue
Block a user