import 'dart:developer'; import 'package:flutter/material.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'; import 'package:sudoku_app/screens/ranking_screen.dart'; 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}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { int _maxUnlockedLevel = 1; // πŸ”½ [μˆ˜μ •] λž­ν‚Ή 변동 μƒνƒœ λŒ€μ‹  (이전 λž­ν‚Ή, ν˜„μž¬ λž­ν‚Ή) νŠœν”Œμ„ μ €μž₯ν•©λ‹ˆλ‹€. // (Key: levelIndex, Value: (oldRank, currentRank)) // (0은 λž­ν‚Ήμ— μ—†μŒμ„ 의미) Map _rankHistory = {}; String? _userName; late String _selectedThemeName; bool _isLoading = false; final PuzzleService _puzzleService = PuzzleService(); final IdentityService _identityService = IdentityService(); @override void initState() { super.initState(); _selectedThemeName = AppThemes.random; _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 { final String difficulty = level.levelIndex.toString(); final SudokuGameDto gameData = await _puzzleService.startGame(difficulty); final String userId = await _identityService.getOrCreateUserId(); final String? userName = _userName; if (mounted) { await Navigator.push( context, MaterialPageRoute( builder: (context) => GameScreen( gameData: gameData, themeName: _selectedThemeName, userId: userId, userName: userName, levelIndex: level.levelIndex, ), ), ); _loadProgress(); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('κ²Œμž„ λ‘œλ”© μ‹€νŒ¨: $e')), ); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } // πŸ”½ [μˆ˜μ •] build λ©”μ†Œλ“œμ—μ„œ λž­ν‚Ή 숫자(νŠœν”Œ)λ₯Ό 직접 λΉ„κ΅ν•˜μ—¬ UI 생성 @override Widget build(BuildContext context) { final bool allLevelsUnlocked = _maxUnlockedLevel >= 99; return Scaffold( appBar: AppBar(title: const Text('μŠ€λ„μΏ  κ²Œμž„')), body: LayoutBuilder( builder: (context, constraints) { const double maxContentRatio = 0.6; final double constrainedWidth = (constraints.maxHeight * maxContentRatio) > 500 ? 500 : (constraints.maxHeight * maxContentRatio); return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: constrainedWidth), child: Column( children: [ // 1. ν…Œλ§ˆ 선택 (λ³€κ²½ μ—†μŒ) Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("ν…Œλ§ˆ: ", style: TextStyle(fontSize: 18)), DropdownButton( value: _selectedThemeName, items: AppThemes.selectableThemeNames.map((themeName) { return DropdownMenuItem( value: themeName, child: Text(themeName, style: const TextStyle(fontSize: 20)), ); }).toList(), onChanged: (themeName) { if (themeName != null) { setState(() { _selectedThemeName = themeName; }); } }, ), ], ), ), // 2. 레벨 선택 리슀트 Expanded( child: ListView.builder( itemCount: AppLevels.allLevels.length, itemBuilder: (context, index) { final GameLevel level = AppLevels.allLevels[index]; 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( leading: Icon( isUnlocked ? Icons.lock_open_rounded : Icons.lock_rounded, color: isUnlocked ? Colors.blue : Colors.grey, ), title: Text(level.name, style: TextStyle( fontSize: 18, fontWeight: isUnlocked ? FontWeight.bold : FontWeight.normal, color: isUnlocked ? Colors.black : Colors.grey, )), // πŸ”½ μ„œλΈŒνƒ€μ΄ν‹€ ν‘œμ‹œ (null이 아닐 λ•Œλ§Œ) subtitle: subtitleText != null ? Text(subtitleText, style: TextStyle(color: subtitleColor, fontWeight: FontWeight.bold)) : null, // πŸ”½ μ΅œμ’… κ²°μ •λœ trailingWidget ν‘œμ‹œ trailing: trailingWidget, onTap: isUnlocked && !_isLoading ? () => _startGame(level) : null, ), ); }, ), ), // 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, ), ), ), ), ), ), ], ), ), ); }, ), bottomNavigationBar: const AdBannerWidget(), ); } }