// 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 = ""; // ๐Ÿ”ฝ [์‹ ๊ทœ] ๋žญํ‚น ๋“ฑ๋ก์„ ๊ฑด๋„ˆ๋›ฐ์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ”Œ๋ž˜๊ทธ bool _didSkipRank = false; @override void initState() { super.initState(); // ๋ ˆ๋ฒจ ํด๋ฆฌ์–ด (๋ ˆ๋ฒจ ์ž ๊ธˆ ํ•ด์ œ)๋ฅผ ์ฆ‰์‹œ ํ˜ธ์ถœ 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); } 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 _skipRankAndClose() { setState(() { _didSkipRank = true; _rankStep = _RankSubmissionStep.showList; // ๋ฆฌ์ŠคํŠธ ํ™”๋ฉด์œผ๋กœ ์ „ํ™˜ํ•˜์—ฌ ๊ธฐ๋ก์€ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ํ•จ }); } /// ๐Ÿ”ฝ [์‹ ๊ทœ] ์ ์ˆ˜ ํ‘œ์‹œ ์œ„์ ฏ (์ตœ์ƒ๋‹จ ๊ณ ์ •) Widget _buildScoreWidget(ThemeData theme) { final String scoreText = widget.args.scoreFormatter( widget.args.primaryScore, widget.args.secondaryScore); return Padding( padding: const EdgeInsets.only(bottom: 20.0), child: Card( color: theme.colorScheme.primary.withOpacity(0.1), margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( '๋‚˜์˜ ์ตœ์ข… ๊ธฐ๋ก', style: theme.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), const SizedBox(height: 8), Text( scoreText, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), ], ), ), ), ); } /// ๐Ÿ”ฝ [์‹ ๊ทœ] ์ด๋ฆ„ ์ž…๋ ฅ ๋ฐ ๋ฒ„ํŠผ ์„น์…˜ (ํ‚ค๋ณด๋“œ ๋Œ€์‘) Widget _buildNameEntrySection(ThemeData theme) { return 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, ), ), const SizedBox(height: 16), // [๐Ÿ”ฅ ์ˆ˜์ •] ๋ฒ„ํŠผ์„ ์ž…๋ ฅ์ฐฝ ๋ฐ”๋กœ ์•„๋ž˜ ๋ฐฐ์น˜ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: _skipRankAndClose, child: const Text('๊ฑด๋„ˆ๋›ฐ๊ธฐ')), const SizedBox(width: 10), ElevatedButton( onPressed: () => _submitRank(), child: const Text('๋žญํ‚น ๋“ฑ๋ก')), ], ), ], ); } /// ๐Ÿ”ฝ [์‹ ๊ทœ] ๋žญํ‚น ๋ฆฌ์ŠคํŠธ ์„น์…˜ (๊ธฐ๋ก ๋ณด๊ธฐ) Widget _buildRankingListSection(ThemeData theme) { if (_didSkipRank) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('๋žญํ‚น ๋“ฑ๋ก์„ ๊ฑด๋„ˆ๋›ฐ์—ˆ์Šต๋‹ˆ๋‹ค.', style: theme.textTheme.titleMedium), const SizedBox(height: 10), Text('๊ธฐ๋ก์€ ์œ„ "๋‚˜์˜ ์ตœ์ข… ๊ธฐ๋ก"์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.', style: theme.textTheme.bodyMedium), const SizedBox(height: 20), ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('๋กœ๋น„๋กœ ๋Œ์•„๊ฐ€๊ธฐ'), ) ], ), ); } // ๋žญํ‚น ๋ฆฌ์ŠคํŠธ (๊ธฐ์กด ๋กœ์ง๊ณผ ์œ ์‚ฌ) Widget topRankListWidget = _rankingList.isEmpty ? const Center(child: Text("๋“ฑ๋ก๋œ ๋žญํ‚น์ด ์—†์Šต๋‹ˆ๋‹ค.")) : ListView.builder( itemCount: _rankingList.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), // SingleChildScrollView ๋‚ด๋ถ€์ด๋ฏ€๋กœ ํ•„์š” 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.colorScheme.secondary.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))), ), ); } } return Expanded( child: SingleChildScrollView( child: Column( children: [ if (_dialogErrorMessage != null) Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text(_dialogErrorMessage!, style: TextStyle(color: theme.colorScheme.error)), ), topRankListWidget, if (myRankWidget != null) ...[ const Divider(height: 16, thickness: 1), myRankWidget, ], const SizedBox(height: 40), ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('๋กœ๋น„๋กœ ๋Œ์•„๊ฐ€๊ธฐ'), ) ], ), ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); String titleText; Widget content; if (_rankStep == _RankSubmissionStep.enterName) { titleText = '๐ŸŽ‰ ๊ฒŒ์ž„ ์™„๋ฃŒ!'; content = _buildNameEntrySection(theme); // ์ด๋ฆ„ ์ž…๋ ฅ ์„น์…˜ } else if (_rankStep == _RankSubmissionStep.submitting) { titleText = '๋žญํ‚น ๋“ฑ๋ก ์ค‘...'; content = const Center(child: CircularProgressIndicator()); } else { // _RankSubmissionStep.showList titleText = _didSkipRank ? 'โœ… ๊ธฐ๋ก ํ™•์ธ' : '๐Ÿ† ๋žญํ‚น ๋“ฑ๋ก ์™„๋ฃŒ'; content = _buildRankingListSection(theme); // ๋žญํ‚น ๋ฆฌ์ŠคํŠธ ์„น์…˜ } return Scaffold( appBar: AppBar( title: Text(titleText), automaticallyImplyLeading: false, ), body: SafeArea( child: Column( children: [ // 1. [๐Ÿ”ฅ ์ˆ˜์ •] ์ตœ์ข… ๊ธฐ๋ก ์„น์…˜ (์Šคํฌ๋กค๊ณผ ๋ถ„๋ฆฌ๋œ ์ตœ์ƒ๋‹จ) _buildScoreWidget(theme), // 2. [๐Ÿ”ฅ ์ˆ˜์ •] ๋ฉ”์ธ ์ปจํ…์ธ  ์„น์…˜ if (_rankStep == _RankSubmissionStep.enterName || _rankStep == _RankSubmissionStep.submitting) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: content, ) else Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: content, ), ), ], ), ), // โŒ bottomNavigationBar๋Š” ์ œ๊ฑฐ๋จ ); } }