feat(arch): GetIt DI + IIAPService/IAdService 인터페이스 도입
- core/di/: service_locator, IIAPService, IAdService 생성 - IAPService/AdService: implements 인터페이스 + GetIt 위임 - main.dart: setupServiceLocator() 호출 - 기존 .instance getter 호환성 100% 유지 - test/helpers/test_setup.dart: 테스트용 서비스 로케이터 초기화
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import 'package:asciineverdie/src/app.dart';
|
||||
import 'package:asciineverdie/src/core/di/service_locator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
// 서비스 로케이터(service locator) 초기화
|
||||
setupServiceLocator();
|
||||
runApp(const AskiiNeverDieApp());
|
||||
}
|
||||
|
||||
31
lib/src/core/di/i_ad_service.dart
Normal file
31
lib/src/core/di/i_ad_service.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:asciineverdie/src/core/engine/ad_service.dart';
|
||||
|
||||
/// 광고 서비스 인터페이스 (interface)
|
||||
///
|
||||
/// 광고 표시 기능의 추상화 계층.
|
||||
/// 테스트 시 목(mock) 구현체로 교체 가능.
|
||||
abstract class IAdService {
|
||||
/// AdMob SDK 초기화
|
||||
Future<void> initialize();
|
||||
|
||||
/// 리워드 광고(rewarded ad) 준비 여부
|
||||
bool get isRewardedAdReady;
|
||||
|
||||
/// 인터스티셜 광고(interstitial ad) 준비 여부
|
||||
bool get isInterstitialAdReady;
|
||||
|
||||
/// 리워드 광고 표시
|
||||
Future<AdResult> showRewardedAd({
|
||||
required AdType adType,
|
||||
required void Function() onRewarded,
|
||||
});
|
||||
|
||||
/// 인터스티셜 광고 표시
|
||||
Future<AdResult> showInterstitialAd({
|
||||
required AdType adType,
|
||||
required void Function() onComplete,
|
||||
});
|
||||
|
||||
/// 리소스 해제
|
||||
void dispose();
|
||||
}
|
||||
34
lib/src/core/di/i_iap_service.dart
Normal file
34
lib/src/core/di/i_iap_service.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
||||
|
||||
/// IAP 서비스 인터페이스 (interface)
|
||||
///
|
||||
/// 인앱 구매 기능의 추상화 계층.
|
||||
/// 테스트 시 목(mock) 구현체로 교체 가능.
|
||||
abstract class IIAPService {
|
||||
/// IAP 서비스 초기화
|
||||
Future<void> initialize();
|
||||
|
||||
/// 광고 제거 구매 여부
|
||||
bool get isAdRemovalPurchased;
|
||||
|
||||
/// 스토어 가용성(store availability)
|
||||
bool get isStoreAvailable;
|
||||
|
||||
/// 광고 제거 상품 가격 문자열
|
||||
String get removeAdsPrice;
|
||||
|
||||
/// 광고 제거 구매
|
||||
Future<IAPResult> purchaseRemoveAds();
|
||||
|
||||
/// 구매 복원(restore purchases)
|
||||
Future<IAPResult> restorePurchases();
|
||||
|
||||
/// 디버그 모드 IAP 시뮬레이션(debug simulation) 활성화 여부
|
||||
bool get debugIAPSimulated;
|
||||
|
||||
/// 디버그 모드 IAP 시뮬레이션 토글
|
||||
set debugIAPSimulated(bool value);
|
||||
|
||||
/// 리소스 해제
|
||||
void dispose();
|
||||
}
|
||||
21
lib/src/core/di/service_locator.dart
Normal file
21
lib/src/core/di/service_locator.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import 'package:asciineverdie/src/core/di/i_ad_service.dart';
|
||||
import 'package:asciineverdie/src/core/di/i_iap_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/ad_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
||||
|
||||
/// 전역 서비스 로케이터(service locator) 인스턴스
|
||||
final GetIt sl = GetIt.instance;
|
||||
|
||||
/// 서비스 로케이터 초기화
|
||||
///
|
||||
/// 앱 시작 시 한 번 호출하여 모든 서비스를 등록합니다.
|
||||
/// 점진적 도입: IAPService, AdService만 먼저 등록.
|
||||
void setupServiceLocator() {
|
||||
// IAP 서비스 (싱글톤 등록)
|
||||
sl.registerLazySingleton<IIAPService>(() => IAPService.createInstance());
|
||||
|
||||
// 광고 서비스 (싱글톤 등록)
|
||||
sl.registerLazySingleton<IAdService>(() => AdService.createInstance());
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import 'package:asciineverdie/src/core/di/i_ad_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
||||
|
||||
/// 광고 타입
|
||||
@@ -44,16 +47,14 @@ enum AdResult {
|
||||
///
|
||||
/// AdMob 리워드/인터스티셜 광고 로드 및 표시를 관리합니다.
|
||||
/// 디버그 모드에서는 광고 ON/OFF 토글이 가능합니다.
|
||||
class AdService {
|
||||
class AdService implements IAdService {
|
||||
AdService._();
|
||||
|
||||
static AdService? _instance;
|
||||
/// GetIt 등록용 팩토리 메서드
|
||||
factory AdService.createInstance() => AdService._();
|
||||
|
||||
/// 싱글톤 인스턴스
|
||||
static AdService get instance {
|
||||
_instance ??= AdService._();
|
||||
return _instance!;
|
||||
}
|
||||
/// 싱글톤 인스턴스 (GetIt 서비스 로케이터에 위임)
|
||||
static IAdService get instance => GetIt.instance<IAdService>();
|
||||
|
||||
// ===========================================================================
|
||||
// 광고 단위 ID
|
||||
@@ -126,6 +127,7 @@ class AdService {
|
||||
// ===========================================================================
|
||||
|
||||
/// AdMob SDK 초기화
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
@@ -197,6 +199,7 @@ class AdService {
|
||||
}
|
||||
|
||||
/// 리워드 광고 준비 여부
|
||||
@override
|
||||
bool get isRewardedAdReady => _rewardedAd != null || _shouldSkipAd;
|
||||
|
||||
/// 리워드 광고 표시
|
||||
@@ -204,6 +207,7 @@ class AdService {
|
||||
/// [adType] 광고 타입 (로깅용)
|
||||
/// [onRewarded] 보상 지급 콜백
|
||||
/// Returns: 광고 결과
|
||||
@override
|
||||
Future<AdResult> showRewardedAd({
|
||||
required AdType adType,
|
||||
required void Function() onRewarded,
|
||||
@@ -308,6 +312,7 @@ class AdService {
|
||||
}
|
||||
|
||||
/// 인터스티셜 광고 준비 여부
|
||||
@override
|
||||
bool get isInterstitialAdReady => _interstitialAd != null || _shouldSkipAd;
|
||||
|
||||
/// 인터스티셜 광고 표시
|
||||
@@ -315,6 +320,7 @@ class AdService {
|
||||
/// [adType] 광고 타입 (로깅용)
|
||||
/// [onComplete] 광고 완료 콜백 (보상 지급)
|
||||
/// Returns: 광고 결과
|
||||
@override
|
||||
Future<AdResult> showInterstitialAd({
|
||||
required AdType adType,
|
||||
required void Function() onComplete,
|
||||
@@ -386,6 +392,7 @@ class AdService {
|
||||
// ===========================================================================
|
||||
|
||||
/// 리소스 해제
|
||||
@override
|
||||
void dispose() {
|
||||
_rewardedAd?.dispose();
|
||||
_rewardedAd = null;
|
||||
|
||||
@@ -12,7 +12,10 @@ import 'package:pointycastle/asn1/primitives/asn1_bit_string.dart';
|
||||
import 'package:pointycastle/asn1/primitives/asn1_integer.dart';
|
||||
import 'package:pointycastle/asn1/primitives/asn1_sequence.dart';
|
||||
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
import 'package:asciineverdie/src/core/di/i_iap_service.dart';
|
||||
|
||||
/// IAP 상품 ID
|
||||
class IAPProductIds {
|
||||
@@ -62,16 +65,14 @@ String get _googlePlayPublicKey => _gpKeyPart1 + _gpKeyPart2;
|
||||
///
|
||||
/// 인앱 구매 (광고 제거) 처리를 담당합니다.
|
||||
/// flutter_secure_storage를 사용하여 구매 상태를 보안 저장합니다.
|
||||
class IAPService {
|
||||
class IAPService implements IIAPService {
|
||||
IAPService._();
|
||||
|
||||
static IAPService? _instance;
|
||||
/// GetIt 등록용 팩토리 메서드
|
||||
factory IAPService.createInstance() => IAPService._();
|
||||
|
||||
/// 싱글톤 인스턴스
|
||||
static IAPService get instance {
|
||||
_instance ??= IAPService._();
|
||||
return _instance!;
|
||||
}
|
||||
/// 싱글톤 인스턴스 (GetIt 서비스 로케이터에 위임)
|
||||
static IIAPService get instance => GetIt.instance<IIAPService>();
|
||||
|
||||
// ===========================================================================
|
||||
// 상수
|
||||
@@ -109,6 +110,7 @@ class IAPService {
|
||||
// ===========================================================================
|
||||
|
||||
/// IAP 서비스 초기화
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
@@ -201,9 +203,11 @@ class IAPService {
|
||||
// ===========================================================================
|
||||
|
||||
/// 디버그 모드 IAP 시뮬레이션 활성화 여부
|
||||
@override
|
||||
bool get debugIAPSimulated => _debugIAPSimulated;
|
||||
|
||||
/// 디버그 모드 IAP 시뮬레이션 토글
|
||||
@override
|
||||
set debugIAPSimulated(bool value) {
|
||||
_debugIAPSimulated = value;
|
||||
if (kDebugMode) {
|
||||
@@ -217,18 +221,21 @@ class IAPService {
|
||||
// ===========================================================================
|
||||
|
||||
/// 광고 제거 구매 여부
|
||||
@override
|
||||
bool get isAdRemovalPurchased {
|
||||
if (kDebugMode && _debugIAPSimulated) return true;
|
||||
return _adRemovalPurchased;
|
||||
}
|
||||
|
||||
/// 스토어 가용성
|
||||
@override
|
||||
bool get isStoreAvailable => _isAvailable;
|
||||
|
||||
/// 광고 제거 상품 정보
|
||||
ProductDetails? get removeAdsProduct => _removeAdsProduct;
|
||||
|
||||
/// 광고 제거 상품 가격 문자열
|
||||
@override
|
||||
String get removeAdsPrice {
|
||||
if (_removeAdsProduct != null) {
|
||||
return _removeAdsProduct!.price;
|
||||
@@ -246,6 +253,7 @@ class IAPService {
|
||||
// ===========================================================================
|
||||
|
||||
/// 광고 제거 구매
|
||||
@override
|
||||
Future<IAPResult> purchaseRemoveAds() async {
|
||||
// 디버그 모드 시뮬레이션
|
||||
if (kDebugMode && _debugIAPSimulated) {
|
||||
@@ -290,6 +298,7 @@ class IAPService {
|
||||
// ===========================================================================
|
||||
|
||||
/// 구매 복원
|
||||
@override
|
||||
Future<IAPResult> restorePurchases() async {
|
||||
// 디버그 모드 시뮬레이션
|
||||
if (kDebugMode && _debugIAPSimulated) {
|
||||
@@ -489,6 +498,7 @@ class IAPService {
|
||||
// ===========================================================================
|
||||
|
||||
/// 리소스 해제
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
@@ -333,6 +333,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
get_it:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: get_it
|
||||
sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -52,6 +52,8 @@ dependencies:
|
||||
pointycastle: ^3.9.1
|
||||
# 앱 버전 정보
|
||||
package_info_plus: ^8.3.0
|
||||
# 의존성 주입(Dependency Injection) 서비스 로케이터
|
||||
get_it: ^8.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:asciineverdie/src/features/new_character/new_character_screen.da
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../helpers/test_setup.dart';
|
||||
|
||||
/// 테스트용 MaterialApp 래퍼 (localization 포함)
|
||||
/// locale을 영어로 고정하여 테스트 텍스트와 일치시킴
|
||||
Widget _buildTestApp(Widget child) {
|
||||
@@ -16,6 +18,10 @@ Widget _buildTestApp(Widget child) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
// 서비스 로케이터(service locator) 초기화
|
||||
setUpAll(() {
|
||||
TestSetup.ensureServiceLocator();
|
||||
});
|
||||
testWidgets('NewCharacterScreen renders main sections', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:asciineverdie/src/core/audio/audio_service.dart';
|
||||
import 'package:asciineverdie/src/core/di/i_iap_service.dart';
|
||||
import 'package:asciineverdie/src/core/di/service_locator.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
/// 위젯 테스트에서 사용하는 공통 셋업/정리 유틸리티
|
||||
///
|
||||
@@ -22,6 +25,15 @@ class TestSetup {
|
||||
);
|
||||
}
|
||||
|
||||
/// 서비스 로케이터(service locator) 초기화
|
||||
///
|
||||
/// 테스트에서 GetIt 의존성이 필요한 경우 setUpAll에서 호출
|
||||
static void ensureServiceLocator() {
|
||||
if (!GetIt.instance.isRegistered<IIAPService>()) {
|
||||
setupServiceLocator();
|
||||
}
|
||||
}
|
||||
|
||||
/// 모든 싱글톤 서비스 정리
|
||||
///
|
||||
/// tearDown에서 호출하여 타이머 및 리소스 정리
|
||||
|
||||
@@ -4,9 +4,10 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'helpers/test_setup.dart';
|
||||
|
||||
void main() {
|
||||
// SharedPreferences 모킹
|
||||
// SharedPreferences 모킹 및 서비스 로케이터 초기화
|
||||
setUpAll(() {
|
||||
TestSetup.mockSharedPreferences();
|
||||
TestSetup.ensureServiceLocator();
|
||||
});
|
||||
|
||||
// 각 테스트 후 싱글톤 서비스 정리 (타이머 누수 방지)
|
||||
|
||||
Reference in New Issue
Block a user