feat: add payment card grouping and analysis
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../providers/subscription_provider.dart';
|
||||
import '../providers/category_provider.dart';
|
||||
import '../providers/payment_card_provider.dart';
|
||||
import '../services/sms_service.dart';
|
||||
import '../services/subscription_url_matcher.dart';
|
||||
import '../widgets/common/snackbar/app_snackbar.dart';
|
||||
@@ -31,6 +32,7 @@ class AddSubscriptionController {
|
||||
DateTime? nextBillingDate;
|
||||
bool isLoading = false;
|
||||
String? selectedCategoryId;
|
||||
String? selectedPaymentCardId;
|
||||
|
||||
// Event State
|
||||
bool isEventActive = false;
|
||||
@@ -126,6 +128,13 @@ class AddSubscriptionController {
|
||||
// Localizations가 아직 준비되지 않은 경우 기본값 유지
|
||||
}
|
||||
|
||||
// 기본 결제수단 설정
|
||||
try {
|
||||
final paymentCardProvider =
|
||||
Provider.of<PaymentCardProvider>(context, listen: false);
|
||||
selectedPaymentCardId = paymentCardProvider.defaultCard?.id;
|
||||
} catch (_) {}
|
||||
|
||||
// 애니메이션 시작
|
||||
animationController!.forward();
|
||||
}
|
||||
@@ -503,6 +512,7 @@ class AddSubscriptionController {
|
||||
nextBillingDate: adjustedNext,
|
||||
websiteUrl: websiteUrlController.text.trim(),
|
||||
categoryId: selectedCategoryId,
|
||||
paymentCardId: selectedPaymentCardId,
|
||||
currency: currency,
|
||||
isEventActive: isEventActive,
|
||||
eventStartDate: eventStartDate,
|
||||
|
||||
@@ -34,6 +34,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
late String _billingCycle;
|
||||
late DateTime _nextBillingDate;
|
||||
String? _selectedCategoryId;
|
||||
String? _selectedPaymentCardId;
|
||||
late String _currency;
|
||||
bool _isLoading = false;
|
||||
|
||||
@@ -46,6 +47,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
String get billingCycle => _billingCycle;
|
||||
DateTime get nextBillingDate => _nextBillingDate;
|
||||
String? get selectedCategoryId => _selectedCategoryId;
|
||||
String? get selectedPaymentCardId => _selectedPaymentCardId;
|
||||
String get currency => _currency;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isEventActive => _isEventActive;
|
||||
@@ -74,6 +76,13 @@ class DetailScreenController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
set selectedPaymentCardId(String? value) {
|
||||
if (_selectedPaymentCardId != value) {
|
||||
_selectedPaymentCardId = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set currency(String value) {
|
||||
if (_currency != value) {
|
||||
_currency = value;
|
||||
@@ -153,6 +162,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
_billingCycle = subscription.billingCycle;
|
||||
_nextBillingDate = subscription.nextBillingDate;
|
||||
_selectedCategoryId = subscription.categoryId;
|
||||
_selectedPaymentCardId = subscription.paymentCardId;
|
||||
_currency = subscription.currency;
|
||||
|
||||
// Event State 초기화
|
||||
@@ -415,6 +425,7 @@ class DetailScreenController extends ChangeNotifier {
|
||||
BillingDateUtil.ensureFutureDate(originalDateOnly, _billingCycle);
|
||||
subscription.nextBillingDate = adjustedNext;
|
||||
subscription.categoryId = _selectedCategoryId;
|
||||
subscription.paymentCardId = _selectedPaymentCardId;
|
||||
subscription.currency = _currency;
|
||||
|
||||
// 이벤트 정보 업데이트
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/sms_scanner.dart';
|
||||
import '../models/subscription.dart';
|
||||
import '../models/payment_card_suggestion.dart';
|
||||
import '../services/sms_scan/subscription_converter.dart';
|
||||
import '../services/sms_scan/subscription_filter.dart';
|
||||
import '../services/sms_scan/sms_scan_result.dart';
|
||||
import '../providers/subscription_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
@@ -11,6 +13,7 @@ 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';
|
||||
|
||||
class SmsScanController extends ChangeNotifier {
|
||||
// 상태 관리
|
||||
@@ -22,12 +25,18 @@ class SmsScanController extends ChangeNotifier {
|
||||
|
||||
List<Subscription> _scannedSubscriptions = [];
|
||||
List<Subscription> get scannedSubscriptions => _scannedSubscriptions;
|
||||
PaymentCardSuggestion? _currentSuggestion;
|
||||
PaymentCardSuggestion? get currentSuggestion => _currentSuggestion;
|
||||
bool _shouldSuggestCardCreation = false;
|
||||
bool get shouldSuggestCardCreation => _shouldSuggestCardCreation;
|
||||
|
||||
int _currentIndex = 0;
|
||||
int get currentIndex => _currentIndex;
|
||||
|
||||
String? _selectedCategoryId;
|
||||
String? get selectedCategoryId => _selectedCategoryId;
|
||||
String? _selectedPaymentCardId;
|
||||
String? get selectedPaymentCardId => _selectedPaymentCardId;
|
||||
|
||||
final TextEditingController websiteUrlController = TextEditingController();
|
||||
|
||||
@@ -47,6 +56,14 @@ class SmsScanController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setSelectedPaymentCardId(String? paymentCardId) {
|
||||
_selectedPaymentCardId = paymentCardId;
|
||||
if (paymentCardId != null) {
|
||||
_shouldSuggestCardCreation = false;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void resetWebsiteUrl() {
|
||||
websiteUrlController.text = '';
|
||||
}
|
||||
@@ -88,18 +105,18 @@ class SmsScanController extends ChangeNotifier {
|
||||
|
||||
// SMS 스캔 실행
|
||||
Log.i('SMS 스캔 시작');
|
||||
final scannedSubscriptionModels =
|
||||
final List<SmsScanResult> scanResults =
|
||||
await _smsScanner.scanForSubscriptions();
|
||||
Log.d('스캔된 구독: ${scannedSubscriptionModels.length}개');
|
||||
Log.d('스캔된 구독: ${scanResults.length}개');
|
||||
|
||||
if (scannedSubscriptionModels.isNotEmpty) {
|
||||
if (scanResults.isNotEmpty) {
|
||||
Log.d(
|
||||
'첫 번째 구독: ${scannedSubscriptionModels[0].serviceName}, 반복 횟수: ${scannedSubscriptionModels[0].repeatCount}');
|
||||
'첫 번째 구독: ${scanResults[0].model.serviceName}, 반복 횟수: ${scanResults[0].model.repeatCount}');
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (scannedSubscriptionModels.isEmpty) {
|
||||
if (scanResults.isEmpty) {
|
||||
Log.i('스캔된 구독이 없음');
|
||||
_errorMessage = AppLocalizations.of(context).subscriptionNotFound;
|
||||
_isLoading = false;
|
||||
@@ -109,7 +126,7 @@ class SmsScanController extends ChangeNotifier {
|
||||
|
||||
// SubscriptionModel을 Subscription으로 변환
|
||||
final scannedSubscriptions =
|
||||
_converter.convertModelsToSubscriptions(scannedSubscriptionModels);
|
||||
_converter.convertResultsToSubscriptions(scanResults);
|
||||
|
||||
// 2회 이상 반복 결제된 구독만 필터링
|
||||
final repeatSubscriptions =
|
||||
@@ -155,7 +172,9 @@ class SmsScanController extends ChangeNotifier {
|
||||
|
||||
_scannedSubscriptions = filteredSubscriptions;
|
||||
_isLoading = false;
|
||||
websiteUrlController.text = ''; // URL 입력 필드 초기화
|
||||
websiteUrlController.text = '';
|
||||
_currentSuggestion = null;
|
||||
_prepareCurrentSelection(context);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
Log.e('SMS 스캔 중 오류 발생', e);
|
||||
@@ -202,10 +221,14 @@ class SmsScanController extends ChangeNotifier {
|
||||
Provider.of<SubscriptionProvider>(context, listen: false);
|
||||
final categoryProvider =
|
||||
Provider.of<CategoryProvider>(context, listen: false);
|
||||
final paymentCardProvider =
|
||||
Provider.of<PaymentCardProvider>(context, listen: false);
|
||||
|
||||
final finalCategoryId = _selectedCategoryId ??
|
||||
subscription.category ??
|
||||
getDefaultCategoryId(categoryProvider);
|
||||
final finalPaymentCardId =
|
||||
_selectedPaymentCardId ?? paymentCardProvider.defaultCard?.id;
|
||||
|
||||
// websiteUrl 처리
|
||||
final websiteUrl = websiteUrlController.text.trim().isNotEmpty
|
||||
@@ -226,6 +249,7 @@ class SmsScanController extends ChangeNotifier {
|
||||
repeatCount: subscription.repeatCount,
|
||||
lastPaymentDate: subscription.lastPaymentDate,
|
||||
categoryId: finalCategoryId,
|
||||
paymentCardId: finalPaymentCardId,
|
||||
currency: subscription.currency,
|
||||
);
|
||||
|
||||
@@ -248,8 +272,9 @@ class SmsScanController extends ChangeNotifier {
|
||||
|
||||
void moveToNextSubscription(BuildContext context) {
|
||||
_currentIndex++;
|
||||
websiteUrlController.text = ''; // URL 입력 필드 초기화
|
||||
_selectedCategoryId = null; // 카테고리 선택 초기화
|
||||
websiteUrlController.text = '';
|
||||
_selectedCategoryId = null;
|
||||
_prepareCurrentSelection(context);
|
||||
|
||||
// 모든 구독을 처리했으면 홈 화면으로 이동
|
||||
if (_currentIndex >= _scannedSubscriptions.length) {
|
||||
@@ -270,6 +295,9 @@ class SmsScanController extends ChangeNotifier {
|
||||
_scannedSubscriptions = [];
|
||||
_currentIndex = 0;
|
||||
_errorMessage = null;
|
||||
_selectedPaymentCardId = null;
|
||||
_currentSuggestion = null;
|
||||
_shouldSuggestCardCreation = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -290,4 +318,78 @@ class SmsScanController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? _getDefaultPaymentCardId(BuildContext context) {
|
||||
try {
|
||||
final provider = Provider.of<PaymentCardProvider>(context, listen: false);
|
||||
return provider.defaultCard?.id;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _prepareCurrentSelection(BuildContext context) {
|
||||
if (_currentIndex >= _scannedSubscriptions.length) {
|
||||
_selectedPaymentCardId = null;
|
||||
_currentSuggestion = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final current = _scannedSubscriptions[_currentIndex];
|
||||
|
||||
// URL 기본값
|
||||
if (current.websiteUrl != null && current.websiteUrl!.isNotEmpty) {
|
||||
websiteUrlController.text = current.websiteUrl!;
|
||||
} else {
|
||||
websiteUrlController.clear();
|
||||
}
|
||||
|
||||
_currentSuggestion = current.paymentCardSuggestion;
|
||||
|
||||
final matchedCardId = _matchCardWithSuggestion(context, _currentSuggestion);
|
||||
_shouldSuggestCardCreation =
|
||||
_currentSuggestion != null && matchedCardId == null;
|
||||
if (matchedCardId != null) {
|
||||
_selectedPaymentCardId = matchedCardId;
|
||||
return;
|
||||
}
|
||||
|
||||
// 모델에 직접 카드 정보가 존재하면 우선 사용
|
||||
if (current.paymentCardId != null) {
|
||||
_selectedPaymentCardId = current.paymentCardId;
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedPaymentCardId = _getDefaultPaymentCardId(context);
|
||||
}
|
||||
|
||||
String? _matchCardWithSuggestion(
|
||||
BuildContext context, PaymentCardSuggestion? suggestion) {
|
||||
if (suggestion == null) return null;
|
||||
try {
|
||||
final provider = Provider.of<PaymentCardProvider>(context, listen: false);
|
||||
final cards = provider.cards;
|
||||
if (cards.isEmpty) return null;
|
||||
|
||||
if (suggestion.hasLast4) {
|
||||
for (final card in cards) {
|
||||
if (card.last4 == suggestion.last4) {
|
||||
return card.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final normalizedIssuer = suggestion.issuerName.toLowerCase();
|
||||
for (final card in cards) {
|
||||
final issuer = card.issuerName.toLowerCase();
|
||||
if (issuer.contains(normalizedIssuer) ||
|
||||
normalizedIssuer.contains(issuer)) {
|
||||
return card.id;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user