광고 추가 보상 서버 시청 검증 · 사용자 퀘스트 승인 후 공개 · TIME ATTACK 모드 · 만보기 퀘스트 · 위치 퀘스트 지도 선택 · 운영 퀘스트 보상 재원

This commit is contained in:
Sn-Kinos
2026-10-05 22:48:09 +09:00
parent 4bb92c2ee7
commit fbf9a2c3d7
39 changed files with 1871 additions and 477 deletions
+25 -14
View File
@@ -69,15 +69,21 @@ class _InviteBackend extends Backend {
Future<FakeFirebaseFirestore> _seed({Duration left = const Duration(minutes: 5)}) async {
final db = FakeFirebaseFirestore();
await db.doc('users/$_uid').set({'uid': _uid, 'fandomId': 'bts'});
// 10/3 통합회신 — TIME ATTACK 은 원 Quest(여기서는 스트리밍)에 붙는 회차다.
await db.doc('quests/ta').set({
'type': 'timeattack', 'title': '돌발', 'fandomId': 'bts', 'rewardPop': 30,
'type': 'streaming', 'title': '돌발', 'fandomId': 'bts',
'dailyLimit': 1, 'active': true,
'activeFrom': Timestamp.fromDate(DateTime.now().subtract(const Duration(minutes: 1))),
'activeTo': Timestamp.fromDate(DateTime.now().add(left)),
'timeAttack': _round('r1', left),
});
return db;
}
Map<String, dynamic> _round(String id, Duration left) => {
'roundId': id, 'fandomId': 'bts', 'rewardPop': 30,
'activeFrom': Timestamp.fromDate(DateTime.now().subtract(const Duration(minutes: 1))),
'activeTo': Timestamp.fromDate(DateTime.now().add(left)),
};
BriefingItem _brief(int rank, String headline) => BriefingItem(
rank: rank,
headline: headline,
@@ -104,7 +110,7 @@ void main() {
final backend = _Backend(db);
final repo = FirestoreQuestRepository(backend);
final card = (await repo.load()).single;
expect(card.type, QuestType.timeAttack);
expect(card.type, QuestType.streaming);
expect(card.endsAt, isNotNull);
expect(card.rewardPop, 30);
final reward = await repo.verify(card);
@@ -123,33 +129,38 @@ void main() {
// 서버가 10분 앞서 있다 → 단말 기준 5분 남은 창은 이미 닫혔다.
final db = await _seed();
final backend = _Backend(db, serverAhead: const Duration(minutes: 10));
expect(await FirestoreQuestRepository(backend).load(), isEmpty);
// 회차가 닫히면 원 Quest 는 일반 Quest 로 남고 회차 표시(마감)만 빠진다.
expect((await FirestoreQuestRepository(backend).load()).single.endsAt, isNull);
expect(ServerClock.offset.inMinutes, inInclusiveRange(9, 10));
final home = await FirestoreHomeRepository(backend).load();
expect(home.quests.where((q) => q.id == 'ta'), isEmpty);
expect(home.quests.singleWhere((q) => q.id == 'ta').endsAt, isNull);
});
test('어제 받은 타임어택도 보상 완료로 남는다(1인 1회)', () async {
test('받은 회차는 날짜와 무관하게 보상 완료로 남는다(회차당 1인 1회)', () async {
final db = await _seed();
await db.doc('users/$_uid/questProgress/ta').set({'dayKey': '2000-01-01', 'count': 1});
await db.doc('users/$_uid/questProgress/ta').set({
'dayKey': '2000-01-01', 'count': 1, 'timeAttackRoundId': 'r1',
});
final card = (await FirestoreQuestRepository(_Backend(db)).load()).single;
expect(card.stage, QuestStage.rewarded);
});
test('홈은 열린 타임어택을 맨 앞에 두고 마감된 것은 뺀다', () async {
test('홈은 회차가 열린 Quest 를 맨 앞에 둔다', () async {
final db = await _seed();
await db.doc('quests/ta-old').set({
'type': 'timeattack', 'title': '지난 돌발', 'fandomId': 'bts', 'active': true,
'activeTo': Timestamp.fromDate(DateTime.now().subtract(const Duration(minutes: 1))),
'type': 'vote', 'title': '지난 돌발', 'fandomId': 'bts', 'active': true,
'timeAttack': _round('r-old', const Duration(minutes: -1)),
});
await db.doc('quests/ta-off').set({
'type': 'timeattack', 'title': '내려진 돌발', 'fandomId': 'bts', 'active': false,
'activeTo': Timestamp.fromDate(DateTime.now().add(const Duration(minutes: 5))),
'type': 'vote', 'title': '내려진 돌발', 'fandomId': 'bts', 'active': false,
'timeAttack': _round('r-off', const Duration(minutes: 5)),
});
await db.doc('quests/attend').set({'type': 'attend', 'title': '출석', 'active': true});
final home = await FirestoreHomeRepository(_Backend(db)).load();
expect(home.quests.map((q) => q.id), ['ta', 'attend']);
// 열린 회차가 맨 앞, 닫힌 회차의 원 Quest 는 일반 Quest 로, 내려진 Quest 는 빠진다.
expect(home.quests.map((q) => q.id), ['ta', 'ta-old', 'attend']);
expect(home.quests.first.endsAt, isNotNull);
expect(home.quests[1].endsAt, isNull);
});
test('성운 목표는 config/policy 운영값 · 탄생 권한은 창립자만(Q-PN-01)', () async {