...
This commit is contained in:
@@ -28,8 +28,8 @@ class GameResultArgs {
|
||||
/// (예: 다음 레벨 잠금 해제)
|
||||
final Future<void> Function(String playerName) onProgressSave;
|
||||
|
||||
/// 팝업이 닫힐 때 게임 화면을 닫기 위한 콜백
|
||||
final VoidCallback onScreenClose;
|
||||
// ❌ [삭제] onScreenClose 콜백 제거
|
||||
// final VoidCallback onScreenClose;
|
||||
|
||||
GameResultArgs({
|
||||
required this.gameType,
|
||||
@@ -40,6 +40,7 @@ class GameResultArgs {
|
||||
this.userName,
|
||||
required this.scoreFormatter,
|
||||
required this.onProgressSave,
|
||||
required this.onScreenClose,
|
||||
// ❌ [삭제]
|
||||
// required this.onScreenClose,
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// packages/feature_common/lib/screens/game_completion_screen.dart
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart'; // 👈 [추가]
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:service_api/service_api.dart';
|
||||
import '../models/game_result_args.dart';
|
||||
|
||||
// 스도쿠/스파이더와 동일한 enum
|
||||
enum _RankSubmissionStep { enterName, submitting, showList }
|
||||
|
||||
class GameCompletionScreen extends StatefulWidget {
|
||||
@@ -17,11 +17,9 @@ class GameCompletionScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
// 서비스 초기화
|
||||
final PuzzleService _puzzleService = PuzzleService();
|
||||
final IdentityService _identityService = IdentityService();
|
||||
|
||||
// 상태 변수
|
||||
late final TextEditingController _nameController;
|
||||
_RankSubmissionStep _rankStep = _RankSubmissionStep.enterName;
|
||||
List<GameRankDto> _rankingList = [];
|
||||
@@ -32,17 +30,21 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 🔽 [수정] 세션에서 userName을 가져옴
|
||||
// (이 화면은 build 이전에 호출되므로 'read' 사용)
|
||||
|
||||
// 🔽 [핵심 수정]
|
||||
// 랭킹 등록 여부와 상관없이, 이 화면에 진입한 것 자체가 "레벨 클리어"이므로
|
||||
// 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;
|
||||
// build가 완료된 후 등록 시작
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// [중요] 세션의 userName으로 자동 제출
|
||||
_submitRank(autoSubmitName: session.userName);
|
||||
});
|
||||
}
|
||||
@@ -54,12 +56,9 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 랭킹 등록 로직 (공통화)
|
||||
// 🔽 [수정] _submitRank가 자동 제출용 이름을 받도록
|
||||
Future<void> _submitRank({String? autoSubmitName}) async {
|
||||
String playerName;
|
||||
|
||||
// 자동 제출(로그인 상태)이 아니면(게스트면), 컨트롤러에서 이름을 가져옴
|
||||
if (autoSubmitName == null) {
|
||||
playerName = _nameController.text.trim();
|
||||
if (playerName.isEmpty) {
|
||||
@@ -80,22 +79,23 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
userId: widget.args.userId,
|
||||
gameType: widget.args.gameType,
|
||||
contextId: widget.args.contextId,
|
||||
playerName: playerName, // 👈 [수정]
|
||||
playerName: playerName,
|
||||
primaryScore: widget.args.primaryScore,
|
||||
secondaryScore: widget.args.secondaryScore,
|
||||
);
|
||||
|
||||
try {
|
||||
// 1. 랭킹 등록
|
||||
final RankSubmissionResult result = await _puzzleService.submitRank(rankDto);
|
||||
|
||||
// 2. 이름 저장 (공통)
|
||||
// [수정] 게스트일 때만 이름을 저장 (소셜 로그인은 이미 이름이 있음)
|
||||
if (autoSubmitName == null) {
|
||||
await _identityService.saveUserName(playerName);
|
||||
}
|
||||
|
||||
// 3. 게임별 후속 처리 (레벨 잠금 해제 등)
|
||||
// 🔽 [수정]
|
||||
// onProgressSave는 initState에서 이미 호출되었지만,
|
||||
// saveMaxUnlockedLevel 함수 자체가 멱등성(Idempotent)을 가지므로
|
||||
// (이미 레벨이 6인데 6으로 덮어써도 문제없음)
|
||||
// 혹시 모를 실패에 대비해 여기서 한 번 더 호출해도 안전합니다.
|
||||
await widget.args.onProgressSave(playerName);
|
||||
|
||||
setState(() {
|
||||
@@ -108,7 +108,6 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
log("!!! 랭킹 등록 실패 !!!", error: e);
|
||||
setState(() {
|
||||
_rankStep = _RankSubmissionStep.enterName;
|
||||
// 🔽 [수정] 게스트가 아닐 때 실패하면, 이름 입력창 대신 리스트로 보냄
|
||||
if (autoSubmitName != null) {
|
||||
_rankStep = _RankSubmissionStep.showList;
|
||||
}
|
||||
@@ -119,18 +118,16 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
|
||||
/// 닫기 버튼 로직
|
||||
void _closeScreen() {
|
||||
// 1. 이 팝업 화면을 닫고
|
||||
// 이 화면만 닫습니다. (GameScreen으로 돌아감)
|
||||
Navigator.of(context).pop();
|
||||
// 2. 이전 화면(게임 화면)을 닫도록 콜백 호출
|
||||
widget.args.onScreenClose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ... (이하 UI 빌드 로직은 모두 동일) ...
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// --- UI 섹션 정의 (스도쿠/스파이더와 동일) ---
|
||||
|
||||
// --- UI 섹션 정의 ---
|
||||
Widget topRankListWidget = _rankingList.isEmpty
|
||||
? const Center(child: Text("현재 랭킹이 없습니다."))
|
||||
: ListView.builder(
|
||||
@@ -139,25 +136,14 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
itemBuilder: (context, index) {
|
||||
final rank = _rankingList[index];
|
||||
final bool isMe = rank.playerName == _submittedPlayerName;
|
||||
|
||||
// [수정] 점수 포맷터를 주입받은 함수로 대체
|
||||
final String scoreText = widget.args.scoreFormatter(
|
||||
rank.primaryScore,
|
||||
rank.secondaryScore
|
||||
);
|
||||
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)
|
||||
)
|
||||
),
|
||||
trailing: Text(scoreText, style: TextStyle(fontWeight: FontWeight.bold, color: theme.textTheme.bodyMedium?.color?.withOpacity(0.9))),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -166,18 +152,10 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
if (_myRankResult != null) {
|
||||
final myRank = _myRankResult!.rankData;
|
||||
final myRankNum = _myRankResult!.rankNumber;
|
||||
|
||||
bool isMeInTop10 = _rankingList.any(
|
||||
(topRank) => topRank.playerName == myRank.playerName
|
||||
);
|
||||
bool isMeInTop10 = _rankingList.any((topRank) => topRank.playerName == myRank.playerName);
|
||||
|
||||
if (!isMeInTop10) {
|
||||
// [수정] 점수 포맷터를 주입받은 함수로 대체
|
||||
final String scoreText = widget.args.scoreFormatter(
|
||||
myRank.primaryScore,
|
||||
myRank.secondaryScore
|
||||
);
|
||||
|
||||
final String scoreText = widget.args.scoreFormatter(myRank.primaryScore, myRank.secondaryScore);
|
||||
myRankWidget = Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: ListTile(
|
||||
@@ -185,13 +163,7 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
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)
|
||||
)
|
||||
),
|
||||
trailing: Text(scoreText, style: TextStyle(fontWeight: FontWeight.bold, color: theme.textTheme.bodyMedium?.color?.withOpacity(0.9))),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -215,16 +187,12 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
Widget nameEntryWidget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// [수정] 게임별 점수 표시 대신 범용 텍스트
|
||||
Text(
|
||||
'축하합니다! 랭킹에 등록할 이름을 입력하세요.',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Text('축하합니다! 랭킹에 등록할 이름을 입력하세요.', style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
autofocus: true,
|
||||
maxLength: 20, // [수정] 10 -> 20
|
||||
maxLength: 20,
|
||||
decoration: InputDecoration(
|
||||
labelText: '이름 (20자 이내)',
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -235,7 +203,6 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
);
|
||||
|
||||
// --- 상태에 따라 UI와 버튼 결정 ---
|
||||
|
||||
Widget content;
|
||||
List<Widget> actions = [];
|
||||
String titleText;
|
||||
@@ -244,37 +211,26 @@ class _GameCompletionScreenState extends State<GameCompletionScreen> {
|
||||
titleText = '🎉 게임 완료!';
|
||||
content = nameEntryWidget;
|
||||
actions = [
|
||||
TextButton(
|
||||
onPressed: _closeScreen, // 닫기
|
||||
child: const Text('나중에 하기'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => _submitRank(), // 👈 [수정] 인자 없이 호출
|
||||
child: const Text('랭킹 등록'),
|
||||
),
|
||||
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('닫기'),
|
||||
)
|
||||
TextButton(onPressed: _closeScreen, child: const Text('닫기')),
|
||||
];
|
||||
}
|
||||
|
||||
// [수정] AlertDialog가 아닌 전체 화면 Scaffold로 변경
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(titleText),
|
||||
automaticallyImplyLeading: false, // 뒤로가기 버튼 숨김
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
|
||||
Reference in New Issue
Block a user