fix: 출시 전 검수 이슈 4건 수정

- save_data: JSON 캐스팅 시 null 안전 처리 (손상된 세이브 크래시 방지)
- settings_repository: _prefs! 강제 언래핑 제거, _getPrefs() 패턴 적용
- game_session_controller: IAP 구매 상태를 MonetizationState에 동기화
- iap_service: InAppPurchase.instance를 lazy 초기화로 변경
This commit is contained in:
JiWoong Sul
2026-03-24 17:40:39 +09:00
parent c54681df8c
commit 863c52600f
4 changed files with 50 additions and 24 deletions

View File

@@ -68,7 +68,7 @@ class IAPService {
// 상태 // 상태
// =========================================================================== // ===========================================================================
final InAppPurchase _iap = InAppPurchase.instance; late final InAppPurchase _iap = InAppPurchase.instance;
bool _isInitialized = false; bool _isInitialized = false;
bool _isAvailable = false; bool _isAvailable = false;

View File

@@ -148,11 +148,16 @@ class GameSave {
} }
static GameSave fromJson(Map<String, dynamic> json) { static GameSave fromJson(Map<String, dynamic> json) {
final traitsJson = json['traits'] as Map<String, dynamic>; final traitsJson =
final statsJson = json['stats'] as Map<String, dynamic>; json['traits'] as Map<String, dynamic>? ?? <String, dynamic>{};
final inventoryJson = json['inventory'] as Map<String, dynamic>; final statsJson =
final equipmentJson = json['equipment'] as Map<String, dynamic>; json['stats'] as Map<String, dynamic>? ?? <String, dynamic>{};
final progressJson = json['progress'] as Map<String, dynamic>; final inventoryJson =
json['inventory'] as Map<String, dynamic>? ?? <String, dynamic>{};
final equipmentJson =
json['equipment'] as Map<String, dynamic>? ?? <String, dynamic>{};
final progressJson =
json['progress'] as Map<String, dynamic>? ?? <String, dynamic>{};
final queueJson = (json['queue'] as List<dynamic>? ?? []).cast<dynamic>(); final queueJson = (json['queue'] as List<dynamic>? ?? []).cast<dynamic>();
final skillsJson = (json['skills'] as List<dynamic>? ?? []).cast<dynamic>(); final skillsJson = (json['skills'] as List<dynamic>? ?? []).cast<dynamic>();

View File

@@ -12,55 +12,56 @@ class SettingsRepository {
SharedPreferences? _prefs; SharedPreferences? _prefs;
/// SharedPreferences 초기화 /// SharedPreferences 초기화
Future<void> init() async { Future<SharedPreferences> _getPrefs() async {
_prefs ??= await SharedPreferences.getInstance(); _prefs ??= await SharedPreferences.getInstance();
return _prefs!;
} }
/// 언어 설정 저장 /// 언어 설정 저장
Future<void> saveLocale(String locale) async { Future<void> saveLocale(String locale) async {
await init(); final prefs = await _getPrefs();
await _prefs!.setString(_keyLocale, locale); await prefs.setString(_keyLocale, locale);
} }
/// 언어 설정 불러오기 /// 언어 설정 불러오기
Future<String?> loadLocale() async { Future<String?> loadLocale() async {
await init(); final prefs = await _getPrefs();
return _prefs!.getString(_keyLocale); return prefs.getString(_keyLocale);
} }
/// BGM 볼륨 저장 (0.0 ~ 1.0) /// BGM 볼륨 저장 (0.0 ~ 1.0)
Future<void> saveBgmVolume(double volume) async { Future<void> saveBgmVolume(double volume) async {
await init(); final prefs = await _getPrefs();
await _prefs!.setDouble(_keyBgmVolume, volume.clamp(0.0, 1.0)); await prefs.setDouble(_keyBgmVolume, volume.clamp(0.0, 1.0));
} }
/// BGM 볼륨 불러오기 (기본값: 0.7) /// BGM 볼륨 불러오기 (기본값: 0.7)
Future<double> loadBgmVolume() async { Future<double> loadBgmVolume() async {
await init(); final prefs = await _getPrefs();
return _prefs!.getDouble(_keyBgmVolume) ?? 0.7; return prefs.getDouble(_keyBgmVolume) ?? 0.7;
} }
/// SFX 볼륨 저장 (0.0 ~ 1.0) /// SFX 볼륨 저장 (0.0 ~ 1.0)
Future<void> saveSfxVolume(double volume) async { Future<void> saveSfxVolume(double volume) async {
await init(); final prefs = await _getPrefs();
await _prefs!.setDouble(_keySfxVolume, volume.clamp(0.0, 1.0)); await prefs.setDouble(_keySfxVolume, volume.clamp(0.0, 1.0));
} }
/// SFX 볼륨 불러오기 (기본값: 0.8) /// SFX 볼륨 불러오기 (기본값: 0.8)
Future<double> loadSfxVolume() async { Future<double> loadSfxVolume() async {
await init(); final prefs = await _getPrefs();
return _prefs!.getDouble(_keySfxVolume) ?? 0.8; return prefs.getDouble(_keySfxVolume) ?? 0.8;
} }
/// 애니메이션 속도 저장 (0.5 ~ 2.0, 1.0이 기본) /// 애니메이션 속도 저장 (0.5 ~ 2.0, 1.0이 기본)
Future<void> saveAnimationSpeed(double speed) async { Future<void> saveAnimationSpeed(double speed) async {
await init(); final prefs = await _getPrefs();
await _prefs!.setDouble(_keyAnimationSpeed, speed.clamp(0.5, 2.0)); await prefs.setDouble(_keyAnimationSpeed, speed.clamp(0.5, 2.0));
} }
/// 애니메이션 속도 불러오기 (기본값: 1.0) /// 애니메이션 속도 불러오기 (기본값: 1.0)
Future<double> loadAnimationSpeed() async { Future<double> loadAnimationSpeed() async {
await init(); final prefs = await _getPrefs();
return _prefs!.getDouble(_keyAnimationSpeed) ?? 1.0; return prefs.getDouble(_keyAnimationSpeed) ?? 1.0;
} }
} }

View File

@@ -1,5 +1,6 @@
import 'dart:async'; 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_loop.dart';
import 'package:asciineverdie/src/core/engine/progress_service.dart'; import 'package:asciineverdie/src/core/engine/progress_service.dart';
import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/core/model/game_state.dart';
@@ -173,6 +174,17 @@ class GameSessionController extends ChangeNotifier {
_status = GameSessionStatus.running; _status = GameSessionStatus.running;
_cheatsEnabled = cheatsEnabled; _cheatsEnabled = cheatsEnabled;
// IAP 구매 상태를 MonetizationState에 동기화
// (테스트 환경에서는 InAppPurchase 플랫폼 채널 미등록으로 예외 발생 가능)
try {
final isPaid = IAPService.instance.isAdRemovalPurchased;
if (_monetization.adRemovalPurchased != isPaid) {
_monetization = _monetization.copyWith(adRemovalPurchased: isPaid);
}
} catch (_) {
// 비모바일 플랫폼 또는 테스트 환경에서는 무시
}
// 통계 초기화 // 통계 초기화
if (isNewGame) { if (isNewGame) {
await _statisticsManager.initializeForNewGame(); await _statisticsManager.initializeForNewGame();
@@ -273,8 +285,16 @@ class GameSessionController extends ChangeNotifier {
return; return;
} }
// 저장된 수익화 상태 복원 // 저장된 수익화(monetization) 상태 복원, IAP 구매 상태 동기화
_monetization = savedMonetization ?? MonetizationState.initial(); _monetization = savedMonetization ?? MonetizationState.initial();
try {
final isPaid = IAPService.instance.isAdRemovalPurchased;
if (_monetization.adRemovalPurchased != isPaid) {
_monetization = _monetization.copyWith(adRemovalPurchased: isPaid);
}
} catch (_) {
// 비모바일 플랫폼 또는 테스트 환경에서는 무시
}
// 복귀 보상 체크 (Phase 7) // 복귀 보상 체크 (Phase 7)
_returnRewardsManager.checkReturnRewards( _returnRewardsManager.checkReturnRewards(