feat: SMS 스캔 전면광고 및 Isolate 버그 수정

## 전면 광고 (AdService)
- AdService 클래스 신규 생성 (lunchpick 패턴 참조)
- Completer 패턴으로 광고 완료 대기 구현
- 로딩 오버레이로 앱 foreground 상태 유지
- 몰입형 모드 (immersiveSticky) 적용
- iOS 테스트 광고 ID 설정

## SMS 스캔 버그 수정
- Isolate 내 Flutter 바인딩 접근 오류 해결
- _isoExtractServiceNameFromSender()에서 하드코딩 사용
- 로딩 위젯 화면 정중앙 배치 수정

## 문서 및 설정
- CLAUDE.md 최적화 (글로벌 규칙 중복 제거)
- Claude Code Skills 5개 추가
  - flutter-build: 빌드/분석
  - hive-model: Hive 모델 관리
  - release-deploy: 릴리즈 배포
  - sms-scanner: SMS 스캔 디버깅
  - admob: 광고 구현

## 버전
- 1.0.1+2 → 1.0.1+3
This commit is contained in:
JiWoong Sul
2025-12-08 18:14:52 +09:00
parent bac4acf9a3
commit 83c43fb61f
12 changed files with 639 additions and 434 deletions

View File

@@ -1,21 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:provider/provider.dart';
import 'package:permission_handler/permission_handler.dart' as permission;
import 'dart:io' show Platform;
import '../services/sms_scanner.dart';
import '../models/subscription.dart';
import '../models/payment_card_suggestion.dart';
import '../services/ad_service.dart';
import '../services/sms_scan/subscription_converter.dart';
import '../services/sms_scan/subscription_filter.dart';
import '../services/sms_scan/sms_scan_result.dart';
import '../models/subscription.dart';
import '../models/payment_card_suggestion.dart';
import '../providers/subscription_provider.dart';
import 'package:provider/provider.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:permission_handler/permission_handler.dart' as permission;
import '../utils/logger.dart';
import '../providers/navigation_provider.dart';
import '../providers/category_provider.dart';
import '../l10n/app_localizations.dart';
import '../providers/payment_card_provider.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'dart:io' show Platform;
import '../l10n/app_localizations.dart';
import '../utils/logger.dart';
class SmsScanController extends ChangeNotifier {
// 상태 관리
@@ -47,10 +48,9 @@ class SmsScanController extends ChangeNotifier {
final SmsScanner _smsScanner = SmsScanner();
final SubscriptionConverter _converter = SubscriptionConverter();
final SubscriptionFilter _filter = SubscriptionFilter();
final AdService _adService = AdService();
bool _forceServiceNameEditing = false;
bool get isServiceNameEditable => _forceServiceNameEditing;
bool _isAdInProgress = false;
bool get isAdInProgress => _isAdInProgress;
@override
void dispose() {
@@ -87,69 +87,26 @@ class SmsScanController extends ChangeNotifier {
notifyListeners();
}
/// SMS 스캔 시작 (전면 광고 표시 후 스캔 진행)
Future<void> startScan(BuildContext context) async {
if (_isLoading) return;
_isAdInProgress = true;
notifyListeners();
// 웹/비지원 플랫폼은 바로 스캔
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) {
_isAdInProgress = false;
notifyListeners();
await scanSms(context);
return;
}
// 전면 광고 로드 및 노출 후 스캔 진행
try {
await InterstitialAd.load(
adUnitId: _interstitialAdUnitId(),
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) {
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
ad.dispose();
_startSmsScanIfMounted(context);
},
onAdFailedToShowFullScreenContent: (ad, error) {
ad.dispose();
_fallbackAfterDelay(context);
},
);
ad.show();
},
onAdFailedToLoad: (error) {
_fallbackAfterDelay(context);
},
),
);
} catch (e) {
Log.e('전면 광고 로드 중 오류, 바로 스캔 진행', e);
if (!context.mounted) return;
_fallbackAfterDelay(context);
}
}
// 광고 표시 (완료까지 대기)
// 광고 실패해도 스캔 진행 (사용자 경험 우선)
await _adService.showInterstitialAd(context);
String _interstitialAdUnitId() {
if (Platform.isAndroid || Platform.isIOS) {
return 'ca-app-pub-6691216385521068~6638409932';
}
return '';
}
Future<void> _startSmsScanIfMounted(BuildContext context) async {
if (!context.mounted) return;
_isAdInProgress = false;
notifyListeners();
// 광고 완료 후 SMS 스캔 실행
await scanSms(context);
}
Future<void> _fallbackAfterDelay(BuildContext context) async {
await Future.delayed(const Duration(seconds: 5));
if (!context.mounted) return;
await _startSmsScanIfMounted(context);
}
Future<void> scanSms(BuildContext context) async {
_isLoading = true;
_errorMessage = null;
@@ -157,6 +114,11 @@ class SmsScanController extends ChangeNotifier {
_currentIndex = 0;
notifyListeners();
await _performSmsScan(context);
}
/// 실제 SMS 스캔 수행 (로딩 상태는 이미 설정되어 있음)
Future<void> _performSmsScan(BuildContext context) async {
try {
// Android에서 SMS 권한 확인 및 요청
final ctx = context;
@@ -399,13 +361,14 @@ class SmsScanController extends ChangeNotifier {
return otherCategory.id;
}
void initializeWebsiteUrl() {
void initializeWebsiteUrl(BuildContext context) {
if (_currentIndex < _scannedSubscriptions.length) {
final currentSub = _scannedSubscriptions[_currentIndex];
if (websiteUrlController.text.isEmpty && currentSub.websiteUrl != null) {
websiteUrlController.text = currentSub.websiteUrl!;
}
if (_shouldEnableServiceNameEditing(currentSub)) {
final unknownLabel = _unknownServiceLabel(context);
if (_shouldEnableServiceNameEditing(currentSub, unknownLabel)) {
if (serviceNameController.text != currentSub.serviceName) {
serviceNameController.clear();
}