퀘스트 일시 정지 · 이어하기 · 진행 상태 서버 저장 · 목록 보상받기
This commit is contained in:
@@ -43,10 +43,15 @@ class QuestDetailScreen extends StatefulWidget {
|
||||
super.key,
|
||||
required this.questId,
|
||||
required this.repository,
|
||||
this.autoAct = false,
|
||||
});
|
||||
|
||||
final String questId;
|
||||
|
||||
/// 목록 카드 버튼으로 들어왔다 — 상세를 읽자마자 그 버튼 동작(보상받기 · 이어하기 · 참여)을 한다.
|
||||
/// 명세 `518:88405` *보상 받기 버튼 클릭 시 POP 수령*(목록에서 바로).
|
||||
final bool autoAct;
|
||||
|
||||
/// 저장소는 호출부가 준다 — 화면이 표본을 기본값으로 만들지 않는다.
|
||||
final QuestRepository repository;
|
||||
|
||||
@@ -69,6 +74,12 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
/// 방금 끝낸 Music Shake 판의 판정 합계 — 결과 화면 · 결과 카드에 쓴다.
|
||||
Map<String, int>? _shakeResult;
|
||||
|
||||
/// 진행 세션(원장 C-83) — 서버에서 읽은 값을 이 화면에서 멈추면 바로 갈아 끼운다(「이어하기」 시작점).
|
||||
QuestSession? _session;
|
||||
|
||||
/// [QuestDetailScreen.autoAct] 를 한 번만 쓴다.
|
||||
bool _autoActed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -94,12 +105,35 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
setState(() {
|
||||
_detail = detail;
|
||||
_stage = detail.card.stage;
|
||||
_session = detail.card.session;
|
||||
});
|
||||
// 목록에서 「시작하기」를 눌러 진행중으로 들어온 경우다. 여기서 다시 켜지 않으면
|
||||
// 상세를 열어도 실행률이 0 에 멈춘 채로 남는다.
|
||||
if (_stage == QuestStage.inProgress && detail.card.type.gathers) {
|
||||
_watch(detail.card);
|
||||
}
|
||||
if (widget.autoAct && !_autoActed) {
|
||||
_autoActed = true;
|
||||
final stage = _stage!;
|
||||
if (stage == QuestStage.completed ||
|
||||
stage == QuestStage.paused ||
|
||||
stage == QuestStage.notStarted) {
|
||||
await _act(detail, stage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 진행 세션을 서버에 남긴다(원장 C-83). 실패해도 이 화면의 진행은 계속된다 — 표시용 상태다.
|
||||
Future<void> _persist(
|
||||
String questId,
|
||||
QuestStage next, [
|
||||
QuestSession? session,
|
||||
]) async {
|
||||
try {
|
||||
await widget.repository.advance(questId, next, session: session);
|
||||
} catch (error) {
|
||||
debugPrint('quest session save failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -231,6 +265,15 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
value:
|
||||
'${formatPop(_live ?? card.progress)} / ${formatPop(card.goal)}',
|
||||
),
|
||||
// 일시 정지 — 멈춘 시점의 진행값을 그대로 둔다(원장 C-83 · 해석 확인 요청서 Q-OB-01 A 5절).
|
||||
if (stage == QuestStage.paused && _session != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
_Row(
|
||||
label: context.l10n.questPaused,
|
||||
value:
|
||||
'${formatPop(_session!.progress)} / ${formatPop(_session!.total)}',
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
_Row(
|
||||
label: context.l10n.questReward,
|
||||
@@ -769,6 +812,8 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
}
|
||||
case QuestStage.inProgress:
|
||||
await _confirmStop(detail);
|
||||
case QuestStage.paused:
|
||||
await _playShake(detail, resume: _session);
|
||||
case QuestStage.completed:
|
||||
await _claim(detail);
|
||||
case QuestStage.mine:
|
||||
@@ -780,21 +825,23 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
|
||||
/// 「시작하기」 — 진행중으로 바꾸고 실측 수집을 켠다.
|
||||
Future<void> _start(QuestDetail detail) async {
|
||||
final next = await widget.repository.advance(
|
||||
detail.card.id,
|
||||
QuestStage.inProgress,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_stage = next;
|
||||
_stage = QuestStage.inProgress;
|
||||
_live = 0;
|
||||
});
|
||||
_watch(detail.card);
|
||||
// 진행중을 먼저 남긴다 — 곧바로 목표에 닿으면 진행완료 저장이 뒤에 와야 한다.
|
||||
await _persist(
|
||||
detail.card.id,
|
||||
QuestStage.inProgress,
|
||||
QuestSession(state: QuestStage.inProgress, total: detail.card.goal),
|
||||
);
|
||||
if (mounted) _watch(detail.card);
|
||||
}
|
||||
|
||||
/// Music Shake 한 판 — 공식 영상을 보며 흔드는 진행 화면을 띄운다(PRD-04).
|
||||
/// 끝까지 했으면 판정표를 남기고 「보상받기」로, 중간에 나왔으면 시작 전으로 돌린다.
|
||||
Future<void> _playShake(QuestDetail detail) async {
|
||||
/// 끝까지 했으면 판정표를 남기고 「보상받기」로, 중간에 나왔으면 **일시 정지**로 둔다(원장 C-83).
|
||||
/// [resume] 이 있으면 멈춘 판정 · 영상 시각부터 잇는다(「이어하기」).
|
||||
Future<void> _playShake(QuestDetail detail, {QuestSession? resume}) async {
|
||||
final MusicShakeSetup setup;
|
||||
try {
|
||||
setup = await widget.repository.musicShakeSetup(detail.card.id);
|
||||
@@ -803,22 +850,59 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
final judge = ShakeJudge(
|
||||
bpm: setup.bpm,
|
||||
segments: setup.segments,
|
||||
windows: setup.windows,
|
||||
hits: resume?.hits ?? const [],
|
||||
position: resume?.position ?? Duration.zero,
|
||||
);
|
||||
QuestSession snapshot(QuestStage state) => QuestSession(
|
||||
state: state,
|
||||
progress: judge.live(judge.position).judged,
|
||||
total: setup.segments,
|
||||
position: judge.position,
|
||||
hits: judge.hits,
|
||||
);
|
||||
setState(() => _stage = QuestStage.inProgress);
|
||||
// 진행 중 앱이 꺼져도 다음에 일시 정지(멈춘 자리)로 보이게 시작 시점을 남긴다.
|
||||
final started = _persist(
|
||||
detail.card.id,
|
||||
QuestStage.inProgress,
|
||||
snapshot(QuestStage.inProgress),
|
||||
);
|
||||
final run = await Navigator.of(context, rootNavigator: true)
|
||||
.push<ShakeJudge>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MusicShakePlayScreen(
|
||||
setup: setup,
|
||||
shakes: widget.repository.shakeEvents(),
|
||||
judge: judge,
|
||||
),
|
||||
),
|
||||
);
|
||||
// 시작 저장이 일시 정지 · 진행완료 저장보다 늦게 닿지 않게 기다린다.
|
||||
await started;
|
||||
if (!mounted) return;
|
||||
if (run != null) widget.repository.recordShake(run);
|
||||
final paused = snapshot(QuestStage.paused);
|
||||
setState(() {
|
||||
_shakeResult = run?.judgement();
|
||||
_stage = run == null ? QuestStage.notStarted : QuestStage.completed;
|
||||
_stage = run == null ? QuestStage.paused : QuestStage.completed;
|
||||
_session = run == null ? paused : null;
|
||||
});
|
||||
await _persist(
|
||||
detail.card.id,
|
||||
_stage!,
|
||||
run == null
|
||||
? paused
|
||||
: QuestSession(
|
||||
state: QuestStage.completed,
|
||||
progress: setup.segments,
|
||||
total: setup.segments,
|
||||
judgement: run.judgement(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 실시간 목표 실행률. 목표를 채우면 버튼이 「보상받기」로 바뀐다.
|
||||
@@ -832,12 +916,30 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
.listen(
|
||||
(value) {
|
||||
if (!mounted) return;
|
||||
final reached =
|
||||
card.goal > 0 &&
|
||||
value >= card.goal &&
|
||||
_stage != QuestStage.completed;
|
||||
setState(() {
|
||||
_live = value;
|
||||
if (card.goal > 0 && value >= card.goal) {
|
||||
_stage = QuestStage.completed;
|
||||
}
|
||||
});
|
||||
// 진행완료를 남긴다 — 목록 「보상받기」 · 진행중 탭이 다시 읽어도 같게(원장 C-83).
|
||||
if (reached) {
|
||||
unawaited(
|
||||
_persist(
|
||||
card.id,
|
||||
QuestStage.completed,
|
||||
QuestSession(
|
||||
state: QuestStage.completed,
|
||||
progress: value,
|
||||
total: card.goal,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (Object error) {
|
||||
// 권한 거부·센서 부재. 진행 상태를 시작 전으로 되돌려야 사용자가 다시
|
||||
@@ -863,16 +965,13 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
);
|
||||
if (!stop) return;
|
||||
_stopWatching();
|
||||
final next = await widget.repository.advance(
|
||||
detail.card.id,
|
||||
QuestStage.notStarted,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_stage = next;
|
||||
_stage = QuestStage.notStarted;
|
||||
_live = null;
|
||||
});
|
||||
}
|
||||
await _persist(detail.card.id, QuestStage.notStarted);
|
||||
}
|
||||
|
||||
/// QR 스캔 · 인증 사진 — Quest 가 고른 수단만. 하나라도 취소하면 null.
|
||||
@@ -933,6 +1032,10 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
|
||||
_stage = QuestStage.notStarted;
|
||||
});
|
||||
_fail(error);
|
||||
// 남겨 둔 진행완료 세션도 지운다 — 남기면 목록에 같은 판의 「보상받기」가 계속 뜬다.
|
||||
if (detail.card.type == QuestType.musicShake) {
|
||||
await _persist(detail.card.id, QuestStage.notStarted);
|
||||
}
|
||||
// 타임어택 창 밖(E-QST-01) — 서버가 본 상태로 다시 읽어 마감 · 완료를 반영한다.
|
||||
if (error is BackendException &&
|
||||
error.code == 'E-QST-01' &&
|
||||
|
||||
Reference in New Issue
Block a user