캠페인 실시간 랭킹 후원자 이름 · 위치 핀 도로명 주소

This commit is contained in:
Sn-Kinos
2026-10-06 01:24:53 +09:00
parent 83d9c1e8d4
commit d441170dd7
6 changed files with 90 additions and 9 deletions
@@ -1001,6 +1001,18 @@ class _CampaignDetailScreenState extends State<CampaignDetailScreen> {
_spent += applied;
_raised += applied;
});
// 실시간 랭킹(상위 3) · 모금액을 서버 값으로 다시 읽는다. 잔액 계산(_balance · _spent)은 그대로 둔다.
try {
final fresh = await widget.repository.detail(widget.campaignId);
if (mounted) {
setState(() {
_detail = fresh;
_raised = fresh.card.raisedPop;
});
}
} on Object catch (_) {
// 다시 읽기 실패는 후원 결과와 무관하다 — 앞 화면을 그대로 둔다.
}
}
Future<void> _openRanking(CampaignDetail detail) =>
@@ -177,12 +177,15 @@ class FirestoreCampaignRepository implements CampaignRepository {
RankWindow.weekly => DateTime.now().subtract(const Duration(days: 7)),
};
final totals = <String, int>{};
final names = <String, String>{};
for (final d in snap.docs) {
final m = d.data();
if (since != null && m.dateOr('createdAt', since).isBefore(since))
continue;
final uid = m.strOr('uid');
totals[uid] = (totals[uid] ?? 0) + m.intOr('amount');
// 서버가 후원 때 실은 공개 닉네임(`donorName`, M-7). 예전 후원에는 없어 「팬」이다.
if (m.strOr('donorName').isNotEmpty) names[uid] = m.strOr('donorName');
}
final me = _backend.uid;
final rows = totals.entries.toList()
@@ -191,8 +194,10 @@ class FirestoreCampaignRepository implements CampaignRepository {
for (var i = 0; i < rows.length; i++)
ContributorRank(
rank: i + 1,
// 남의 이름은 서버에 읽을 자리가 없다. UID 는 화면에 내보내지 않는다.
name: rows[i].key == me ? appL10n.commonMe : appL10n.commonFan,
// UID · 이메일은 화면에 내보내지 않는다(G-07) — 공개 닉네임만.
name: rows[i].key == me
? appL10n.commonMe
: publicName(names[rows[i].key]),
pop: rows[i].value,
isMe: rows[i].key == me,
),
@@ -68,12 +68,8 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
try {
final marks = await Geocoding().placemarkFromCoordinates(point.latitude, point.longitude);
if (marks.isNotEmpty) {
final m = marks.first;
final parts = [m.administrativeArea, m.locality, m.subLocality, m.thoroughfare, m.subThoroughfare]
.whereType<String>()
.where((s) => s.isNotEmpty)
.toList();
if (parts.isNotEmpty) address = parts.join(' ');
final line = placeAddress(marks.first);
if (line.isNotEmpty) address = line;
}
} catch (_) {
// 지오코더가 없거나(에뮬레이터 · 테스트 — 플랫폼 구현 없음) 실패 — 좌표 표시로 둔다.
@@ -230,3 +226,26 @@ class _PlacePickerScreenState extends State<PlacePickerScreen> {
);
}
}
/// 지오코더 결과 → 한 줄 주소. 한국 주소는 도로명이 `thoroughfare` 에 오지 않아(번지만 `subThoroughfare`)
/// 칸을 이어 붙이면 「서울특별시 마포구 34」가 된다(10/6 실기기). 도로명이 든 `street` 를 쓴다 —
/// Android 는 주소 전체 줄(나라 이름 포함), iOS 는 「도로명 번지」만 준다.
String placeAddress(Placemark m) {
String t(String? v) => v?.trim() ?? '';
final street = t(m.street);
final area = t(m.administrativeArea);
if (street.isNotEmpty && area.isNotEmpty && street.contains(area)) {
// 전체 줄 — 앞의 나라 이름만 뗀다.
final country = t(m.country);
return country.isNotEmpty && street.startsWith(country)
? street.substring(country.length).trim()
: street;
}
final road = street.isNotEmpty
? street
: [t(m.thoroughfare), t(m.subThoroughfare)].where((s) => s.isNotEmpty).join(' ');
return [area, t(m.locality), t(m.subLocality), road]
.where((s) => s.isNotEmpty)
.toSet()
.join(' ');
}