퀘스트 목록 페이지 판정 · 다시 읽기 · 캠페인 응원 수를 고유 후원자 기준으로

This commit is contained in:
Sn-Kinos
2026-10-06 02:19:40 +09:00
parent 807e446b8b
commit 82662b9b9f
8 changed files with 164 additions and 18 deletions
@@ -60,6 +60,7 @@ class FirestoreQuestRepository implements QuestRepository {
@override
Future<List<QuestCard>> load({int limit = 10}) async {
_cursor = null;
_exhausted = false;
return _page(limit);
}
@@ -67,19 +68,37 @@ class FirestoreQuestRepository implements QuestRepository {
Future<List<QuestCard>> more({required int offset, int limit = 10}) =>
_page(limit);
/// 원본 문서를 끝까지 읽었는가. 끝은 **걸러내기 전 문서 수**로 판정한다 — 비활성 ·
/// 마감 문서가 한 페이지를 채워도 뒤 문서가 사라지지 않게(2026-10-06 실기기: 11번째 이후 누락).
bool _exhausted = false;
/// 보이는 카드가 [limit] 장 모일 때까지(또는 문서가 끝날 때까지) 페이지를 이어 읽는다.
/// 그래서 [limit] 장보다 적게 돌려주면 문서가 끝났다는 뜻이다(`QuestRepository` 계약).
Future<List<QuestCard>> _page(int limit) async {
final cards = <QuestCard>[];
while (cards.length < limit && !_exhausted) {
cards.addAll(await _batch(limit));
}
return cards;
}
Future<List<QuestCard>> _batch(int limit) async {
final uid = _backend.requireUid;
final db = _backend.db;
// 정렬하지 않는다. 설계서 §2.8 의 `quests` 에 정렬 키가 없고, Admin 이 쓰는
// 문서에는 `createdAt` 이 없다(`fn-admin-write` 는 `updatedAt` 만 찍는다).
// `orderBy('createdAt')` 로 두면 그 문서들이 목록에서 조용히 사라진다.
var query = db.collection('quests').limit(limit);
// 커서는 기본 순서(문서 ID)를 따른다. 커서를 limit 보다 먼저 건다 — 서버는 순서를 가리지 않지만
// 테스트용 fake 는 적은 순서대로 적용한다.
Query<Map<String, dynamic>> query = db.collection('quests');
final cursor = _cursor;
if (cursor != null) query = query.startAfterDocument(cursor);
query = query.limit(limit);
final snap = await query.get();
if (snap.docs.isNotEmpty) _cursor = snap.docs.last;
if (snap.docs.length < limit) _exhausted = true;
// 타임어택 창은 서버 시각이다. 회차가 붙은 Quest 가 있을 때만 서버 시계를 잰다(세션당 1회).
if (snap.docs.any((d) => d.data()['timeAttack'] is Map)) {