From 863c52600f9f2fe212e6ed587a1c486041c0cedb Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Tue, 24 Mar 2026 17:40:39 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=B6=9C=EC=8B=9C=20=EC=A0=84=20?= =?UTF-8?q?=EA=B2=80=EC=88=98=20=EC=9D=B4=EC=8A=88=204=EA=B1=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - save_data: JSON 캐스팅 시 null 안전 처리 (손상된 세이브 크래시 방지) - settings_repository: _prefs! 강제 언래핑 제거, _getPrefs() 패턴 적용 - game_session_controller: IAP 구매 상태를 MonetizationState에 동기화 - iap_service: InAppPurchase.instance를 lazy 초기화로 변경 --- lib/src/core/engine/iap_service.dart | 2 +- lib/src/core/model/save_data.dart | 15 +++++--- lib/src/core/storage/settings_repository.dart | 35 ++++++++++--------- .../game/game_session_controller.dart | 22 +++++++++++- 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/lib/src/core/engine/iap_service.dart b/lib/src/core/engine/iap_service.dart index 651a4f8..19bfbea 100644 --- a/lib/src/core/engine/iap_service.dart +++ b/lib/src/core/engine/iap_service.dart @@ -68,7 +68,7 @@ class IAPService { // 상태 // =========================================================================== - final InAppPurchase _iap = InAppPurchase.instance; + late final InAppPurchase _iap = InAppPurchase.instance; bool _isInitialized = false; bool _isAvailable = false; diff --git a/lib/src/core/model/save_data.dart b/lib/src/core/model/save_data.dart index bf18fa3..d0d2177 100644 --- a/lib/src/core/model/save_data.dart +++ b/lib/src/core/model/save_data.dart @@ -148,11 +148,16 @@ class GameSave { } static GameSave fromJson(Map json) { - final traitsJson = json['traits'] as Map; - final statsJson = json['stats'] as Map; - final inventoryJson = json['inventory'] as Map; - final equipmentJson = json['equipment'] as Map; - final progressJson = json['progress'] as Map; + final traitsJson = + json['traits'] as Map? ?? {}; + final statsJson = + json['stats'] as Map? ?? {}; + final inventoryJson = + json['inventory'] as Map? ?? {}; + final equipmentJson = + json['equipment'] as Map? ?? {}; + final progressJson = + json['progress'] as Map? ?? {}; final queueJson = (json['queue'] as List? ?? []).cast(); final skillsJson = (json['skills'] as List? ?? []).cast(); diff --git a/lib/src/core/storage/settings_repository.dart b/lib/src/core/storage/settings_repository.dart index d36549b..fdbf3ed 100644 --- a/lib/src/core/storage/settings_repository.dart +++ b/lib/src/core/storage/settings_repository.dart @@ -12,55 +12,56 @@ class SettingsRepository { SharedPreferences? _prefs; /// SharedPreferences 초기화 - Future init() async { + Future _getPrefs() async { _prefs ??= await SharedPreferences.getInstance(); + return _prefs!; } /// 언어 설정 저장 Future saveLocale(String locale) async { - await init(); - await _prefs!.setString(_keyLocale, locale); + final prefs = await _getPrefs(); + await prefs.setString(_keyLocale, locale); } /// 언어 설정 불러오기 Future loadLocale() async { - await init(); - return _prefs!.getString(_keyLocale); + final prefs = await _getPrefs(); + return prefs.getString(_keyLocale); } /// BGM 볼륨 저장 (0.0 ~ 1.0) Future saveBgmVolume(double volume) async { - await init(); - await _prefs!.setDouble(_keyBgmVolume, volume.clamp(0.0, 1.0)); + final prefs = await _getPrefs(); + await prefs.setDouble(_keyBgmVolume, volume.clamp(0.0, 1.0)); } /// BGM 볼륨 불러오기 (기본값: 0.7) Future loadBgmVolume() async { - await init(); - return _prefs!.getDouble(_keyBgmVolume) ?? 0.7; + final prefs = await _getPrefs(); + return prefs.getDouble(_keyBgmVolume) ?? 0.7; } /// SFX 볼륨 저장 (0.0 ~ 1.0) Future saveSfxVolume(double volume) async { - await init(); - await _prefs!.setDouble(_keySfxVolume, volume.clamp(0.0, 1.0)); + final prefs = await _getPrefs(); + await prefs.setDouble(_keySfxVolume, volume.clamp(0.0, 1.0)); } /// SFX 볼륨 불러오기 (기본값: 0.8) Future loadSfxVolume() async { - await init(); - return _prefs!.getDouble(_keySfxVolume) ?? 0.8; + final prefs = await _getPrefs(); + return prefs.getDouble(_keySfxVolume) ?? 0.8; } /// 애니메이션 속도 저장 (0.5 ~ 2.0, 1.0이 기본) Future saveAnimationSpeed(double speed) async { - await init(); - await _prefs!.setDouble(_keyAnimationSpeed, speed.clamp(0.5, 2.0)); + final prefs = await _getPrefs(); + await prefs.setDouble(_keyAnimationSpeed, speed.clamp(0.5, 2.0)); } /// 애니메이션 속도 불러오기 (기본값: 1.0) Future loadAnimationSpeed() async { - await init(); - return _prefs!.getDouble(_keyAnimationSpeed) ?? 1.0; + final prefs = await _getPrefs(); + return prefs.getDouble(_keyAnimationSpeed) ?? 1.0; } } diff --git a/lib/src/features/game/game_session_controller.dart b/lib/src/features/game/game_session_controller.dart index ec6aa05..3d0063c 100644 --- a/lib/src/features/game/game_session_controller.dart +++ b/lib/src/features/game/game_session_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:asciineverdie/src/core/engine/iap_service.dart'; import 'package:asciineverdie/src/core/engine/progress_loop.dart'; import 'package:asciineverdie/src/core/engine/progress_service.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; @@ -173,6 +174,17 @@ class GameSessionController extends ChangeNotifier { _status = GameSessionStatus.running; _cheatsEnabled = cheatsEnabled; + // IAP 구매 상태를 MonetizationState에 동기화 + // (테스트 환경에서는 InAppPurchase 플랫폼 채널 미등록으로 예외 발생 가능) + try { + final isPaid = IAPService.instance.isAdRemovalPurchased; + if (_monetization.adRemovalPurchased != isPaid) { + _monetization = _monetization.copyWith(adRemovalPurchased: isPaid); + } + } catch (_) { + // 비모바일 플랫폼 또는 테스트 환경에서는 무시 + } + // 통계 초기화 if (isNewGame) { await _statisticsManager.initializeForNewGame(); @@ -273,8 +285,16 @@ class GameSessionController extends ChangeNotifier { return; } - // 저장된 수익화 상태 복원 + // 저장된 수익화(monetization) 상태 복원, IAP 구매 상태 동기화 _monetization = savedMonetization ?? MonetizationState.initial(); + try { + final isPaid = IAPService.instance.isAdRemovalPurchased; + if (_monetization.adRemovalPurchased != isPaid) { + _monetization = _monetization.copyWith(adRemovalPurchased: isPaid); + } + } catch (_) { + // 비모바일 플랫폼 또는 테스트 환경에서는 무시 + } // 복귀 보상 체크 (Phase 7) _returnRewardsManager.checkReturnRewards(