FPI 사용자·국가 랭킹 서버 집계 · YouTube 스밍 · Music Shake 참여
This commit is contained in:
@@ -29,14 +29,23 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
this._backend, {
|
||||
StepCountProbe stepCount = deviceStepCount,
|
||||
PlaceProbe placeFix = devicePlaceFix,
|
||||
ShakeProbe shakes = deviceShakes,
|
||||
WalkBaseline? baseline,
|
||||
}) : _stepCount = stepCount,
|
||||
_placeFix = placeFix,
|
||||
_shakes = shakes,
|
||||
_baseline = baseline ?? WalkBaseline();
|
||||
|
||||
final Backend _backend;
|
||||
final StepCountProbe _stepCount;
|
||||
final PlaceProbe _placeFix;
|
||||
final ShakeProbe _shakes;
|
||||
|
||||
/// 마지막 Music Shake 한 판의 판정표. 다 끝난 판만 서버에 보낸다.
|
||||
ShakeJudge? _shakeRun;
|
||||
|
||||
/// 곡 BPM 이 문서에 없을 때 — 🟡 운영 Quest 는 `quests.bpm` 을 채운다.
|
||||
static const defaultBpm = 100.0;
|
||||
|
||||
/// 오늘 걸은 수를 내는 기준선. 하루 단위라 앱을 껐다 켜도 이어진다.
|
||||
final WalkBaseline _baseline;
|
||||
@@ -157,6 +166,9 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
final steps = type == QuestType.steps;
|
||||
final goal = steps
|
||||
? tiers[count.clamp(0, tiers.length - 1)]
|
||||
// Music Shake 의 목표는 곡의 구간 수다 — 진행도가 「n / 구간 수」로 찬다.
|
||||
: type == QuestType.musicShake
|
||||
? quest.intOr('totalSegments', dailyLimit)
|
||||
: dailyLimit;
|
||||
|
||||
return QuestCard(
|
||||
@@ -175,7 +187,7 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
: QuestStage.notStarted,
|
||||
goal: authorUid.isEmpty ? goal : quest.intOr('goal', 1),
|
||||
// 걸음은 센서를 구독해야 알 수 있어 시작 전에는 0 이다. 나머지는 당일 수행 횟수다.
|
||||
progress: steps ? (done ? goal : 0) : count,
|
||||
progress: type.gathers ? (done ? goal : 0) : count,
|
||||
rewardPop: type.rewardFrom(quest, policy),
|
||||
// 설계서 §2.8 의 `quests` 에 참여 인원 필드가 없다. 세는 곳이 없어 0 이다.
|
||||
participants: quest.intOr('participantCount'),
|
||||
@@ -435,6 +447,7 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
|
||||
@override
|
||||
Stream<int> progressStream(QuestCard card) {
|
||||
if (card.type == QuestType.musicShake) return _shakeProgress(card);
|
||||
if (card.type != QuestType.steps) return const Stream<int>.empty();
|
||||
// 센서는 부팅 이후 누적을 준다. 하루 단위 기준선을 빼야 정책 원장 **B-26** 의
|
||||
// 구간(1 천·5 천·1 만·1.5 만보)과 같은 것을 세게 된다.
|
||||
@@ -460,6 +473,10 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
QuestType.community => await _backend.call('fnQuestSubmit', {'questId': card.id}),
|
||||
QuestType.timeAttack =>
|
||||
await _backend.call('fnQuestTimeAttack', {'questId': card.id}),
|
||||
// 무보상 참여(C-04) — POP 원장 없이 참여 기록 · FPI 원천만 남는다.
|
||||
QuestType.streaming =>
|
||||
await _backend.call('fnQuestStreamParticipate', {'questId': card.id}),
|
||||
QuestType.musicShake => await _verifyShake(card),
|
||||
_ => throw BackendException('E-QST-01', card.type.blockedReason ?? ''),
|
||||
};
|
||||
|
||||
@@ -502,6 +519,53 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
/// 곡 박마다 한 구간 — 시작부터 박 수를 세어 내고, 마지막 구간 판정 창이 닫히면 구간 수를 낸다.
|
||||
/// 흔들기는 그동안만 모은다.
|
||||
Stream<int> _shakeProgress(QuestCard card) async* {
|
||||
_shakeRun = null;
|
||||
final quest = (await _backend.db.doc('quests/${card.id}').get()).data() ??
|
||||
const <String, dynamic>{};
|
||||
final segments = quest.intOr('totalSegments');
|
||||
final bpm = quest['bpm'] is num && (quest['bpm'] as num) > 0
|
||||
? (quest['bpm'] as num).toDouble()
|
||||
: defaultBpm;
|
||||
if (segments <= 0) {
|
||||
throw BackendException('E-QST-01', appL10n.questTypeUnverifiable);
|
||||
}
|
||||
final run = ShakeJudge(bpm: bpm, segments: segments, start: DateTime.now());
|
||||
final sub = _shakes().listen(run.add, onError: (_) {});
|
||||
try {
|
||||
for (var i = 1; i < segments; i++) {
|
||||
await Future<void>.delayed(run.beat);
|
||||
yield i;
|
||||
}
|
||||
await Future<void>.delayed(run.end.difference(DateTime.now()));
|
||||
_shakeRun = run;
|
||||
yield segments;
|
||||
} finally {
|
||||
await sub.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _verifyShake(QuestCard card) async {
|
||||
final run = _shakeRun;
|
||||
if (run == null) throw BackendException('E-QST-01', appL10n.questNoShake);
|
||||
final quest = (await _backend.db.doc('quests/${card.id}').get()).data() ??
|
||||
const <String, dynamic>{};
|
||||
final ids = quest['trackIds'];
|
||||
// 곡 ID 는 서버 대조용이다 — 화면에 띄우지 않는다.
|
||||
final trackId = ids is List && ids.isNotEmpty ? '${ids.first}' : quest.strOr('trackId');
|
||||
_shakeRun = null;
|
||||
return _backend.call('fnQuestVerifyMusicShake', {
|
||||
'questId': card.id,
|
||||
'trackId': trackId,
|
||||
'totalSegments': run.segments,
|
||||
'judgement': run.judgement(),
|
||||
'occurredAt': run.end.toUtc().toIso8601String(),
|
||||
...await _fandomField(),
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _verifyPlace(QuestCard card) async {
|
||||
final quest = await _backend.db
|
||||
.doc('quests/${card.id}')
|
||||
|
||||
Reference in New Issue
Block a user