// 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 createState() => _GameCompletionScreenState(); } class _GameCompletionScreenState extends State { final PuzzleService _puzzleService = PuzzleService(); final IdentityService _identityService = IdentityService(); late final TextEditingController _nameController; _RankSubmissionStep _rankStep = _RankSubmissionStep.enterName; List _rankingList = []; GameRankWithRankNumber? _myRankResult; String? _dialogErrorMessage; String _submittedPlayerName = ""; @override void initState() { super.initState(); // ๐Ÿ”ฝ [ํ•ต์‹ฌ ์ˆ˜์ •] // ๋žญํ‚น ๋“ฑ๋ก ์—ฌ๋ถ€์™€ ์ƒ๊ด€์—†์ด, ์ด ํ™”๋ฉด์— ์ง„์ž…ํ•œ ๊ฒƒ ์ž์ฒด๊ฐ€ "๋ ˆ๋ฒจ ํด๋ฆฌ์–ด"์ด๋ฏ€๋กœ // onProgressSave (๋ ˆ๋ฒจ ์ž ๊ธˆ ํ•ด์ œ)๋ฅผ ์ฆ‰์‹œ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. // (playerName์€ ์ด ์ฝœ๋ฐฑ์—์„œ ์‚ฌ์šฉ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋นˆ ๊ฐ’์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.) widget.args.onProgressSave(""); // --- (์ดํ•˜ ๊ธฐ์กด ๋กœ์ง) --- final session = context.read().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 _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 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, ), ), ); } }