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 _isAvailable = false;

View File

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

View File

@@ -12,55 +12,56 @@ class SettingsRepository {
SharedPreferences? _prefs;
/// SharedPreferences 초기화
Future<void> init() async {
Future<SharedPreferences> _getPrefs() async {
_prefs ??= await SharedPreferences.getInstance();
return _prefs!;
}
/// 언어 설정 저장
Future<void> saveLocale(String locale) async {
await init();
await _prefs!.setString(_keyLocale, locale);
final prefs = await _getPrefs();
await prefs.setString(_keyLocale, locale);
}
/// 언어 설정 불러오기
Future<String?> loadLocale() async {
await init();
return _prefs!.getString(_keyLocale);
final prefs = await _getPrefs();
return prefs.getString(_keyLocale);
}
/// BGM 볼륨 저장 (0.0 ~ 1.0)
Future<void> 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<double> 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<void> 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<double> 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<void> 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<double> loadAnimationSpeed() async {
await init();
return _prefs!.getDouble(_keyAnimationSpeed) ?? 1.0;
final prefs = await _getPrefs();
return prefs.getDouble(_keyAnimationSpeed) ?? 1.0;
}
}

View File

@@ -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(