HOT ARCHIVE 정렬 기준
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart' show Timestamp;
|
||||
import 'package:cloud_firestore/cloud_firestore.dart'
|
||||
show QueryDocumentSnapshot, Timestamp;
|
||||
|
||||
import '../../core/data/backend.dart';
|
||||
import '../../core/data/invite_code.dart';
|
||||
@@ -152,14 +153,13 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
.orderBy('rank')
|
||||
.limit(6)
|
||||
.get();
|
||||
// HOT ARCHIVE 캐러셀 — 명세 §2.114-6 「최대 5개」. 차단한 개설자의 캠페인은 건너뛰므로
|
||||
// 열 건을 읽어 다섯을 채운다. ponytail: 정렬은 모금액 순 하나(명세의 「달성 임박 > 기여 급증 >
|
||||
// 신규」 3단 정렬은 기여 급증 집계가 없어 아직 못 한다).
|
||||
// HOT ARCHIVE 캐러셀 — Figma 홈 주석 `2097:16415` 「최대 5개 소팅(목표 달성 임박 건 > 실시간
|
||||
// 기여 급증 건 > 신규 등록 건 순)」 · 별첨 B 「HOT ARCHIVE」. 정렬은 아래 [hotArchiveOrder].
|
||||
// ponytail: 모금 중 30건 안에서 고른다. 모금 중 캠페인이 이보다 많아지면 서버 집계 필드로 옮긴다.
|
||||
final campaignF = db
|
||||
.collection('campaigns')
|
||||
.where('status', isEqualTo: 'funding')
|
||||
.orderBy('raisedPop', descending: true)
|
||||
.limit(10)
|
||||
.limit(30)
|
||||
.get();
|
||||
// 팬덤행성 인큐베이터 — 명세 §2.114-4 「기여율 높은 순 최대 6개」. 성운(Stage0)만.
|
||||
// ponytail: 성운 30건 안에서 고른다. (stage, growthPop) 색인을 배포하면 질의로 옮긴다.
|
||||
@@ -209,9 +209,13 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
final progress = (await progressF).docs;
|
||||
final briefings = (await briefingsF).docs;
|
||||
final blocked = {for (final d in (await blocksF).docs) d.id};
|
||||
final campaigns = (await campaignF).docs
|
||||
.where((d) => !blocked.contains(d.data()['creatorUid']))
|
||||
.toList();
|
||||
final policy = (await policyF).data() ?? const <String, dynamic>{};
|
||||
final campaigns = await _hotArchive(
|
||||
(await campaignF).docs
|
||||
.where((d) => !blocked.contains(d.data()['creatorUid']))
|
||||
.toList(),
|
||||
policy,
|
||||
);
|
||||
final insideDocs = {
|
||||
for (final doc in (await insidesF).docs) doc.data(): doc.id,
|
||||
};
|
||||
@@ -221,7 +225,6 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
];
|
||||
|
||||
final progressById = {for (final doc in progress) doc.id: doc.data()};
|
||||
final policy = (await policyF).data() ?? const <String, dynamic>{};
|
||||
|
||||
return HomeSnapshot(
|
||||
popBalance: wallet.intOr('activityBalance'),
|
||||
@@ -235,7 +238,7 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
_quest(doc.id, doc.data(), progressById[doc.id], policy),
|
||||
],
|
||||
briefings: [for (final doc in briefings) ?_briefing(doc.data())],
|
||||
campaigns: [for (final d in campaigns.take(5)) _campaign(d.id, d.data())],
|
||||
campaigns: [for (final d in campaigns) _campaign(d.id, d.data())],
|
||||
insides: [
|
||||
for (final (id, data) in insides) insideQuestionFrom(id, data, uid),
|
||||
],
|
||||
@@ -393,6 +396,47 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
);
|
||||
}
|
||||
|
||||
/// HOT ARCHIVE 5건 — 「목표 달성 임박 > 실시간 기여 급증 > 신규 등록」.
|
||||
///
|
||||
/// 급증은 최근 [HotArchiveRule.surgeWindow] 안의 후원 합(`campaigns/{id}/donations`)으로 잰다.
|
||||
/// 임박 묶음에 든 캠페인은 급증을 셀 필요가 없어 읽지 않는다.
|
||||
Future<List<QueryDocumentSnapshot<Map<String, dynamic>>>> _hotArchive(
|
||||
List<QueryDocumentSnapshot<Map<String, dynamic>>> docs,
|
||||
Map<String, dynamic> policy,
|
||||
) async {
|
||||
final rule = HotArchiveRule.from(policy);
|
||||
final since = Timestamp.fromDate(DateTime.now().subtract(rule.surgeWindow));
|
||||
final candidates = await Future.wait([
|
||||
for (final d in docs)
|
||||
() async {
|
||||
final goal = d.data().intOr('targetPop', d.data().intOr('goalPop'));
|
||||
final raised = d.data().intOr('raisedPop');
|
||||
final percent = goal == 0 ? 0.0 : raised * 100 / goal;
|
||||
var recent = 0;
|
||||
if (percent < rule.imminentPercent) {
|
||||
final snap = await d.reference
|
||||
.collection('donations')
|
||||
.where('createdAt', isGreaterThanOrEqualTo: since)
|
||||
.get();
|
||||
for (final x in snap.docs) {
|
||||
recent += x.data().intOr('amount');
|
||||
}
|
||||
}
|
||||
return (
|
||||
id: d.id,
|
||||
percent: percent,
|
||||
recentPop: recent,
|
||||
createdAt: d.data().dateOr(
|
||||
'createdAt',
|
||||
DateTime.fromMillisecondsSinceEpoch(0),
|
||||
),
|
||||
);
|
||||
}(),
|
||||
]);
|
||||
final byId = {for (final d in docs) d.id: d};
|
||||
return [for (final id in hotArchiveOrder(candidates, rule)) byId[id]!];
|
||||
}
|
||||
|
||||
CampaignHighlight _campaign(String id, Map<String, dynamic> data) {
|
||||
// 규칙의 스키마 이름(`targetPop` · `creatorName` · `summary`)을 먼저 읽는다.
|
||||
final goal = data.intOr('targetPop', data.intOr('goalPop'));
|
||||
@@ -410,6 +454,62 @@ class FirestoreHomeRepository implements HomeRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// HOT ARCHIVE 정렬의 기준값 — `config/policy.hotArchive`.
|
||||
///
|
||||
/// **임시값 — 회신 대기.** 「목표 달성 임박」의 달성률 기준과 「실시간 기여 급증」의 측정 구간은
|
||||
/// Figma 주석 `2097:16415` · 별첨 B 어디에도 수치가 없다(해석 확인 요청 영역D). 운영값 자리에 두고
|
||||
/// 회신을 받으면 값만 바꾼다. 기본값 80% · 60분은 회신 전 임시값이다.
|
||||
class HotArchiveRule {
|
||||
const HotArchiveRule({
|
||||
this.imminentPercent = 80,
|
||||
this.surgeWindow = const Duration(minutes: 60),
|
||||
});
|
||||
|
||||
factory HotArchiveRule.from(Map<String, dynamic>? policy) {
|
||||
final raw = policy?['hotArchive'];
|
||||
if (raw is! Map) return const HotArchiveRule();
|
||||
final percent = raw['imminentPercent'];
|
||||
final minutes = raw['surgeWindowMinutes'];
|
||||
return HotArchiveRule(
|
||||
imminentPercent: percent is num && percent > 0 ? percent.toDouble() : 80,
|
||||
surgeWindow: Duration(
|
||||
minutes: minutes is int && minutes > 0 ? minutes : 60,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 이 달성률(%) 이상이면 「목표 달성 임박」.
|
||||
final double imminentPercent;
|
||||
|
||||
/// 「실시간 기여 급증」을 재는 최근 구간.
|
||||
final Duration surgeWindow;
|
||||
}
|
||||
|
||||
/// HOT ARCHIVE 최대 5건의 순서 — 「목표 달성 임박 건 > 실시간 기여 급증 건 > 신규 등록 건」.
|
||||
///
|
||||
/// 임박(달성률 ≥ 기준)은 달성률 높은 순, 급증(최근 구간 후원 > 0)은 최근 후원 많은 순,
|
||||
/// 나머지는 신규 등록 순이다.
|
||||
List<String> hotArchiveOrder(
|
||||
List<({String id, double percent, int recentPop, DateTime createdAt})> all,
|
||||
HotArchiveRule rule,
|
||||
) {
|
||||
final imminent = all.where((c) => c.percent >= rule.imminentPercent).toList()
|
||||
..sort((a, b) => b.percent.compareTo(a.percent));
|
||||
final surging =
|
||||
all
|
||||
.where((c) => c.percent < rule.imminentPercent && c.recentPop > 0)
|
||||
.toList()
|
||||
..sort((a, b) => b.recentPop.compareTo(a.recentPop));
|
||||
final rest =
|
||||
all
|
||||
.where((c) => c.percent < rule.imminentPercent && c.recentPop <= 0)
|
||||
.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return [
|
||||
for (final c in [...imminent, ...surging, ...rest].take(5)) c.id,
|
||||
];
|
||||
}
|
||||
|
||||
/// 인기 K-INSIDE 3건 — 미해결(`status == open`) 먼저, 같은 묶음 안에서는 현상금 큰 순.
|
||||
/// 입력이 현상금 내림차순이라 미해결 묶음만 앞으로 옮기면 된다.
|
||||
List<Map<String, dynamic>> popularInsides(
|
||||
|
||||
Reference in New Issue
Block a user