From 3b6328cc4c7fc16c9e3a916aad0a49cbb391341e Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Tue, 11 Nov 2025 17:45:02 +0900 Subject: [PATCH] ... --- lib/screens/game_screen.dart | 195 ++++++++++++++++---------- lib/screens/home_screen.dart | 215 ++++++++++++++++++++++++----- lib/services/identity_service.dart | 43 +++++- lib/services/puzzle_service.dart | 17 ++- lib/widgets/number_pad.dart | 27 ++-- 5 files changed, 373 insertions(+), 124 deletions(-) diff --git a/lib/screens/game_screen.dart b/lib/screens/game_screen.dart index 509c452..f22e408 100644 --- a/lib/screens/game_screen.dart +++ b/lib/screens/game_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:flutter/material.dart'; -import 'package:sudoku_app/models/game_level.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] ๋ ˆ๋ฒจ ๋ชจ๋ธ ์ž„ํฌํŠธ +import 'package:sudoku_app/models/game_level.dart'; import 'package:sudoku_app/models/sudoku_game_dto.dart'; import 'package:sudoku_app/models/sudoku_theme.dart'; import 'package:sudoku_app/models/unified_rank_dto.dart'; @@ -20,7 +20,7 @@ class GameScreen extends StatefulWidget { final String themeName; final String userId; final String? userName; - final int levelIndex; // ๐Ÿ‘ˆ HomeScreen์—์„œ ์ „๋‹ฌ๋ฐ›์€ ํ˜„์žฌ ๋ ˆ๋ฒจ + final int levelIndex; const GameScreen({ super.key, @@ -39,12 +39,11 @@ class _GameScreenState extends State { final PuzzleService _puzzleService = PuzzleService(); final IdentityService _identityService = IdentityService(); - late final GameLevel currentLevel; // ๐Ÿ‘ˆ ํ˜„์žฌ ๋ ˆ๋ฒจ ์ •๋ณด + late final GameLevel currentLevel; late final int blockSize; late final int gridSize; - late final SudokuTheme activeTheme; // ๐Ÿ‘ˆ ์ด๋ฒˆ ๊ฒŒ์ž„์˜ '๋™์ ' ํ…Œ๋งˆ + late final SudokuTheme activeTheme; - // ๋ชจ๋“  ๋‚ด๋ถ€ ๋กœ์ง์€ 'int'๋กœ ๊ด€๋ฆฌ late List puzzleCells; late List solutionCells; late List originalCells; @@ -60,7 +59,9 @@ class _GameScreenState extends State { _RankSubmissionStep _rankStep = _RankSubmissionStep.enterName; List _rankingList = []; String _submittedPlayerName = ""; - + + // ์คŒ ์ปจํŠธ๋กค๋Ÿฌ + late final TransformationController _transformationController; // "A" -> 10 (ํŒŒ์‹ฑ์šฉ) int _charToInt(String char) { @@ -85,31 +86,28 @@ class _GameScreenState extends State { @override void initState() { super.initState(); - // 1. HomeScreen์—์„œ ๋ฐ›์€ levelIndex๋กœ ํ˜„์žฌ ๋ ˆ๋ฒจ ์ •๋ณด ๋กœ๋“œ currentLevel = AppLevels.getLevel(widget.levelIndex); blockSize = currentLevel.blockSize; gridSize = blockSize * blockSize; - // 2. ๐Ÿ”ฝ [์ˆ˜์ •] ํ…Œ๋งˆ ์ •์ฑ… ์ ์šฉ - String themeForThisGame = widget.themeName; + _transformationController = TransformationController(); - // ๐Ÿ”ฝ [์ˆ˜์ •] 'isEasyMode' ๋ณ€์ˆ˜๋ฅผ 'GameLevel'์˜ ์†์„ฑ๋“ค๋กœ ์กฐํ•ฉ + // ๐Ÿ”ฝ [์ˆ˜์ •] 'isEasyMode'๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ณ„์‚ฐํ•˜์—ฌ ์ „๋‹ฌ + String themeForThisGame = widget.themeName; bool isEasyMode = currentLevel.isSequentialNumbers || currentLevel.isSequentialLetters; if (currentLevel.isSequentialNumbers) { - themeForThisGame = AppThemes.numbers; // ๐Ÿ‘ˆ ์ˆซ์ž ํ…Œ๋งˆ ๊ฐ•์ œ + themeForThisGame = AppThemes.numbers; } else if (currentLevel.isSequentialLetters) { - themeForThisGame = AppThemes.letters; // ๐Ÿ‘ˆ ์•ŒํŒŒ๋ฒณ ํ…Œ๋งˆ ๊ฐ•์ œ + themeForThisGame = AppThemes.letters; } - - // 3. '์ด๋ฒˆ ๊ฒŒ์ž„'์˜ ํ…Œ๋งˆ ๊ฐ์ฒด ์ƒ์„ฑ + activeTheme = AppThemes.buildGameTheme( themeForThisGame, gridSize, - isEasyMode: isEasyMode, // ๐Ÿ‘ˆ [์ˆ˜์ •] ์กฐํ•ฉ๋œ bool ๊ฐ’ ์ „๋‹ฌ + isEasyMode: isEasyMode, // ๐Ÿ‘ˆ ์ˆ˜์ •๋œ bool ๊ฐ’ ); - // 4. ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ(String)๋ฅผ ๋‚ด๋ถ€ ๋กœ์ง(int)์œผ๋กœ ๋ณ€ํ™˜ puzzleCells = widget.gameData.question.split('').map(_charToInt).toList(); solutionCells = widget.gameData.solution.split('').map(_charToInt).toList(); originalCells = widget.gameData.question.split('').map(_charToInt).toList(); @@ -120,6 +118,7 @@ class _GameScreenState extends State { @override void dispose() { timer?.cancel(); + _transformationController.dispose(); super.dispose(); } @@ -213,6 +212,7 @@ class _GameScreenState extends State { selectedIndex = null; selectedNumberPad = null; score = 5; + _resetBoardZoom(); timer?.cancel(); secondsElapsed = 0; @@ -224,8 +224,11 @@ class _GameScreenState extends State { Navigator.of(context).pop(); } + void _resetBoardZoom() { + _transformationController.value = Matrix4.identity(); + } + - // ์ •๋‹ต ํ™•์ธ API ํ˜ธ์ถœ Future _validateGame() async { if (isValidating) return; setState(() { isValidating = true; }); @@ -263,9 +266,9 @@ class _GameScreenState extends State { } } - // ๋žญํ‚น ๋“ฑ๋ก ํŒ์—… (2๋‹จ๊ณ„ UI + ๋ ˆ๋ฒจ ์ž ๊ธˆ ํ•ด์ œ) void _showRankingDialog() { final nameController = TextEditingController(text: widget.userName); + // (์ƒํƒœ ๋ณ€์ˆ˜๋“ค์€ ๋ณ€๊ฒฝ ์—†์Œ) bool isSubmitting = false; String? dialogErrorMessage; @@ -282,6 +285,7 @@ class _GameScreenState extends State { return StatefulBuilder( builder: (context, setDialogState) { + // (์œ„์ ฏ ๋นŒ๋“œ ๋กœ์ง์€ ๋ณ€๊ฒฝ ์—†์Œ) Widget closeButton = TextButton( onPressed: () { Navigator.of(ctx).pop(); @@ -359,8 +363,12 @@ class _GameScreenState extends State { secondaryScore: (5 - score), ); + // ๐Ÿ”ฝ [์ˆ˜์ •] ์—ฌ๊ธฐ๊ฐ€ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. try { - await _puzzleService.submitRank(rankDto); + // 1. submitRank๊ฐ€ ์ด์ œ ๋žญํ‚น ๋ฆฌ์ŠคํŠธ๋ฅผ ์ง์ ‘ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + final List ranks = await _puzzleService.submitRank(rankDto); + + // 2. ๋žญํ‚น ๋“ฑ๋ก ์„ฑ๊ณต ํ›„ ๋กœ์ปฌ ์ž‘์—… ์ˆ˜ํ–‰ await _identityService.saveUserName(playerName); final int currentMaxLevel = await _identityService.getMaxUnlockedLevel(); @@ -375,10 +383,12 @@ class _GameScreenState extends State { } } - final ranks = await _puzzleService.fetchRanks('SUDOKU', contextId); + // 3. ๐Ÿ”ฝ [์‚ญ์ œ] ๋ณ„๋„๋กœ ๋žญํ‚น์„ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ฌ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + // final ranks = await _puzzleService.fetchRanks('SUDOKU', contextId); + // 4. submitRank๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ๋žญํ‚น ๋ฆฌ์ŠคํŠธ๋กœ ์ฆ‰์‹œ UI ์—…๋ฐ์ดํŠธ setDialogState(() { - _rankingList = ranks; + _rankingList = ranks; // ๐Ÿ‘ˆ ๋ฐ˜ํ™˜๋œ ๋žญํ‚น ๋ฆฌ์ŠคํŠธ ์‚ฌ์šฉ _rankStep = _RankSubmissionStep.showList; }); @@ -393,6 +403,9 @@ class _GameScreenState extends State { child: const Text('๋žญํ‚น ๋“ฑ๋ก'), ); + // (dialogContent, dialogActions ๋“ฑ ๋‚˜๋จธ์ง€ ๋กœ์ง์€ ๋ณ€๊ฒฝ ์—†์Œ) + // ... + // ... Widget dialogContent; if (_rankStep == _RankSubmissionStep.showList) { dialogContent = rankListWidget; @@ -435,7 +448,6 @@ class _GameScreenState extends State { ); } - // ๋žญํ‚น ID ์ƒ์„ฑ์„ ์œ„ํ•ด, ์›๋ณธ ๋ฌธ์ œ์˜ ๋นˆ์นธ ๋น„์œจ๋กœ ๋ ˆ๋ฒจ(1~5)์„ ์—ญ์‚ฐ int _difficultyLevel(String question) { int holes = question.split('').where((c) => c == '0').length; double holeRatio = holes / (gridSize * gridSize); @@ -482,6 +494,7 @@ class _GameScreenState extends State { ); } + // ์„ธ๋กœ ๋ชจ๋“œ(์ผ๋ฐ˜ ํฐ) ๋ ˆ์ด์•„์›ƒ Widget _buildPortraitLayout(BuildContext context, Map numberCounts, BoxConstraints constraints, String formattedTime) { final double boardWidth = (constraints.maxWidth > 600) ? 600 : constraints.maxWidth; @@ -492,19 +505,22 @@ class _GameScreenState extends State { children: [ Padding( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), - child: _buildGameInfoWidget(formattedTime), + child: _buildGameInfoWidget(formattedTime), // ๐Ÿ‘ˆ ์ƒ๋‹จ ์ปจํŠธ๋กค ๋ฐ” ), Expanded( child: Center( child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.symmetric(vertical: 16.0), // ๐Ÿ‘ˆ ๋ณด๋“œ/ํŒจ๋“œ ์ƒํ•˜ ํŒจ๋”ฉ child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - _buildSudokuBoardWidget(), + _buildSudokuBoardWidget(), // ๐Ÿ‘ˆ ์คŒ ๊ธฐ๋Šฅ const SizedBox(height: 15), - _buildNumberPadWidget(context, numberCounts, isLandscape: false, boardWidth: boardWidth), + Padding( // ๐Ÿ‘ˆ ํ•˜๋‹จ ์ปจํŠธ๋กค ํŒจ๋„์—๋งŒ ์ขŒ์šฐ ํŒจ๋”ฉ + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _buildControlPanelWidget(context, numberCounts, isLandscape: false, boardWidth: boardWidth), + ), ], ), ), @@ -517,29 +533,35 @@ class _GameScreenState extends State { ); } + // ๊ฐ€๋กœ ๋ชจ๋“œ(ํด๋”๋ธ”/ํƒœ๋ธ”๋ฆฟ) ๋ ˆ์ด์•„์›ƒ Widget _buildLandscapeLayout(BuildContext context, Map numberCounts, BoxConstraints constraints, String formattedTime) { const double infoBarHeight = 60.0; + // ๐Ÿ”ฝ [์ˆ˜์ •] ๋ณด๋“œ ๋„ˆ๋น„๋Š” (์‚ฌ์šฉ ๊ฐ€๋Šฅ ๋†’์ด - ์ƒ๋‹จ ์ปจํŠธ๋กค๋ฐ” ๋†’์ด - ์ƒํ•˜ ํŒจ๋”ฉ) double boardWidth = constraints.maxHeight - infoBarHeight - 32.0; + // ๐Ÿ”ฝ [์ˆ˜์ •] ์ปจํŠธ๋กค ํŒจ๋„ ๋„ˆ๋น„ ๊ณ„์‚ฐ + double controlPanelWidth; const double numberPadScaleRatio = 0.6; double padWidth = boardWidth * numberPadScaleRatio; if (padWidth < 200) padWidth = 200; if (padWidth > 350) padWidth = 350; + controlPanelWidth = padWidth + 100; // 100 for buttons - double totalWidth = boardWidth + (padWidth + 100) + 16.0; - if (totalWidth > (constraints.maxWidth - 32.0)) { + // ๐Ÿ”ฝ [์ˆ˜์ •] ํ™”๋ฉด ๋„ˆ๋น„์— ๋งž๊ฒŒ ์ „์ฒด ํฌ๊ธฐ ๋™์  ์กฐ์ ˆ + double totalWidth = boardWidth + controlPanelWidth + 16.0; // 16 for spacing + if (totalWidth > (constraints.maxWidth - 32.0)) { // 32 for screen padding double scale = (constraints.maxWidth - 32.0) / totalWidth; boardWidth *= scale; - padWidth *= scale; + controlPanelWidth *= scale; } return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ - _buildGameInfoWidget(formattedTime), + _buildGameInfoWidget(formattedTime), // ๐Ÿ‘ˆ ์ƒ๋‹จ ์ปจํŠธ๋กค ๋ฐ” Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -547,15 +569,15 @@ class _GameScreenState extends State { children: [ SizedBox( width: boardWidth, - child: _buildSudokuBoardWidget(), + child: _buildSudokuBoardWidget(), // ๐Ÿ‘ˆ ์คŒ ๊ธฐ๋Šฅ ), const SizedBox(width: 16), SizedBox( - width: padWidth + 100, + width: controlPanelWidth, // ๐Ÿ‘ˆ ๊ณ„์‚ฐ๋œ ๋„ˆ๋น„ child: SingleChildScrollView( child: Column( children: [ - _buildNumberPadWidget(context, numberCounts, isLandscape: true, boardWidth: boardWidth), + _buildControlPanelWidget(context, numberCounts, isLandscape: true, boardWidth: boardWidth), ], ), ), @@ -568,6 +590,7 @@ class _GameScreenState extends State { ); } + // ๐Ÿ”ฝ [์ˆ˜์ •] ์ƒ๋‹จ ์ •๋ณด (์ ์ˆ˜, ์‹œ๊ฐ„๋งŒ) Widget _buildGameInfoWidget(String formattedTime) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -575,48 +598,48 @@ class _GameScreenState extends State { children: [ Text('SCORE: $score', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), Text(formattedTime, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: onHintTapped, - icon: const Icon(Icons.lightbulb_outline, color: Colors.orange), - iconSize: 30, - ), - IconButton( - onPressed: onUndoTapped, - icon: const Icon(Icons.undo, color: Colors.red), - iconSize: 30, - ), - ], - ) ], ); } + // ๊ฒŒ์ž„ ๋ณด๋“œ (์คŒ ๊ธฐ๋Šฅ + ๋กฑํ”„๋ ˆ์Šค ๋ฆฌ์…‹) Widget _buildSudokuBoardWidget() { - return SudokuBoard( - blockSize: blockSize, - theme: activeTheme, - cells: puzzleCells, - originalCells: originalCells, - selectedIndex: selectedIndex, - selectedNumberPad: selectedNumberPad, - incorrectCells: incorrectCells, - onCellTapped: onCellTapped, + return GestureDetector( // ๐Ÿ‘ˆ ๋กฑํ”„๋ ˆ์Šค ๊ฐ์ง€ + onLongPress: _resetBoardZoom, + child: InteractiveViewer( + transformationController: _transformationController, // ๐Ÿ‘ˆ ์ปจํŠธ๋กค๋Ÿฌ ์—ฐ๊ฒฐ + boundaryMargin: const EdgeInsets.all(20.0), + minScale: 1.0, + maxScale: 2.5, + child: SudokuBoard( + blockSize: blockSize, + theme: activeTheme, + cells: puzzleCells, + originalCells: originalCells, + selectedIndex: selectedIndex, + selectedNumberPad: selectedNumberPad, + incorrectCells: incorrectCells, + onCellTapped: onCellTapped, + ), + ), ); } - Widget _buildNumberPadWidget(BuildContext context, Map numberCounts, {required bool isLandscape, required double boardWidth}) { + // ๐Ÿ”ฝ [์ˆ˜์ •] ํ•˜๋‹จ ์ปจํŠธ๋กค ํŒจ๋„ (๋„˜๋ฒ„ํŒจ๋“œ + ๋ชจ๋“  ๋ฒ„ํŠผ) + Widget _buildControlPanelWidget(BuildContext context, Map numberCounts, {required bool isLandscape, required double boardWidth}) { const double numberPadScaleRatio = 0.6; double? padMaxWidth; if (!isLandscape) { + // ์„ธ๋กœ ๋ชจ๋“œ: (๋ณด๋“œ ๋„ˆ๋น„ * 0.6) padMaxWidth = boardWidth * numberPadScaleRatio; } else { - padMaxWidth = null; + // ๊ฐ€๋กœ ๋ชจ๋“œ: (๋ณด๋“œ ๋„ˆ๋น„ * 0.6) -> ๊ฐ€๋กœ๋ชจ๋“œ์—์„œ๋„ ๋น„์œจ ์œ ์ง€ + padMaxWidth = boardWidth * numberPadScaleRatio; + if (padMaxWidth < 200) padMaxWidth = 200; // ์ตœ์†Œ ๋„ˆ๋น„ } + // 1. ์ˆซ์ž ๊ทธ๋ฆฌ๋“œ Widget numberPadGrid = ConstrainedBox( constraints: BoxConstraints(maxWidth: padMaxWidth ?? double.infinity), child: NumberPad( @@ -629,19 +652,47 @@ class _GameScreenState extends State { ), ); - Widget quitButton = IconButton( - icon: Icon(Icons.close, color: Colors.red.shade700, size: 30), - onPressed: _onQuitGameTapped, - tooltip: "๊ฒŒ์ž„ ์ข…๋ฃŒ", + // 2. ์™ผ์ชฝ ๋ฒ„ํŠผ (์ข…๋ฃŒ, ๋‹ค์‹œํ•˜๊ธฐ) + Widget leftButtons = Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.close, color: Colors.red.shade700, size: 30), + onPressed: _onQuitGameTapped, + tooltip: "๊ฒŒ์ž„ ์ข…๋ฃŒ", + ), + IconButton( + icon: Icon(Icons.refresh, color: Colors.blue.shade700, size: 30), + onPressed: _onRestartGameTapped, + tooltip: "๋‹ค์‹œํ•˜๊ธฐ", + ), + ], ); - Widget restartButton = IconButton( - icon: Icon(Icons.refresh, color: Colors.blue.shade700, size: 30), - onPressed: _onRestartGameTapped, - tooltip: "๋‹ค์‹œํ•˜๊ธฐ", + // 3. ์˜ค๋ฅธ์ชฝ ๋ฒ„ํŠผ (ํžŒํŠธ, ๋˜๋Œ๋ฆฌ๊ธฐ) + Widget rightButtons = Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: onHintTapped, + icon: const Icon(Icons.lightbulb_outline, color: Colors.orange), + iconSize: 30, + tooltip: "ํžŒํŠธ", + ), + IconButton( + onPressed: onUndoTapped, + icon: const Icon(Icons.undo, color: Colors.red), + iconSize: 30, + tooltip: "๋˜๋Œ๋ฆฌ๊ธฐ", + ), + ], ); + // 4. ๋ ˆ์ด์•„์›ƒ ์กฐ๋ฆฝ if (isLandscape) { + // ๊ฐ€๋กœ ๋ชจ๋“œ: (์„ธ๋กœ) Column -> [ NumberPad, (๊ฐ€๋กœ) Row [๋ฒ„ํŠผ 4๊ฐœ] ] return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -649,18 +700,22 @@ class _GameScreenState extends State { const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [quitButton, restartButton], + children: [ + leftButtons, // ๐Ÿ‘ˆ Column + rightButtons // ๐Ÿ‘ˆ Column + ], ) ], ); } else { + // ์„ธ๋กœ ๋ชจ๋“œ: (๊ฐ€๋กœ) Row -> [ ์™ผ์ชฝ ๋ฒ„ํŠผ, Expanded(NumberPad), ์˜ค๋ฅธ์ชฝ ๋ฒ„ํŠผ ] return Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - quitButton, + leftButtons, Expanded(child: numberPadGrid), - restartButton, + rightButtons, ], ); } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 274bfbc..05d2669 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,5 +1,7 @@ +import 'dart:developer'; import 'package:flutter/material.dart'; -import 'package:sudoku_app/models/game_level.dart'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] +import 'package:sudoku_app/models/game_level.dart'; +import 'package:sudoku_app/models/game_rank_dto.dart'; import 'package:sudoku_app/models/sudoku_game_dto.dart'; import 'package:sudoku_app/models/sudoku_theme.dart'; import 'package:sudoku_app/screens/game_screen.dart'; @@ -8,6 +10,8 @@ import 'package:sudoku_app/services/puzzle_service.dart'; import 'package:sudoku_app/services/identity_service.dart'; import 'package:sudoku_app/widgets/ad_banner_widget.dart'; +// ๐Ÿ”ฝ [์‚ญ์ œ] RankChangeStatus enum์€ ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -16,8 +20,13 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - // '์ตœ๋Œ€ ์ž ๊ธˆ ํ•ด์ œ ๋ ˆ๋ฒจ' ์ƒํƒœ int _maxUnlockedLevel = 1; + // ๐Ÿ”ฝ [์ˆ˜์ •] ๋žญํ‚น ๋ณ€๋™ ์ƒํƒœ ๋Œ€์‹  (์ด์ „ ๋žญํ‚น, ํ˜„์žฌ ๋žญํ‚น) ํŠœํ”Œ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + // (Key: levelIndex, Value: (oldRank, currentRank)) + // (0์€ ๋žญํ‚น์— ์—†์Œ์„ ์˜๋ฏธ) + Map _rankHistory = {}; + String? _userName; + late String _selectedThemeName; bool _isLoading = false; @@ -28,48 +37,106 @@ class _HomeScreenState extends State { void initState() { super.initState(); _selectedThemeName = AppThemes.random; - _loadProgress(); // ๐Ÿ‘ˆ ์ €์žฅ๋œ ์ง„ํ–‰ ์ƒํ™ฉ ๋กœ๋“œ + _loadProgress(); } - // ๋กœ์ปฌ ์ €์žฅ์†Œ์—์„œ ํด๋ฆฌ์–ด ๋ ˆ๋ฒจ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + // ๐Ÿ”ฝ [์‚ญ์ œ] _calculateRankStatus ํ—ฌํผ ํ•จ์ˆ˜๋Š” ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + + // ๐Ÿ”ฝ [์ˆ˜์ •] ๋žญํ‚น ๋ณ€๋™ ๋งต์„ (int, int) ํŠœํ”Œ๋กœ ์ €์žฅํ•˜๋„๋ก ์ˆ˜์ • Future _loadProgress() async { + // 1. ๊ธฐ๋ณธ ์ •๋ณด ๋กœ๋“œ (๋ ˆ๋ฒจ, ์œ ์ € ์ด๋ฆ„) final maxLevel = await _identityService.getMaxUnlockedLevel(); + final String? myName = await _identityService.getSavedUserName(); + if (mounted) { setState(() { _maxUnlockedLevel = maxLevel; + _userName = myName; }); } + + if (myName == null) { + return; + } + + try { + // 3-1. ์ด์ „์— ์ €์žฅ๋œ ๋žญํ‚น ๋งต ๋กœ๋“œ + final Map oldRankMap = await _identityService.getLastSavedRankMap(); + + // 3-2. ๋ชจ๋“  ๋ ˆ๋ฒจ์˜ ๋žญํ‚น์„ ๋ณ‘๋ ฌ๋กœ ์กฐํšŒ + List>> rankFutures = []; + for (final level in AppLevels.allLevels) { + rankFutures.add(_puzzleService.fetchRanks('SUDOKU', level.contextId)); + } + + final List> allRankResults = await Future.wait(rankFutures); + + // 3-4. ๊ฒฐ๊ณผ ๋ถ„์„ + Map newRankMapForStorage = {}; // ๐Ÿ‘ˆ ๋‹ค์Œ์— ์ €์žฅํ•  ์ƒˆ๋กœ์šด ๋žญํ‚น ๋งต + Map newRankHistoryForState = {}; // ๐Ÿ‘ˆ UI์— ๋ฐ˜์˜ํ•  ๋ณ€๋™ ๋งต + + for (int i = 0; i < AppLevels.allLevels.length; i++) { + final level = AppLevels.allLevels[i]; + final currentRanks = allRankResults[i]; + final int levelIndex = level.levelIndex; + + // 3-5. ์ด ๋ ˆ๋ฒจ์˜ ์ด์ „ ๋žญํ‚น (์—†์œผ๋ฉด 0) + final int oldRank = oldRankMap[levelIndex] ?? 0; + + // 3-6. ์ด ๋ ˆ๋ฒจ์˜ ํ˜„์žฌ ๋žญํ‚น (์—†์œผ๋ฉด 0) + int currentRank = 0; + int myRankIndex = currentRanks.indexWhere((r) => r.playerName == myName); + if (myRankIndex != -1) { + currentRank = myRankIndex + 1; // 1-based ์ˆœ์œ„ + } + + // 3-7. ์ƒํƒœ ์ €์žฅ (์ˆซ์ž ์Œ ์ž์ฒด๋ฅผ ์ €์žฅ) + newRankMapForStorage[levelIndex] = currentRank; + newRankHistoryForState[levelIndex] = (oldRank, currentRank); + } + + // 4. ์ƒˆ๋กœ์šด ๋žญํ‚น ๋งต์„ ๋กœ์ปฌ์— ์ €์žฅ (๋‹ค์Œ ์‹คํ–‰ ์‹œ ๋น„๊ต์šฉ) + await _identityService.saveLastRankMap(newRankMapForStorage); + + // 5. UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ + if (mounted) { + setState(() { + _rankHistory = newRankHistoryForState; // ๐Ÿ‘ˆ ํŠœํ”Œ ๋งต์œผ๋กœ UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ + }); + } + log("๋ชจ๋“  ๋ ˆ๋ฒจ ๋žญํ‚น ๋ณ€๋™ ํ™•์ธ ์™„๋ฃŒ. (์œ ์ €: $myName)"); + + } catch (e) { + log("HomeScreen: ๋žญํ‚น ํ™•์ธ ์‹คํŒจ: $e"); + } } - // ๊ฒŒ์ž„ ์‹œ์ž‘ ๋กœ์ง + + // (startGame ํ•จ์ˆ˜๋Š” ๋ณ€๊ฒฝ ์—†์Œ) Future _startGame(GameLevel level) async { setState(() { _isLoading = true; }); try { - // 1. ์„ ํƒํ•œ ๋ ˆ๋ฒจ์˜ '์ธ๋ฑ์Šค'(1-11)๋ฅผ ๋ฌธ์ž์—ด๋กœ ์ „๋‹ฌ final String difficulty = level.levelIndex.toString(); - final SudokuGameDto gameData = await _puzzleService.startGame(difficulty); final String userId = await _identityService.getOrCreateUserId(); - final String? userName = await _identityService.getSavedUserName(); + final String? userName = _userName; if (mounted) { - // 2. GameScreen์œผ๋กœ ์ด๋™ (๊ฒŒ์ž„ ํด๋ฆฌ์–ด ํ›„ ๋Œ์•„์˜ค๋ฉด _loadProgress() ํ˜ธ์ถœ) await Navigator.push( context, MaterialPageRoute( builder: (context) => GameScreen( gameData: gameData, - themeName: _selectedThemeName, // ๐Ÿ‘ˆ '๋žœ๋ค' ๋˜๋Š” '๊ณผ์ผ' ๋“ฑ + themeName: _selectedThemeName, userId: userId, userName: userName, - levelIndex: level.levelIndex, // ๐Ÿ‘ˆ 1~11 + levelIndex: level.levelIndex, ), ), ); - // 3. GameScreen์—์„œ ๋Œ์•„์™”์„ ๋•Œ, ์ง„ํ–‰ ์ƒํ™ฉ(ํด๋ฆฌ์–ด)์„ ๋‹ค์‹œ ๋กœ๋“œ _loadProgress(); } } catch (e) { @@ -85,9 +152,9 @@ class _HomeScreenState extends State { } } + // ๐Ÿ”ฝ [์ˆ˜์ •] build ๋ฉ”์†Œ๋“œ์—์„œ ๋žญํ‚น ์ˆซ์ž(ํŠœํ”Œ)๋ฅผ ์ง์ ‘ ๋น„๊ตํ•˜์—ฌ UI ์ƒ์„ฑ @override Widget build(BuildContext context) { - // 99 = ๋ชจ๋“  ๋ ˆ๋ฒจ ํด๋ฆฌ์–ด final bool allLevelsUnlocked = _maxUnlockedLevel >= 99; return Scaffold( @@ -95,7 +162,6 @@ class _HomeScreenState extends State { body: LayoutBuilder( builder: (context, constraints) { const double maxContentRatio = 0.6; - // ๐Ÿ”ฝ [์ˆ˜์ •] ํƒœ๋ธ”๋ฆฟ ๋“ฑ์—์„œ ๋„ˆ๋ฌด ์ปค์ง€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ (์ตœ๋Œ€ 500px) final double constrainedWidth = (constraints.maxHeight * maxContentRatio) > 500 ? 500 : (constraints.maxHeight * maxContentRatio); @@ -104,7 +170,7 @@ class _HomeScreenState extends State { constraints: BoxConstraints(maxWidth: constrainedWidth), child: Column( children: [ - // 1. ํ…Œ๋งˆ ์„ ํƒ + // 1. ํ…Œ๋งˆ ์„ ํƒ (๋ณ€๊ฒฝ ์—†์Œ) Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), child: Row( @@ -129,15 +195,65 @@ class _HomeScreenState extends State { ), ), - // 2. ๐Ÿ”ฝ [์ˆ˜์ •] ๋ ˆ๋ฒจ ์„ ํƒ ๋ฆฌ์ŠคํŠธ (Slider ๋Œ€์ฒด) + // 2. ๋ ˆ๋ฒจ ์„ ํƒ ๋ฆฌ์ŠคํŠธ Expanded( child: ListView.builder( itemCount: AppLevels.allLevels.length, itemBuilder: (context, index) { final GameLevel level = AppLevels.allLevels[index]; - // 3. ์ž ๊ธˆ ํ•ด์ œ ๋กœ์ง final bool isUnlocked = allLevelsUnlocked || level.levelIndex <= _maxUnlockedLevel; + // ๐Ÿ”ฝ [์ˆ˜์ •] ๋žญํ‚น ํŠœํ”Œ(์ˆซ์ž ์Œ)์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + final (int oldRank, int currentRank) = _rankHistory[level.levelIndex] ?? (0, 0); + + // 1. ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (์ž ๊ธˆ ํ•ด์ œ ์‹œ ํ”Œ๋ ˆ์ด ์•„์ด์ฝ˜, ์ž ๊ฒผ์œผ๋ฉด null) + Widget? trailingWidget = isUnlocked ? const Icon(Icons.play_arrow_rounded) : null; + String? subtitleText; + Color? subtitleColor; + + // ๐Ÿ”ฝ [์ˆ˜์ •] ๋žญํ‚น ์ˆซ์ž๋ฅผ ์ง์ ‘ ๋น„๊ตํ•˜์—ฌ UI ํ…์ŠคํŠธ์™€ ์•„์ด์ฝ˜์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + if (currentRank > 0) { + // 1. ํ˜„์žฌ ๋žญํ‚น์ด ์žˆ๋Š” ๊ฒฝ์šฐ (1์œ„, 5์œ„ ๋“ฑ) + String rankStr = "${currentRank}์œ„"; // ์˜ˆ: "5์œ„" + + if (oldRank > 0) { + // 1a. ์ด์ „ ๋žญํ‚น๋„ ์žˆ๋Š” ๊ฒฝ์šฐ (๋ณ€๋™ ๋น„๊ต) + int change = oldRank - currentRank; // (7 -> 5) = +2 (์ƒ์Šน) | (5 -> 7) = -2 (ํ•˜๋ฝ) + + if (change > 0) { // ์ˆœ์œ„ ์ƒ์Šน + subtitleText = "$rankStr (โ–ฒ $change)"; + subtitleColor = Colors.green; + trailingWidget = const Icon(Icons.arrow_circle_up_rounded, color: Colors.green, size: 28); + } else if (change < 0) { // ์ˆœ์œ„ ํ•˜๋ฝ + subtitleText = "$rankStr (โ–ผ ${change.abs()})"; + subtitleColor = Colors.red; + trailingWidget = const Icon(Icons.arrow_circle_down_rounded, color: Colors.red, size: 28); + } else { // ์ˆœ์œ„ ์œ ์ง€ + subtitleText = "$rankStr (์œ ์ง€)"; + subtitleColor = Colors.grey; + trailingWidget = const Icon(Icons.check_circle_outline_rounded, color: Colors.grey, size: 28); + } + } else { + // 1b. ์‹ ๊ทœ ๋žญํ‚น ์ง„์ž… (currentRank > 0, oldRank == 0) + subtitleText = "$rankStr (์‹ ๊ทœ ์ง„์ž…)"; + subtitleColor = Colors.blue; + trailingWidget = const Icon(Icons.new_releases_rounded, color: Colors.blue, size: 28); + } + + } else { + // 2. ํ˜„์žฌ ๋žญํ‚น์ด ์—†๋Š” ๊ฒฝ์šฐ (currentRank == 0) + if (oldRank > 0) { + // 2a. ๋žญํ‚น์—์„œ ์ดํƒˆ (currentRank == 0, oldRank > 0) + subtitleText = "๋žญํ‚น ์ดํƒˆ (์ด์ „ ${oldRank}์œ„)"; + subtitleColor = Colors.orange; + trailingWidget = const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28); + } else { + // 2b. ๋žญํ‚น ๊ธฐ๋ก ์—†์Œ (currentRank == 0, oldRank == 0) + // subtitleText๋Š” null, trailingWidget์€ ๊ธฐ๋ณธ๊ฐ’(ํ”Œ๋ ˆ์ด ์•„์ด์ฝ˜) ์œ ์ง€ + } + } + // ๐Ÿ”ผ [์ˆ˜์ •] ์™„๋ฃŒ + return Card( margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), child: ListTile( @@ -150,31 +266,64 @@ class _HomeScreenState extends State { fontWeight: isUnlocked ? FontWeight.bold : FontWeight.normal, color: isUnlocked ? Colors.black : Colors.grey, )), - trailing: isUnlocked ? const Icon(Icons.play_arrow_rounded) : null, + // ๐Ÿ”ฝ ์„œ๋ธŒํƒ€์ดํ‹€ ํ‘œ์‹œ (null์ด ์•„๋‹ ๋•Œ๋งŒ) + subtitle: subtitleText != null + ? Text(subtitleText, style: TextStyle(color: subtitleColor, fontWeight: FontWeight.bold)) + : null, + // ๐Ÿ”ฝ ์ตœ์ข… ๊ฒฐ์ •๋œ trailingWidget ํ‘œ์‹œ + trailing: trailingWidget, onTap: isUnlocked && !_isLoading ? () => _startGame(level) - : null, // ์ž ๊ฒผ๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ํƒญ ๋น„ํ™œ์„ฑํ™” + : null, ), ); }, ), ), - // 3. ๋žญํ‚น ๋ณด๊ธฐ ๋ฒ„ํŠผ - TextButton( - onPressed: () { - // ํ˜„์žฌ ์ตœ๊ณ  ๋ ˆ๋ฒจ์„ ๋žญํ‚น ํ™”๋ฉด์˜ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ „๋‹ฌ - final String currentDifficultyName = AppLevels.getLevel(_maxUnlockedLevel).name; - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RankingScreen( - initialDifficultyName: currentDifficultyName, + // 3. ๋žญํ‚น ๋ณด๊ธฐ ๋ฒ„ํŠผ (๋ณ€๊ฒฝ ์—†์Œ - ์ด์ „ ์Šคํƒ€์ผ ์œ ์ง€) + Container( + margin: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 8.0), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: InkWell( + onTap: () { + final String currentDifficultyName = AppLevels.getLevel(_maxUnlockedLevel).name; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RankingScreen( + initialDifficultyName: currentDifficultyName, + ), + ), + ); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14.0), + child: const Text( + '๐Ÿ† ์ „์ฒด ๋žญํ‚น ๋ณด๊ธฐ', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), ), - ); - }, - child: const Text('์ „์ฒด ๋žญํ‚น ๋ณด๊ธฐ'), + ), + ), ), ], ), @@ -182,7 +331,7 @@ class _HomeScreenState extends State { ); }, ), - bottomNavigationBar: const AdBannerWidget(), // ๐Ÿ‘ˆ [์ˆ˜์ •] body -> bottomNavigationBar + bottomNavigationBar: const AdBannerWidget(), ); } } \ No newline at end of file diff --git a/lib/services/identity_service.dart b/lib/services/identity_service.dart index e97f732..b343f10 100644 --- a/lib/services/identity_service.dart +++ b/lib/services/identity_service.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; @@ -5,7 +6,10 @@ import 'package:uuid/uuid.dart'; class IdentityService { static const String _userIdKey = 'app_user_id'; static const String _userNameKey = 'app_user_name'; - static const String _maxLevelKey = 'max_unlocked_level'; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] + static const String _maxLevelKey = 'max_unlocked_level'; + + // ๐Ÿ”ฝ [์ˆ˜์ •] ๋žญํ‚น ์ •๋ณด๋ฅผ Map ํ˜•ํƒœ๋กœ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ Key + static const String _lastRankMapKey = 'last_checked_rank_map'; // 1. ์•ฑ-๊ณ ์œ  ID ๊ฐ€์ ธ์˜ค๊ธฐ (์—†์œผ๋ฉด ์ƒ์„ฑ) Future getOrCreateUserId() async { @@ -13,7 +17,6 @@ class IdentityService { String? userId = prefs.getString(_userIdKey); if (userId == null) { - // ID๊ฐ€ ์—†์œผ๋ฉด V4 UUID ์ƒ์„ฑ userId = const Uuid().v4(); await prefs.setString(_userIdKey, userId); } @@ -32,16 +35,46 @@ class IdentityService { await prefs.setString(_userNameKey, name); } - // 4. ๐Ÿ”ฝ [์ถ”๊ฐ€] ํ˜„์žฌ ์ž ๊ธˆ ํ•ด์ œ๋œ ์ตœ๊ณ  ๋ ˆ๋ฒจ ๊ฐ€์ ธ์˜ค๊ธฐ + // 4. ํ˜„์žฌ ์ž ๊ธˆ ํ•ด์ œ๋œ ์ตœ๊ณ  ๋ ˆ๋ฒจ ๊ฐ€์ ธ์˜ค๊ธฐ Future getMaxUnlockedLevel() async { final prefs = await SharedPreferences.getInstance(); - // ์ตœ์ดˆ ์‹คํ–‰ ์‹œ 1 (L1) ๋ฐ˜ํ™˜, 9๋ ˆ๋ฒจ ํด๋ฆฌ์–ด ์‹œ 99 ๋ฐ˜ํ™˜ return prefs.getInt(_maxLevelKey) ?? 1; } - // 5. ๐Ÿ”ฝ [์ถ”๊ฐ€] ์ƒˆ ๋ ˆ๋ฒจ ์ž ๊ธˆ ํ•ด์ œ + // 5. ์ƒˆ ๋ ˆ๋ฒจ ์ž ๊ธˆ ํ•ด์ œ Future saveMaxUnlockedLevel(int level) async { final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_maxLevelKey, level); } + + // 6. ๐Ÿ”ฝ [์ˆ˜์ •] ๋ชจ๋“  ๋ ˆ๋ฒจ์˜ ๋žญํ‚น ๋งต(Map) ๊ฐ€์ ธ์˜ค๊ธฐ + Future> getLastSavedRankMap() async { + final prefs = await SharedPreferences.getInstance(); + String? jsonString = prefs.getString(_lastRankMapKey); + + if (jsonString == null) { + return {}; // ์ €์žฅ๋œ ๋งต์ด ์—†์œผ๋ฉด ๋นˆ ๋งต ๋ฐ˜ํ™˜ + } + + try { + // JSON์€ Map์ด๋ฏ€๋กœ, ํ‚ค๋ฅผ int๋กœ ๋ณ€ํ™˜ + final Map decodedMap = jsonDecode(jsonString); + return decodedMap.map((key, value) => MapEntry(int.parse(key), value as int)); + } catch (e) { + // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋นˆ ๋งต ๋ฐ˜ํ™˜ + return {}; + } + } + + // 7. ๐Ÿ”ฝ [์ˆ˜์ •] ๋ชจ๋“  ๋ ˆ๋ฒจ์˜ ๋žญํ‚น ๋งต(Map) ์ €์žฅํ•˜๊ธฐ + Future saveLastRankMap(Map rankMap) async { + final prefs = await SharedPreferences.getInstance(); + + // JSON ์ €์žฅ์„ ์œ„ํ•ด ํ‚ค๋ฅผ String์œผ๋กœ ๋ณ€ํ™˜ + final Map stringKeyMap = + rankMap.map((key, value) => MapEntry(key.toString(), value)); + + String jsonString = jsonEncode(stringKeyMap); + await prefs.setString(_lastRankMapKey, jsonString); + } } \ No newline at end of file diff --git a/lib/services/puzzle_service.dart b/lib/services/puzzle_service.dart index 36a3542..dad6484 100644 --- a/lib/services/puzzle_service.dart +++ b/lib/services/puzzle_service.dart @@ -44,7 +44,7 @@ class PuzzleService { } // ๋žญํ‚น ๋“ฑ๋ก - Future submitRank(UnifiedRankDto rankDto) async { + Future> submitRank(UnifiedRankDto rankDto) async { final requestBody = jsonEncode(rankDto.toJson()); log(">>> ๋žญํ‚น ๋“ฑ๋ก ์š”์ฒญ: $requestBody"); @@ -55,7 +55,19 @@ class PuzzleService { body: requestBody, ); - if (response.statusCode != 200) { + // ๐Ÿ”ฝ [์ˆ˜์ •] ์„ฑ๊ณต(200) ์‹œ, ์„œ๋ฒ„๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ๋žญํ‚น ๋ฆฌ์ŠคํŠธ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ + if (response.statusCode == 200) { + log("<<< ๋žญํ‚น ๋“ฑ๋ก ์„ฑ๊ณต: 200 OK (๋žญํ‚น ๋ชฉ๋ก ๋ฐ˜ํ™˜๋จ)"); + try { + final List data = jsonDecode(utf8.decode(response.bodyBytes)); + return data.map((json) => GameRankDto.fromJson(json)).toList(); + } catch (e) { + log("<<< ๋žญํ‚น ๋“ฑ๋ก ์„ฑ๊ณตํ–ˆ์œผ๋‚˜, ๋ฐ˜ํ™˜๋œ ๋žญํ‚น ๋ชฉ๋ก ํŒŒ์‹ฑ ์‹คํŒจ: $e"); + throw Exception('๋žญํ‚น ๋ชฉ๋ก ํŒŒ์‹ฑ ์‹คํŒจ: $e'); + } + } + // ๐Ÿ”ฝ [์ˆ˜์ •] ์‹คํŒจ ์‹œ, ๊ธฐ์กด ๋กœ์ง๊ณผ ๋™์ผํ•˜๊ฒŒ ์—๋Ÿฌ ์ฒ˜๋ฆฌ + else { log("<<< ๋žญํ‚น ๋“ฑ๋ก ์‹คํŒจ: ${response.statusCode}"); try { final errorBody = utf8.decode(response.bodyBytes); @@ -65,7 +77,6 @@ class PuzzleService { throw Exception('๋žญํ‚น ๋“ฑ๋ก ์‹คํŒจ: ${response.reasonPhrase}'); } } - log("<<< ๋žญํ‚น ๋“ฑ๋ก ์„ฑ๊ณต: 200 OK"); } // ๋žญํ‚น ์กฐํšŒ diff --git a/lib/widgets/number_pad.dart b/lib/widgets/number_pad.dart index d03b4e3..7e1f083 100644 --- a/lib/widgets/number_pad.dart +++ b/lib/widgets/number_pad.dart @@ -7,7 +7,7 @@ class NumberPad extends StatelessWidget { final Map numberCounts; final int? selectedNumber; final Function(int) onNumberTapped; - final bool isLandscape; // ๐Ÿ‘ˆ [์ถ”๊ฐ€] ์ด ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค + final bool isLandscape; const NumberPad({ super.key, @@ -16,14 +16,13 @@ class NumberPad extends StatelessWidget { required this.numberCounts, required this.selectedNumber, required this.onNumberTapped, - required this.isLandscape, // ๐Ÿ‘ˆ [์ถ”๊ฐ€] ์ƒ์„ฑ์ž์— ์ถ”๊ฐ€ + required this.isLandscape, }); @override Widget build(BuildContext context) { final int gridSize = blockSize * blockSize; - // 1. ๋ฒ„ํŠผ ์œ„์ ฏ ๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ List numberButtons = List.generate(gridSize, (index) { int numberValue = index + 1; String numberSymbol = theme.getSymbol(numberValue); @@ -34,38 +33,40 @@ class NumberPad extends StatelessWidget { style: ElevatedButton.styleFrom( backgroundColor: isSelected ? Colors.blue.shade300 : null, foregroundColor: isSelected ? Colors.white : null, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 0), - textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + // ๐Ÿ”ฝ [์ˆ˜์ •] ํŒจ๋”ฉ์„ ์กฐ์ ˆํ•ด ๋ฒ„ํŠผ์„ ์ฑ„์›€ + padding: const EdgeInsets.all(4.0), + // ๐Ÿ”ฝ [์ˆ˜์ •] ๊ธฐ๋ณธ ํฐํŠธ ํฌ๊ธฐ๋ฅผ ํ‚ค์›€ (์ด๋ชจ์ง€ ๋“ฑ) + textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)) ), onPressed: isCompleted ? null : () => onNumberTapped(numberValue), - child: Text(numberSymbol), + // ๐Ÿ”ฝ [์ˆ˜์ •] Text๋ฅผ FittedBox๋กœ ๊ฐ์‹ธ ๊ธฐํ˜ธ๊ฐ€ ๋ฒ„ํŠผ์— ๊ฝ‰ ์ฐจ๊ฒŒ ํ•จ + child: FittedBox( + fit: BoxFit.contain, + child: Text(numberSymbol), + ), ); - // ๊ฐ€๋กœ ๋ชจ๋“œ(Wrap)์—์„œ๋Š” Flexible๋กœ ๊ฐ์‹ธ๊ณ , - // ์„ธ๋กœ ๋ชจ๋“œ(Grid)์—์„œ๋Š” ๊ฐ์‹ธ์ง€ ์•Š์Œ if (isLandscape) { - // Flexible์„ ์‚ฌ์šฉํ•ด Wrap ๋‚ด์—์„œ ๋ฒ„ํŠผ์ด ๊ณต๊ฐ„์„ ์ฐจ์ง€ํ•˜๋„๋ก ํ•จ return Flexible(child: button); } else { return button; } }); - // 2. ๊ฐ€๋กœ/์„ธ๋กœ ๋ชจ๋“œ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋ ˆ์ด์•„์›ƒ ๋ฐ˜ํ™˜ if (isLandscape) { // --- ๊ฐ€๋กœ ๋ชจ๋“œ: Wrap ์‚ฌ์šฉ (๋ฒ„ํŠผ์ด ๊ฐ€๋กœ๋กœ ํ๋ฆ„) --- return Wrap( - runSpacing: 4.0, // ์ค„(์„ธ๋กœ) ๊ฐ„๊ฒฉ - spacing: 4.0, // ๋ฒ„ํŠผ(๊ฐ€๋กœ) ๊ฐ„๊ฒฉ + runSpacing: 4.0, + spacing: 4.0, children: numberButtons, ); } else { // --- ์„ธ๋กœ ๋ชจ๋“œ: GridView ์‚ฌ์šฉ (๋ธ”๋ก ๋ชจ์–‘) --- return GridView.count( - crossAxisCount: blockSize, // 2x2, 3x3, 4x4, 5x5 + crossAxisCount: blockSize, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: EdgeInsets.zero,