feat(ads): AdMob 광고 서비스 추가

- 리워드/인터스티셜 광고 로드 및 표시
- 디버그 모드 광고 토글 지원
- 비모바일 플랫폼 자동 스킵
This commit is contained in:
JiWoong Sul
2026-01-16 20:08:27 +09:00
parent 724f08f56d
commit 6662a5dcfb

View File

@@ -0,0 +1,359 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
/// 광고 타입
enum AdType {
/// 부활용 리워드 광고 (30초)
rewardRevive,
/// 캐릭터 생성 되돌리기용 리워드 광고 (30초)
rewardUndo,
/// 굴리기 충전용 인터스티셜 광고 (6초)
interstitialRoll,
/// 속도업용 인터스티셜 광고 (6초)
interstitialSpeed,
}
/// 광고 결과
enum AdResult {
/// 광고 시청 완료 (보상 지급)
completed,
/// 광고 시청 취소/스킵
cancelled,
/// 광고 로드 실패
failed,
/// 디버그 모드에서 광고 스킵
debugSkipped,
}
/// 광고 서비스
///
/// AdMob 리워드/인터스티셜 광고 로드 및 표시를 관리합니다.
/// 디버그 모드에서는 광고 ON/OFF 토글이 가능합니다.
class AdService {
AdService._();
static AdService? _instance;
/// 싱글톤 인스턴스
static AdService get instance {
_instance ??= AdService._();
return _instance!;
}
// ===========================================================================
// 광고 단위 ID
// ===========================================================================
// ─────────────────────────────────────────────────────────────────────────
// 테스트 광고 ID (Google 공식 테스트 ID)
// ─────────────────────────────────────────────────────────────────────────
static const String _testRewardedAndroid =
'ca-app-pub-3940256099942544/5224354917';
static const String _testRewardedIos =
'ca-app-pub-3940256099942544/1712485313';
static const String _testInterstitialAndroid =
'ca-app-pub-3940256099942544/1033173712';
static const String _testInterstitialIos =
'ca-app-pub-3940256099942544/4411468910';
// ─────────────────────────────────────────────────────────────────────────
// 프로덕션 광고 ID (AdMob 콘솔에서 생성 후 교체)
// ─────────────────────────────────────────────────────────────────────────
// TODO: AdMob 콘솔에서 광고 단위 생성 후 아래 ID 교체
static const String _prodRewardedAndroid =
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // Android 리워드 광고
static const String _prodRewardedIos =
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // iOS 리워드 광고
static const String _prodInterstitialAndroid =
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // Android 인터스티셜 광고
static const String _prodInterstitialIos =
'ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX'; // iOS 인터스티셜 광고
/// 리워드 광고 단위 ID (릴리즈 빌드: 프로덕션 ID, 디버그 빌드: 테스트 ID)
String get _rewardAdUnitId {
if (Platform.isAndroid) {
return kReleaseMode ? _prodRewardedAndroid : _testRewardedAndroid;
} else if (Platform.isIOS) {
return kReleaseMode ? _prodRewardedIos : _testRewardedIos;
}
return '';
}
/// 인터스티셜 광고 단위 ID (릴리즈 빌드: 프로덕션 ID, 디버그 빌드: 테스트 ID)
String get _interstitialAdUnitId {
if (Platform.isAndroid) {
return kReleaseMode ? _prodInterstitialAndroid : _testInterstitialAndroid;
} else if (Platform.isIOS) {
return kReleaseMode ? _prodInterstitialIos : _testInterstitialIos;
}
return '';
}
// ===========================================================================
// 상태
// ===========================================================================
bool _isInitialized = false;
/// 디버그 모드에서 광고 활성화 여부
bool _debugAdEnabled = true;
/// 로드된 리워드 광고
RewardedAd? _rewardedAd;
/// 로드된 인터스티셜 광고
InterstitialAd? _interstitialAd;
/// 리워드 광고 로딩 중 여부
bool _isLoadingRewardedAd = false;
/// 인터스티셜 광고 로딩 중 여부
bool _isLoadingInterstitialAd = false;
// ===========================================================================
// 초기화
// ===========================================================================
/// AdMob SDK 초기화
Future<void> initialize() async {
if (_isInitialized) return;
// 모바일 플랫폼에서만 초기화
if (!Platform.isAndroid && !Platform.isIOS) {
debugPrint('[AdService] Non-mobile platform, skipping initialization');
return;
}
await MobileAds.instance.initialize();
_isInitialized = true;
debugPrint('[AdService] Initialized');
// 초기 광고 로드
_loadRewardedAd();
_loadInterstitialAd();
}
// ===========================================================================
// 디버그 설정
// ===========================================================================
/// 디버그 모드 광고 활성화 여부
bool get debugAdEnabled => _debugAdEnabled;
/// 디버그 모드 광고 토글
set debugAdEnabled(bool value) {
_debugAdEnabled = value;
debugPrint('[AdService] Debug ad enabled: $value');
}
/// 광고를 스킵할지 여부
///
/// 스킵 조건:
/// - 비모바일 플랫폼 (macOS, Windows, Linux, Web)
/// - 디버그 모드에서 광고 비활성화
bool get _shouldSkipAd {
// 웹에서는 항상 스킵
if (kIsWeb) return true;
// 비모바일 플랫폼(데스크톱)에서는 항상 스킵
if (!Platform.isAndroid && !Platform.isIOS) {
return true;
}
// 디버그 모드에서 광고 비활성화 시 스킵
return kDebugMode && !_debugAdEnabled;
}
// ===========================================================================
// 리워드 광고
// ===========================================================================
/// 리워드 광고 로드
void _loadRewardedAd() {
if (_isLoadingRewardedAd || _rewardedAd != null) return;
if (!_isInitialized) return;
_isLoadingRewardedAd = true;
debugPrint('[AdService] Loading rewarded ad...');
RewardedAd.load(
adUnitId: _rewardAdUnitId,
request: const AdRequest(),
rewardedAdLoadCallback: RewardedAdLoadCallback(
onAdLoaded: (ad) {
_rewardedAd = ad;
_isLoadingRewardedAd = false;
debugPrint('[AdService] Rewarded ad loaded');
},
onAdFailedToLoad: (error) {
_isLoadingRewardedAd = false;
debugPrint('[AdService] Rewarded ad failed to load: ${error.message}');
},
),
);
}
/// 리워드 광고 준비 여부
bool get isRewardedAdReady => _rewardedAd != null || _shouldSkipAd;
/// 리워드 광고 표시
///
/// [adType] 광고 타입 (로깅용)
/// [onRewarded] 보상 지급 콜백
/// Returns: 광고 결과
Future<AdResult> showRewardedAd({
required AdType adType,
required void Function() onRewarded,
}) async {
// 디버그 모드에서 광고 스킵
if (_shouldSkipAd) {
debugPrint('[AdService] Debug: Skipping $adType ad');
onRewarded();
return AdResult.debugSkipped;
}
// 광고가 로드되지 않은 경우
if (_rewardedAd == null) {
debugPrint('[AdService] Rewarded ad not ready');
_loadRewardedAd();
return AdResult.failed;
}
final ad = _rewardedAd!;
_rewardedAd = null;
// 결과 추적용
var result = AdResult.cancelled;
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
debugPrint('[AdService] Rewarded ad dismissed');
ad.dispose();
_loadRewardedAd(); // 다음 광고 미리 로드
},
onAdFailedToShowFullScreenContent: (ad, error) {
debugPrint('[AdService] Rewarded ad failed to show: ${error.message}');
ad.dispose();
result = AdResult.failed;
_loadRewardedAd();
},
);
await ad.show(
onUserEarnedReward: (ad, reward) {
debugPrint('[AdService] User earned reward: ${reward.amount}');
result = AdResult.completed;
onRewarded();
},
);
return result;
}
// ===========================================================================
// 인터스티셜 광고
// ===========================================================================
/// 인터스티셜 광고 로드
void _loadInterstitialAd() {
if (_isLoadingInterstitialAd || _interstitialAd != null) return;
if (!_isInitialized) return;
_isLoadingInterstitialAd = true;
debugPrint('[AdService] Loading interstitial ad...');
InterstitialAd.load(
adUnitId: _interstitialAdUnitId,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) {
_interstitialAd = ad;
_isLoadingInterstitialAd = false;
debugPrint('[AdService] Interstitial ad loaded');
},
onAdFailedToLoad: (error) {
_isLoadingInterstitialAd = false;
debugPrint(
'[AdService] Interstitial ad failed to load: ${error.message}',
);
},
),
);
}
/// 인터스티셜 광고 준비 여부
bool get isInterstitialAdReady => _interstitialAd != null || _shouldSkipAd;
/// 인터스티셜 광고 표시
///
/// [adType] 광고 타입 (로깅용)
/// [onComplete] 광고 완료 콜백 (보상 지급)
/// Returns: 광고 결과
Future<AdResult> showInterstitialAd({
required AdType adType,
required void Function() onComplete,
}) async {
// 디버그 모드에서 광고 스킵
if (_shouldSkipAd) {
debugPrint('[AdService] Debug: Skipping $adType ad');
onComplete();
return AdResult.debugSkipped;
}
// 광고가 로드되지 않은 경우
if (_interstitialAd == null) {
debugPrint('[AdService] Interstitial ad not ready');
_loadInterstitialAd();
return AdResult.failed;
}
final ad = _interstitialAd!;
_interstitialAd = null;
// 결과 추적용
var result = AdResult.cancelled;
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
debugPrint('[AdService] Interstitial ad dismissed');
ad.dispose();
result = AdResult.completed;
onComplete();
_loadInterstitialAd(); // 다음 광고 미리 로드
},
onAdFailedToShowFullScreenContent: (ad, error) {
debugPrint(
'[AdService] Interstitial ad failed to show: ${error.message}',
);
ad.dispose();
result = AdResult.failed;
_loadInterstitialAd();
},
);
await ad.show();
return result;
}
// ===========================================================================
// 정리
// ===========================================================================
/// 리소스 해제
void dispose() {
_rewardedAd?.dispose();
_rewardedAd = null;
_interstitialAd?.dispose();
_interstitialAd = null;
debugPrint('[AdService] Disposed');
}
}