248 lines
8.4 KiB
Dart
248 lines
8.4 KiB
Dart
// packages/feature_common/lib/screens/game_completion_screen.dart
|
|
import 'dart:developer';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:service_api/service_api.dart';
|
|
import '../models/game_result_args.dart';
|
|
|
|
enum _RankSubmissionStep { enterName, submitting, showList }
|
|
|
|
class GameCompletionScreen extends StatefulWidget {
|
|
final GameResultArgs args;
|
|
|
|
const GameCompletionScreen({super.key, required this.args});
|
|
|
|
@override
|
|
State<GameCompletionScreen> createState() => _GameCompletionScreenState();
|
|
}
|
|
|
|
class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
|
final PuzzleService _puzzleService = PuzzleService();
|
|
final IdentityService _identityService = IdentityService();
|
|
|
|
late final TextEditingController _nameController;
|
|
_RankSubmissionStep _rankStep = _RankSubmissionStep.enterName;
|
|
List<GameRankDto> _rankingList = [];
|
|
GameRankWithRankNumber? _myRankResult;
|
|
String? _dialogErrorMessage;
|
|
String _submittedPlayerName = "";
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// 🔽 [핵심 수정]
|
|
// 랭킹 등록 여부와 상관없이, 이 화면에 진입한 것 자체가 "레벨 클리어"이므로
|
|
// onProgressSave (레벨 잠금 해제)를 즉시 호출합니다.
|
|
// (playerName은 이 콜백에서 사용되지 않으므로 빈 값을 전달합니다.)
|
|
widget.args.onProgressSave("");
|
|
|
|
// --- (이하 기존 로직) ---
|
|
final session = context.read<SessionNotifier>().session;
|
|
_nameController = TextEditingController(text: session?.userName ?? widget.args.userName);
|
|
|
|
// 로그인 유저일 경우, 이름 입력 생략하고 자동 등록
|
|
if (session != null && !session.isGuest) {
|
|
_rankStep = _RankSubmissionStep.submitting;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_submitRank(autoSubmitName: session.userName);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submitRank({String? autoSubmitName}) async {
|
|
String playerName;
|
|
|
|
if (autoSubmitName == null) {
|
|
playerName = _nameController.text.trim();
|
|
if (playerName.isEmpty) {
|
|
setState(() { _dialogErrorMessage = "이름을 입력해주세요."; });
|
|
return;
|
|
}
|
|
} else {
|
|
playerName = autoSubmitName;
|
|
}
|
|
|
|
setState(() {
|
|
_rankStep = _RankSubmissionStep.submitting;
|
|
_submittedPlayerName = playerName;
|
|
_dialogErrorMessage = null;
|
|
});
|
|
|
|
final rankDto = UnifiedRankDto(
|
|
userId: widget.args.userId,
|
|
gameType: widget.args.gameType,
|
|
contextId: widget.args.contextId,
|
|
playerName: playerName,
|
|
primaryScore: widget.args.primaryScore,
|
|
secondaryScore: widget.args.secondaryScore,
|
|
);
|
|
|
|
try {
|
|
final RankSubmissionResult result = await _puzzleService.submitRank(rankDto);
|
|
|
|
if (autoSubmitName == null) {
|
|
await _identityService.saveUserName(playerName);
|
|
}
|
|
|
|
// 🔽 [수정]
|
|
// onProgressSave는 initState에서 이미 호출되었지만,
|
|
// saveMaxUnlockedLevel 함수 자체가 멱등성(Idempotent)을 가지므로
|
|
// (이미 레벨이 6인데 6으로 덮어써도 문제없음)
|
|
// 혹시 모를 실패에 대비해 여기서 한 번 더 호출해도 안전합니다.
|
|
await widget.args.onProgressSave(playerName);
|
|
|
|
setState(() {
|
|
_rankingList = result.topRanks;
|
|
_myRankResult = result.myRank;
|
|
_rankStep = _RankSubmissionStep.showList;
|
|
});
|
|
|
|
} catch (e) {
|
|
log("!!! 랭킹 등록 실패 !!!", error: e);
|
|
setState(() {
|
|
_rankStep = _RankSubmissionStep.enterName;
|
|
if (autoSubmitName != null) {
|
|
_rankStep = _RankSubmissionStep.showList;
|
|
}
|
|
_dialogErrorMessage = e.toString().replaceFirst("Exception: ", "");
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 닫기 버튼 로직
|
|
void _closeScreen() {
|
|
// 이 화면만 닫습니다. (GameScreen으로 돌아감)
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// ... (이하 UI 빌드 로직은 모두 동일) ...
|
|
final theme = Theme.of(context);
|
|
|
|
// --- UI 섹션 정의 ---
|
|
Widget topRankListWidget = _rankingList.isEmpty
|
|
? const Center(child: Text("현재 랭킹이 없습니다."))
|
|
: ListView.builder(
|
|
itemCount: _rankingList.length,
|
|
shrinkWrap: true,
|
|
itemBuilder: (context, index) {
|
|
final rank = _rankingList[index];
|
|
final bool isMe = rank.playerName == _submittedPlayerName;
|
|
final String scoreText = widget.args.scoreFormatter(rank.primaryScore, rank.secondaryScore);
|
|
|
|
return ListTile(
|
|
selected: isMe,
|
|
selectedTileColor: theme.primaryColor.withOpacity(0.1),
|
|
leading: Text('${index + 1}.', style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
title: Text(rank.playerName, style: TextStyle(fontWeight: isMe ? FontWeight.bold : FontWeight.normal)),
|
|
trailing: Text(scoreText, style: TextStyle(fontWeight: FontWeight.bold, color: theme.textTheme.bodyMedium?.color?.withOpacity(0.9))),
|
|
);
|
|
},
|
|
);
|
|
|
|
Widget? myRankWidget;
|
|
if (_myRankResult != null) {
|
|
final myRank = _myRankResult!.rankData;
|
|
final myRankNum = _myRankResult!.rankNumber;
|
|
bool isMeInTop10 = _rankingList.any((topRank) => topRank.playerName == myRank.playerName);
|
|
|
|
if (!isMeInTop10) {
|
|
final String scoreText = widget.args.scoreFormatter(myRank.primaryScore, myRank.secondaryScore);
|
|
myRankWidget = Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: ListTile(
|
|
selected: true,
|
|
selectedTileColor: theme.primaryColor.withOpacity(0.1),
|
|
leading: Text('$myRankNum.', style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
title: Text(myRank.playerName, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
trailing: Text(scoreText, style: TextStyle(fontWeight: FontWeight.bold, color: theme.textTheme.bodyMedium?.color?.withOpacity(0.9))),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget rankDisplaySection = Column(
|
|
children: [
|
|
if (_dialogErrorMessage != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
child: Text(_dialogErrorMessage!, style: TextStyle(color: theme.colorScheme.error)),
|
|
),
|
|
Expanded(child: topRankListWidget),
|
|
if (myRankWidget != null) ...[
|
|
const Divider(height: 16, thickness: 1),
|
|
myRankWidget,
|
|
],
|
|
],
|
|
);
|
|
|
|
Widget nameEntryWidget = Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('축하합니다! 랭킹에 등록할 이름을 입력하세요.', style: theme.textTheme.titleMedium),
|
|
const SizedBox(height: 20),
|
|
TextField(
|
|
controller: _nameController,
|
|
autofocus: true,
|
|
maxLength: 20,
|
|
decoration: InputDecoration(
|
|
labelText: '이름 (20자 이내)',
|
|
border: const OutlineInputBorder(),
|
|
errorText: _dialogErrorMessage,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
// --- 상태에 따라 UI와 버튼 결정 ---
|
|
Widget content;
|
|
List<Widget> actions = [];
|
|
String titleText;
|
|
|
|
if (_rankStep == _RankSubmissionStep.enterName) {
|
|
titleText = '🎉 게임 완료!';
|
|
content = nameEntryWidget;
|
|
actions = [
|
|
TextButton(onPressed: _closeScreen, child: const Text('나중에 하기')),
|
|
ElevatedButton(onPressed: () => _submitRank(), child: const Text('랭킹 등록')),
|
|
];
|
|
}
|
|
else if (_rankStep == _RankSubmissionStep.submitting) {
|
|
titleText = '랭킹 등록 중...';
|
|
content = const Center(child: CircularProgressIndicator());
|
|
}
|
|
else { // _RankSubmissionStep.showList
|
|
titleText = '🏆 랭킹 (${widget.args.contextId})';
|
|
content = rankDisplaySection;
|
|
actions = [
|
|
TextButton(onPressed: _closeScreen, child: const Text('닫기')),
|
|
];
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(titleText),
|
|
automaticallyImplyLeading: false,
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: content,
|
|
),
|
|
bottomNavigationBar: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: actions,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |