FPI 사용자·국가 랭킹 서버 집계 · YouTube 스밍 · Music Shake 참여

This commit is contained in:
Sn-Kinos
2026-10-05 17:49:47 +09:00
parent 5238fc4ef3
commit 96a95a12d1
31 changed files with 738 additions and 116 deletions
@@ -134,9 +134,9 @@ class FirestorePlanetRepository implements PlanetRepository {
final axes = _map(fandom, 'fpiAxes');
final ranks = _map(fandom, 'fpiRanks');
final myTop = _map(member, 'topPercentByAxis');
// MOCK: `topPercentByAxis` 는 서버 산출값인데 이를 쓰는 집계 함수가 아직
// 없습니다. 필드는 실연동으로 읽고, **값이 없으면 null** 로 둡니다 —
// 100%(최하위)로 채우면 없는 값이 사실처럼 보입니다. 대조표 3-5 참조.
// `topPercentByAxis` 는 서버 5 분 집계(`fnRankingCountryAggregate`)가 적는다.
// **값이 없으면 null** — 그 축 기여가 0 이거나 모집단(B-17) 밖이다. 100%(최하위)로
// 채우면 없는 값이 사실처럼 보인다.
int? myTopOf(FpiMetric metric) => myTop[_axisKey(metric)] is num
? myTop.intOr(_axisKey(metric))
: null;
@@ -189,9 +189,7 @@ class FirestorePlanetRepository implements PlanetRepository {
posts: posts,
// 한 축이라도 서버 값이 없으면 배지로 알린다. 화면이 이 문구를 그대로
// 띄운다 — 「집계 준비 중」 카드만으로는 이유가 보이지 않는다(대조표 3-5).
mockNote: FpiMetric.values.any((m) => myTopOf(m) == null)
? '「내 기여 상위 %」 집계 함수가 아직 없습니다'
: null,
mockNote: null,
);
}
@@ -288,18 +286,7 @@ class FirestorePlanetRepository implements PlanetRepository {
(d) => _raw(d.data(), axis),
(d) => d.id,
))
FpiRankRow(
id: doc.id,
rank: rank,
// UID·이메일을 이름 자리에 내보내지 않는다(G-07).
name: publicName(doc.data()['nickname']),
value: _map(doc.data(), 'fpiAxes').intOr(axis),
// MOCK: 멤버의 직전 축별 순위(`rankPrevByAxis`)를 쓰는 서버 집계가
// 아직 없습니다. 필드가 없으면 현재 순위를 그대로 넣어 동향이 0 이 되고,
// 화면은 ▲▼ 를 그리지 않습니다. 대조표 3-5 참조.
change: _map(doc.data(), 'rankPrevByAxis').intOr(axis, rank) - rank,
isMine: doc.id == uid,
),
_userRow(doc.data(), id: doc.id, axis: axis, fallbackRank: rank, uid: uid),
];
// 한 줄도 직전 순위를 갖고 있지 않으면 동향은 전부 0 이다. 그 사실을 배지로
@@ -317,22 +304,45 @@ class FirestorePlanetRepository implements PlanetRepository {
// Figma `546:14668` 「나의 랭킹 영역」은 내가 몇 등이든 떠야 한다.
final me = (await members.doc(uid).get()).data();
if (me == null) return FpiRanking(rows: rows, mockNote: note);
final myValue = _map(me, 'fpiAxes').intOr(axis);
// 실값보다 큰 멤버 수 + 1 = 공동 순위(C-19). 반올림한 값으로 세면 동률이 생긴다.
final above = await members
.where('fpiAxes.$axis', isGreaterThan: _raw(me, axis))
.count()
.get();
// 서버 순위가 없을 때만 실값보다 큰 멤버 수 + 1 = 공동 순위(C-19)로 센다.
final serverRank = _map(me, 'rankByAxis')[axis];
final fallback = serverRank is num
? 0
: ((await members
.where('fpiAxes.$axis', isGreaterThan: _raw(me, axis))
.count()
.get())
.count ??
rows.length) +
1;
return FpiRanking(
rows: rows,
mockNote: note,
mine: FpiRankRow(
id: uid,
rank: (above.count ?? rows.length) + 1,
name: publicName(me['nickname'], appL10n.commonMe),
value: myValue,
isMine: true,
),
mine: _userRow(me, id: uid, axis: axis, fallbackRank: fallback, uid: uid,
name: appL10n.commonMe),
);
}
/// 사용자 랭킹 한 줄. 순위 · 직전 순위는 서버 5 분 집계(`rankByAxis` · `rankPrevByAxis`)를
/// 읽는다 — 상위 % 와 같은 원천이라 두 값이 갈리지 않는다. 서버 값이 없을 때만 [fallbackRank].
static FpiRankRow _userRow(
Map<String, dynamic> data, {
required String id,
required String axis,
required int fallbackRank,
required String uid,
String? name,
}) {
final rank = _map(data, 'rankByAxis').intOr(axis, fallbackRank);
return FpiRankRow(
id: id,
rank: rank,
// UID·이메일을 이름 자리에 내보내지 않는다(G-07).
name: publicName(data['nickname'], name),
value: _map(data, 'fpiAxes').intOr(axis),
// 순위 동향 = 직전 순위 − 현재 순위. 직전 값이 없으면 0(▲▼ 없음).
change: _map(data, 'rankPrevByAxis').intOr(axis, rank) - rank,
isMine: id == uid,
);
}