광고 추가 보상 서버 시청 검증 · 사용자 퀘스트 승인 후 공개 · TIME ATTACK 모드 · 만보기 퀘스트 · 위치 퀘스트 지도 선택 · 운영 퀘스트 보상 재원

This commit is contained in:
Sn-Kinos
2026-10-05 22:48:09 +09:00
parent 4bb92c2ee7
commit fbf9a2c3d7
39 changed files with 1871 additions and 477 deletions
+22 -10
View File
@@ -35,6 +35,8 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
final _manualTitle = TextEditingController();
final _reward = TextEditingController();
final _participants = TextEditingController();
// 만보기 목표 걸음(Figma `259:45798` 「목표 0 / 10,000」).
final _goal = TextEditingController();
/// 이 화면 한 번의 생성 요청 키 — 응답 유실 뒤 다시 눌러도 서버가 한 건으로 묶는다.
final _clientSeq = DateTime.now().microsecondsSinceEpoch.toString();
@@ -52,7 +54,7 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
@override
void initState() {
super.initState();
for (final c in [_title, _description, _manualTitle, _reward, _participants]) {
for (final c in [_title, _description, _manualTitle, _reward, _participants, _goal]) {
// 입력이 바뀔 때마다 미리보기와 완료 버튼 활성화가 함께 움직여야 한다.
c.addListener(() => setState(() {}));
}
@@ -65,7 +67,7 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
@override
void dispose() {
for (final c in [_title, _description, _url, _manualTitle, _reward, _participants]) {
for (final c in [_title, _description, _url, _manualTitle, _reward, _participants, _goal]) {
c.dispose();
}
super.dispose();
@@ -88,6 +90,7 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
bool get _ready {
if (_activity == null || _title.text.trim().isEmpty) return false;
if (_needs('place') && _place == null) return false;
if (_needs('goal') && _intOf(_goal) <= 0) return false;
if (_needs('link')) {
final link = _link;
if (link == null || !(link.ok || (link.failed && _manualTitle.text.trim().isNotEmpty))) {
@@ -105,7 +108,7 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
categoryName: _activity?.name ?? _category?.name ?? context.l10n.questNoCategory,
author: QuestAuthor.user,
stage: QuestStage.mine,
goal: 1,
goal: _needs('goal') ? _intOf(_goal) : 1,
progress: 0,
rewardPop: _intOf(_reward),
// 시안 — *참여 인원 : 0 / 0*. 분모가 모집 인원이다.
@@ -284,6 +287,15 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
const SizedBox(height: AppSpace.s3),
],
if (_needs('goal')) ...[
TextField(
controller: _goal,
keyboardType: TextInputType.number,
decoration: _field(context.l10n.questGoalSteps),
),
const SizedBox(height: AppSpace.s3),
],
if (_needs('place')) ...[
ListTile(
contentPadding: EdgeInsets.zero,
@@ -377,13 +389,11 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
if (!mounted) return;
_places = places;
}
final name = await Navigator.of(context, rootNavigator: true).push<String>(
MaterialPageRoute(
builder: (_) => PlacePickerScreen(places: [for (final p in _places) p.name]),
),
final picked = await Navigator.of(context, rootNavigator: true).push<QuestPlace>(
MaterialPageRoute(builder: (_) => PlacePickerScreen(places: _places)),
);
if (name == null || !mounted) return;
setState(() => _place = _places.firstWhere((p) => p.name == name));
if (picked == null || !mounted) return;
setState(() => _place = picked);
}
/// 공개일을 1일째로 최대 [QuestCatalog.maxDays] 일째까지. 남은 1시간 미만 판정은 서버가 한다.
@@ -433,7 +443,9 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
endDate: _endDate,
url: _needs('link') ? _link?.url : null,
manualTitle: _needs('link') && (_link?.failed ?? false) ? _manualTitle.text.trim() : null,
placeId: _needs('place') ? _place?.id : null,
placeId: _needs('place') && !(_place?.isPin ?? true) ? _place?.id : null,
pin: _needs('place') && (_place?.isPin ?? false) ? _place : null,
goal: _needs('goal') ? _intOf(_goal) : null,
),
);
} on BackendException catch (error) {