걸음 수 Health Connect·HealthKit · 위치 퀘스트 사진·QR 인증 · 행성 형태 확정
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart' show SettableMetadata;
|
||||
|
||||
import '../../core/data/backend.dart';
|
||||
import '../../core/data/server_clock.dart';
|
||||
@@ -28,16 +29,19 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
FirestoreQuestRepository(
|
||||
this._backend, {
|
||||
StepCountProbe stepCount = deviceStepCount,
|
||||
TodayStepsProbe todaySteps = deviceTodaySteps,
|
||||
PlaceProbe placeFix = devicePlaceFix,
|
||||
ShakeProbe shakes = deviceShakes,
|
||||
WalkBaseline? baseline,
|
||||
}) : _stepCount = stepCount,
|
||||
_todaySteps = todaySteps,
|
||||
_placeFix = placeFix,
|
||||
_shakes = shakes,
|
||||
_baseline = baseline ?? WalkBaseline();
|
||||
|
||||
final Backend _backend;
|
||||
final StepCountProbe _stepCount;
|
||||
final TodayStepsProbe _todaySteps;
|
||||
final PlaceProbe _placeFix;
|
||||
final ShakeProbe _shakes;
|
||||
|
||||
@@ -249,9 +253,23 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
moderationReason: quest['moderation'] is Map
|
||||
? (quest['moderation'] as Map)['reason'] as String?
|
||||
: null,
|
||||
verifyMethods: _verifyMethodsOf(quest),
|
||||
qrCode: quest.strOr('authorUid') == uid && _verifyMethodsOf(quest).contains('qr')
|
||||
? await db
|
||||
.doc('questSecrets/$questId')
|
||||
.get()
|
||||
.then((s) => s.data()?['qrCode'] as String?, onError: (Object _) => null)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 위치 Quest 의 인증 수단(C-03). 문서에 없으면 GPS 하나다(운영 Quest · 예전 문서).
|
||||
List<String> _verifyMethodsOf(Map<String, dynamic> quest) {
|
||||
final raw = quest['verifyMethods'];
|
||||
final methods = raw is List ? raw.whereType<String>().toList() : <String>[];
|
||||
return methods.isEmpty ? const ['gps'] : methods;
|
||||
}
|
||||
|
||||
QuestLink _link(Map<String, dynamic> m) => QuestLink(
|
||||
url: m.strOr('url'),
|
||||
status: m.strOr('status'),
|
||||
@@ -476,6 +494,7 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
if (draft.pin case final pin?)
|
||||
'pin': {'lat': pin.lat, 'lng': pin.lng, 'address': pin.name},
|
||||
if (draft.goal != null) 'goal': draft.goal,
|
||||
if (draft.extraVerify.isNotEmpty) 'extraVerify': draft.extraVerify,
|
||||
});
|
||||
return result.strOr('questId');
|
||||
}
|
||||
@@ -489,16 +508,37 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
@override
|
||||
Stream<int> progressStream(QuestCard card) {
|
||||
if (card.type != QuestType.steps) return const Stream<int>.empty();
|
||||
// 센서는 부팅 이후 누적을 준다. 하루 단위 기준선을 빼야 정책 원장 **B-26** 의
|
||||
// 구간(1 천·5 천·1 만·1.5 만보)과 같은 것을 세게 된다.
|
||||
return _stepCount().asyncMap((cumulative) async {
|
||||
_walkSteps = await _baseline.todaySteps(cumulative);
|
||||
return _walkSteps;
|
||||
});
|
||||
return _steps();
|
||||
}
|
||||
|
||||
/// Health 걸음 갱신 주기. Health Connect · HealthKit 은 변경을 밀어 주지 않아 다시 묻는다.
|
||||
static const _healthPoll = Duration(seconds: 15);
|
||||
|
||||
/// 오늘 걸음 — Health(0시 KST 부터 합계, B-02)가 1차, 못 쓰면 기기 센서(B-26 「권한 거부 시 앱 자체 집계」).
|
||||
Stream<int> _steps() async* {
|
||||
var health = await _todaySteps();
|
||||
if (health == null) {
|
||||
// 센서는 부팅 이후 누적을 준다. 하루 단위 기준선을 빼야 정책 원장 **B-26** 의
|
||||
// 구간(1 천·5 천·1 만·1.5 만보)과 같은 것을 세게 된다.
|
||||
yield* _stepCount().asyncMap((cumulative) async {
|
||||
_walkSteps = await _baseline.todaySteps(cumulative);
|
||||
return _walkSteps;
|
||||
});
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
_walkSteps = health!;
|
||||
yield health;
|
||||
await Future<void>.delayed(_healthPoll);
|
||||
health = await _todaySteps() ?? health;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<QuestReward> verify(QuestCard card) async {
|
||||
Future<QuestReward> verify(
|
||||
QuestCard card, {
|
||||
PlaceProof proof = const PlaceProof(),
|
||||
}) async {
|
||||
// 열린 TIME ATTACK 회차 — 원 Quest 유형과 무관하게 회차 참여로 보낸다(10/3 통합회신 Q-TA-01).
|
||||
final reason = card.endsAt != null ? null : card.type.blockedReason;
|
||||
if (reason != null) throw BackendException('E-QST-01', reason);
|
||||
@@ -511,7 +551,7 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
// 쓰고 앱은 문서 ID 로 읽어, 지급은 되는데 화면은 「시작하기」에 머문다.
|
||||
QuestType.attend => await _backend.call('fnQuestAttend', {'questId': card.id}),
|
||||
QuestType.steps => await _verifySteps(card),
|
||||
QuestType.place => await _verifyPlace(card),
|
||||
QuestType.place => await _verifyPlace(card, proof),
|
||||
// Community 는 서버가 응원글 · 반응을 확인해 즉시 지급한다(C-11). 증거를 싣지 않는다.
|
||||
QuestType.community => await _backend.call('fnQuestSubmit', {'questId': card.id}),
|
||||
// 무보상 참여(C-04) — POP 원장 없이 참여 기록 · FPI 원천만 남는다.
|
||||
@@ -630,7 +670,7 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _verifyPlace(QuestCard card) async {
|
||||
Future<Map<String, dynamic>> _verifyPlace(QuestCard card, PlaceProof proof) async {
|
||||
final quest = await _backend.db
|
||||
.doc('quests/${card.id}')
|
||||
.get()
|
||||
@@ -642,7 +682,23 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
throw BackendException('E-QST-03', appL10n.questNoPlace);
|
||||
}
|
||||
final fix = await _placeFix();
|
||||
// 사진은 Storage 에 올리고 경로를 보낸다 — 서버가 접두(본인 · 이 Quest)와 파일 존재를 다시 본다.
|
||||
String? photoPath;
|
||||
if (proof.photo case final bytes?) {
|
||||
photoPath =
|
||||
'questProofs/${_backend.requireUid}/${card.id}/${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
try {
|
||||
await _backend.storage
|
||||
.ref(photoPath)
|
||||
.putData(bytes, SettableMetadata(contentType: 'image/jpeg'));
|
||||
} on FirebaseException {
|
||||
throw BackendException('E-QST-01', appL10n.photoUploadFailed);
|
||||
}
|
||||
}
|
||||
return _backend.call('fnQuestVerifyPlace', {
|
||||
// 추가 인증 수단(C-03) — GPS 와 함께 보낸다. 판정은 서버가 한다.
|
||||
if (proof.qr case final qr? when qr.isNotEmpty) 'qr': qr,
|
||||
'photo': ?photoPath,
|
||||
// 참여 횟수는 Quest 단위로 센다(정책 원장 C-12 — 같은 장소의 다른 Quest 는 별개).
|
||||
'questId': card.id,
|
||||
'placeId': placeId,
|
||||
|
||||
Reference in New Issue
Block a user