Files
submanager/lib/services/ad_service.dart
2026-01-06 15:53:45 +09:00

199 lines
5.5 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'dart:io' show Platform;
import '../utils/logger.dart';
/// 전면 광고(Interstitial Ad) 서비스
/// lunchpick 프로젝트의 AdService 패턴을 참조하여 구현
class AdService {
InterstitialAd? _interstitialAd;
Completer<bool>? _loadingCompleter;
/// 모바일 플랫폼 여부 확인
bool get _isMobilePlatform {
if (kIsWeb) return false;
return Platform.isAndroid || Platform.isIOS;
}
/// 전면 광고 Unit ID 반환
String get _interstitialAdUnitId {
if (Platform.isAndroid) {
return 'ca-app-pub-6691216385521068/5281562472';
} else if (Platform.isIOS) {
// iOS 테스트 광고 ID
return 'ca-app-pub-3940256099942544/1033173712';
}
return '';
}
/// 광고를 로드하고 표시한 뒤 완료 여부를 반환
/// true: 광고 시청 완료 또는 미지원 플랫폼
/// false: 광고 로드/표시 실패
Future<bool> showInterstitialAd(BuildContext context) async {
if (!_isMobilePlatform) return true;
Log.i('광고 표시 시작');
// 1. 로딩 오버레이 표시 (앱이 foreground 상태 유지)
final closeLoading = _showLoadingOverlay(context);
// 2. 몰입형 모드 진입
await _enterImmersiveMode();
// 3. 광고 로드
final loaded = await _ensureAdLoaded();
// 4. 로딩 오버레이 닫기
closeLoading();
if (!loaded) {
Log.w('광고 로드 실패, 건너뜀');
await _restoreSystemUi();
return false;
}
final ad = _interstitialAd;
if (ad == null) {
Log.w('광고 인스턴스 없음, 건너뜀');
await _restoreSystemUi();
return false;
}
// 현재 광고를 null로 설정 (다음 광고 미리로드)
_interstitialAd = null;
final completer = Completer<bool>();
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdShowedFullScreenContent: (ad) {
Log.i('전면 광고 표시됨 (콜백)');
},
onAdDismissedFullScreenContent: (ad) {
Log.i('전면 광고 닫힘 (콜백)');
ad.dispose();
_preload();
unawaited(_restoreSystemUi());
if (!completer.isCompleted) {
completer.complete(true);
}
},
onAdFailedToShowFullScreenContent: (ad, error) {
Log.e('전면 광고 표시 실패 (콜백)', error);
ad.dispose();
_preload();
unawaited(_restoreSystemUi());
if (!completer.isCompleted) {
completer.complete(false);
}
},
);
// 전체 화면으로 표시하도록 immersive 모드 설정
ad.setImmersiveMode(true);
Log.i('ad.show() 호출 직전');
try {
ad.show();
Log.i('ad.show() 호출 완료');
} catch (e) {
Log.e('광고 show() 호출 실패', e);
unawaited(_restoreSystemUi());
if (!completer.isCompleted) {
completer.complete(false);
}
return false;
}
// 타임아웃 설정 (15초 후 자동 건너뜀)
return completer.future.timeout(
const Duration(seconds: 15),
onTimeout: () {
Log.w('광고 표시 타임아웃, 건너뜀');
unawaited(_restoreSystemUi());
return false;
},
);
}
/// 로딩 오버레이 표시 (앱이 foreground 상태 유지)
VoidCallback _showLoadingOverlay(BuildContext context) {
final navigator = Navigator.of(context, rootNavigator: true);
showDialog<void>(
context: context,
barrierDismissible: false,
barrierColor: Colors.black.withValues(alpha: 0.35),
builder: (_) => const Center(child: CircularProgressIndicator()),
);
return () {
if (navigator.mounted && navigator.canPop()) {
navigator.pop();
}
};
}
/// 몰입형 모드 진입 (상하단 시스템 UI 숨김)
Future<void> _enterImmersiveMode() async {
try {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [],
);
} catch (_) {}
}
/// UI 복구 (main.dart의 설정과 동일하게 immersiveSticky 유지)
Future<void> _restoreSystemUi() async {
try {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [SystemUiOverlay.top], // 상태바만 유지
);
} catch (_) {}
}
/// 광고 로드 보장 (이미 로드된 경우 즉시 반환)
Future<bool> _ensureAdLoaded() async {
if (_interstitialAd != null) return true;
if (_loadingCompleter != null) {
return _loadingCompleter!.future;
}
final completer = Completer<bool>();
_loadingCompleter = completer;
Log.i('전면 광고 로드 시작: $_interstitialAdUnitId');
InterstitialAd.load(
adUnitId: _interstitialAdUnitId,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) {
Log.i('전면 광고 로드 성공');
_interstitialAd = ad;
completer.complete(true);
_loadingCompleter = null;
},
onAdFailedToLoad: (error) {
Log.e('전면 광고 로드 실패', error);
completer.complete(false);
_loadingCompleter = null;
},
),
);
return completer.future;
}
/// 다음 광고 미리로드
void _preload() {
if (_interstitialAd != null || _loadingCompleter != null) return;
_ensureAdLoaded();
}
}