games/packages/feature_common/lib/screens/game_completion_screen.dart
2025-11-19 17:00:33 +09:00

328 lines
11 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 = "";
// 🔽 [신규] 랭킹 등록을 건너뛰었는지 확인하는 플래그
bool _didSkipRank = false;
@override
void initState() {
super.initState();
// 레벨 클리어 (레벨 잠금 해제)를 즉시 호출
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);
}
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는 제거됨
);
}
}