걸음 수 Health Connect·HealthKit · 위치 퀘스트 사진·QR 인증 · 행성 형태 확정
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'dart:typed_data' show Uint8List;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../core/ui/app_dialog.dart';
|
||||
@@ -17,6 +21,7 @@ import 'quest_ad_screens.dart';
|
||||
import 'quest_create_screen.dart' show QuestLinkCard;
|
||||
import 'quest_data.dart';
|
||||
import 'quest_evidence.dart';
|
||||
import 'qr_scan_screen.dart';
|
||||
import 'quest_reward_screen.dart';
|
||||
import 'widgets/quest_card_tile.dart';
|
||||
import 'widgets/time_attack_countdown.dart';
|
||||
@@ -251,6 +256,35 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
// 인증 방법 — PRD-05 MVP 수용 기준 「장소 상세에서 인증 방법 · 보상 · 참여 현황 확인」 · 원장 C-03.
|
||||
if (card.type == QuestType.place) ...[
|
||||
_Row(
|
||||
label: context.l10n.questVerifyMethods,
|
||||
value: [
|
||||
for (final m in detail.verifyMethods)
|
||||
switch (m) {
|
||||
'gps' => context.l10n.questVerifyGps,
|
||||
'photo' => context.l10n.questVerifyPhoto,
|
||||
'qr' => context.l10n.questVerifyQr,
|
||||
_ => m.toUpperCase(),
|
||||
},
|
||||
].join(' · '),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
// 생성자 — 현장에 붙일 QR(생성 때 서버가 발급). 참여자에게는 값이 오지 않는다.
|
||||
if (detail.qrCode case final code?) ...[
|
||||
Text(context.l10n.questQrForPlace, style: _caption),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: QrImageView(data: code, size: 180),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// 참여 전 간격 · 최대 횟수 안내(C-12).
|
||||
if (card.type == QuestType.place)
|
||||
Padding(
|
||||
@@ -826,6 +860,30 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// QR 스캔 · 인증 사진 — Quest 가 고른 수단만. 하나라도 취소하면 null.
|
||||
Future<PlaceProof?> _placeProof(List<String> methods) async {
|
||||
String? qr;
|
||||
if (methods.contains('qr')) {
|
||||
qr = await Navigator.of(context, rootNavigator: true).push<String>(
|
||||
MaterialPageRoute(builder: (_) => const QrScanScreen()),
|
||||
);
|
||||
if (qr == null) return null;
|
||||
}
|
||||
Uint8List? photo;
|
||||
if (methods.contains('photo')) {
|
||||
// B-44 이미지 10MB 상한 — 긴 변 1600 · JPEG 80 으로 줄여 올린다.
|
||||
final shot = await ImagePicker().pickImage(
|
||||
source: ImageSource.camera,
|
||||
maxWidth: 1600,
|
||||
maxHeight: 1600,
|
||||
imageQuality: 80,
|
||||
);
|
||||
if (shot == null) return null;
|
||||
photo = await shot.readAsBytes();
|
||||
}
|
||||
return PlaceProof(qr: qr, photo: photo);
|
||||
}
|
||||
|
||||
/// 보상받기.
|
||||
///
|
||||
/// 순서는 **참여 조건 판정 → 기본 보상 → 광고 선택**이다. 위치 · 권한 · 걸음 · 일일 한도는
|
||||
@@ -839,10 +897,17 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
if (_busy) return;
|
||||
// 판정과 지급은 서버가 한다. 화면은 결과를 받아 띄우기만 한다 —
|
||||
// 별첨 1 ② 「클라이언트 단독 지급 0건」.
|
||||
// 위치 Quest 의 추가 인증(C-03) — 고른 수단의 증거를 먼저 모은다. 취소하면 아무것도 보내지 않는다.
|
||||
var proof = const PlaceProof();
|
||||
if (detail.card.type == QuestType.place && detail.card.endsAt == null) {
|
||||
final collected = await _placeProof(detail.verifyMethods);
|
||||
if (collected == null || !mounted) return;
|
||||
proof = collected;
|
||||
}
|
||||
setState(() => _busy = true);
|
||||
final QuestReward reward;
|
||||
try {
|
||||
reward = await widget.repository.verify(detail.card);
|
||||
reward = await widget.repository.verify(detail.card, proof: proof);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
|
||||
Reference in New Issue
Block a user