퀘스트 일시 정지 · 이어하기 · 진행 상태 서버 저장 · 목록 보상받기

This commit is contained in:
Sn-Kinos
2026-10-06 03:20:46 +09:00
parent ecf5998725
commit abe5f1ae50
28 changed files with 1023 additions and 118 deletions
+71 -3
View File
@@ -8,6 +8,8 @@ library;
import 'dart:typed_data' show Uint8List;
import 'package:flutter/foundation.dart' show ValueNotifier;
import '../../core/data/backend.dart' show BackendException;
import '../../core/monetization/rewarded_ad.dart';
import '../../core/i18n/app_locale.dart';
@@ -28,6 +30,10 @@ enum QuestStage {
mine,
notStarted,
inProgress,
/// 화면 안에서 진행하는 Quest(Music Shake)를 진행 화면 밖으로 나와 멈춘 상태 — 원장 C-83(10/3 통합회신
/// Q-OB-01 A): 진행 상태 값 「일시 정지」 · 버튼 「이어하기」. Walk · 출석은 이 상태가 되지 않는다.
paused,
completed,
rewarded;
@@ -36,6 +42,7 @@ enum QuestStage {
mine => appL10n.questDetails,
notStarted => appL10n.questStart,
inProgress => appL10n.questInProgress,
paused => appL10n.questResume,
completed => appL10n.questClaim,
rewarded => appL10n.questRewardDone,
};
@@ -335,6 +342,42 @@ bool questFull(QuestCard card) =>
card.capacity! > 0 &&
card.participants >= card.capacity!;
/// 진행 세션 — `users/{uid}/questProgress/{questId}.session`(`fn-quest-progress` 만 쓴다 · 원장 C-83).
///
/// **표시용 상태다.** 화면을 다시 열거나 앱을 다시 켜도 「진행중」 탭 · 「이어하기」 · 목록 「보상받기」가
/// 남게 한다. 지급 판정은 여전히 검증 함수가 증거로 한다.
class QuestSession {
const QuestSession({
required this.state,
this.progress = 0,
this.total = 0,
this.position = Duration.zero,
this.hits = const [],
this.judgement,
});
/// [QuestStage.inProgress] · [QuestStage.paused] · [QuestStage.completed] 중 하나.
final QuestStage state;
/// 실행한 값 — 걸음 수 · Music Shake 판정이 끝난 박 수.
final int progress;
/// 목표 — 걸음 목표 · Music Shake 박 수.
final int total;
/// Music Shake 를 멈춘 영상 시각 — 「이어하기」 시작점.
final Duration position;
/// Music Shake 박마다 가장 가까운 흔들기의 거리(박 비율). 흔들기가 없으면 -1.
final List<double> hits;
/// 끝까지 한 Music Shake 판의 판정 합계 — 목록에서 바로 「보상받기」할 때 서버에 보낸다.
final Map<String, int>? judgement;
}
/// 일시 정지 · 진행 세션이 바뀌었다 — 탭 화면 하단 캡슐(원장 C-83)이 다시 읽는다.
final questSessionSignal = ValueNotifier<int>(0);
/// 목록 카드 하나.
class QuestCard {
const QuestCard({
@@ -354,12 +397,16 @@ class QuestCard {
this.endsAt,
this.publishState = '',
this.authorName = '',
this.session,
});
final String id;
final String title;
final String categoryName;
/// 서버에 남은 진행 세션(진행중 · 일시 정지 · 진행완료) — 원장 C-83. 없으면 null.
final QuestSession? session;
/// 만든 사람의 공개 닉네임(`quests.authorName`). 운영 Quest 는 빈 값이다.
/// 신고하기 「신고 정보 영역 — 작성자 닉네임」(Figma `548:21389`)에 쓴다.
final String authorName;
@@ -409,6 +456,7 @@ class QuestCard {
/// 상태·진행도만 바꾼 사본. [QuestCard] 는 불변이라 갈아 끼운다.
QuestCard copyWith({QuestStage? stage, int? progress}) => QuestCard(
session: session,
id: id,
title: title,
categoryName: categoryName,
@@ -536,8 +584,11 @@ enum QuestTab {
QuestTab.all => true,
// 명세 `518:88405` — *진행중 - 진행중 탭 / 진행완료 - 진행중 탭 / 보상완료 - 완료 탭*.
// 보상을 받기 전(진행완료)까지는 진행중 탭에 두고, 받은 뒤에 완료 탭으로 옮긴다.
// 일시 정지도 진행 중이다(원장 C-83 — 진행 상태 값에 「일시 정지」를 더함).
QuestTab.inProgress =>
card.stage == QuestStage.inProgress || card.stage == QuestStage.completed,
card.stage == QuestStage.inProgress ||
card.stage == QuestStage.paused ||
card.stage == QuestStage.completed,
QuestTab.done => card.stage == QuestStage.rewarded,
QuestTab.mine => card.stage == QuestStage.mine,
};
@@ -630,7 +681,17 @@ abstract interface class QuestRepository {
Future<void> cancel(String questId);
/// 상태 전이. 서버가 확정한 상태를 돌려준다.
Future<QuestStage> advance(String questId, QuestStage next);
///
/// 실행률을 모으는 Quest(걸음 · Music Shake)는 진행 세션을 서버에 남긴다(`fn-quest-progress` · C-83).
/// [session] 은 그 진행값이다. [QuestStage.notStarted] 는 중단(세션 삭제)이다.
Future<QuestStage> advance(
String questId,
QuestStage next, {
QuestSession? session,
});
/// 가장 최근에 일시 정지한 Quest 1건 — 탭 화면 하단 캡슐(원장 C-83 · QA 3-34). 없으면 null.
Future<QuestCard?> pausedQuest();
Future<String> create(QuestDraft draft);
@@ -881,7 +942,14 @@ class SampleQuestRepository implements QuestRepository {
Future<void> cancel(String questId) async {}
@override
Future<QuestStage> advance(String questId, QuestStage next) async => next;
Future<QuestStage> advance(
String questId,
QuestStage next, {
QuestSession? session,
}) async => next;
@override
Future<QuestCard?> pausedQuest() async => null;
@override
Future<String> create(QuestDraft draft) async => 'q-new';