feat: add payment card grouping and analysis
This commit is contained in:
@@ -8,11 +8,13 @@ import '../temp/test_sms_data.dart';
|
||||
import '../services/subscription_url_matcher.dart';
|
||||
import '../utils/platform_helper.dart';
|
||||
import '../utils/business_day_util.dart';
|
||||
import '../services/sms_scan/sms_scan_result.dart';
|
||||
import '../models/payment_card_suggestion.dart';
|
||||
|
||||
class SmsScanner {
|
||||
final SmsQuery _query = SmsQuery();
|
||||
|
||||
Future<List<SubscriptionModel>> scanForSubscriptions() async {
|
||||
Future<List<SmsScanResult>> scanForSubscriptions() async {
|
||||
try {
|
||||
List<dynamic> smsList;
|
||||
Log.d('SmsScanner: 스캔 시작');
|
||||
@@ -39,7 +41,7 @@ class SmsScanner {
|
||||
}
|
||||
|
||||
// SMS 데이터를 분석하여 반복 결제되는 구독 식별
|
||||
final List<SubscriptionModel> subscriptions = [];
|
||||
final List<SmsScanResult> subscriptions = [];
|
||||
final serviceGroups = _groupMessagesByIdentifier(smsList);
|
||||
|
||||
Log.d('SmsScanner: 그룹화된 키 수: ${serviceGroups.length}');
|
||||
@@ -52,12 +54,12 @@ class SmsScanner {
|
||||
continue;
|
||||
}
|
||||
|
||||
final subscription =
|
||||
final result =
|
||||
_parseSms(repeatResult.baseMessage, repeatResult.repeatCount);
|
||||
if (subscription != null) {
|
||||
if (result != null) {
|
||||
Log.i(
|
||||
'SmsScanner: 구독 추가: ${subscription.serviceName}, 반복 횟수: ${subscription.repeatCount}');
|
||||
subscriptions.add(subscription);
|
||||
'SmsScanner: 구독 추가: ${result.model.serviceName}, 반복 횟수: ${result.model.repeatCount}');
|
||||
subscriptions.add(result);
|
||||
} else {
|
||||
Log.w('SmsScanner: 구독 파싱 실패: ${entry.key}');
|
||||
}
|
||||
@@ -99,7 +101,7 @@ class SmsScanner {
|
||||
|
||||
// (사용되지 않음) 기존 단일 메시지 파서 제거됨 - Isolate 배치 파싱으로 대체
|
||||
|
||||
SubscriptionModel? _parseSms(Map<String, dynamic> sms, int repeatCount) {
|
||||
SmsScanResult? _parseSms(Map<String, dynamic> sms, int repeatCount) {
|
||||
try {
|
||||
final serviceName = sms['serviceName'] as String? ?? '알 수 없는 서비스';
|
||||
final monthlyCost = (sms['monthlyCost'] as num?)?.toDouble() ?? 0.0;
|
||||
@@ -150,7 +152,7 @@ class SmsScanner {
|
||||
adjustedNextBillingDate =
|
||||
BusinessDayUtil.nextBusinessDay(adjustedNextBillingDate);
|
||||
|
||||
return SubscriptionModel(
|
||||
final model = SubscriptionModel(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
serviceName: serviceName,
|
||||
monthlyCost: monthlyCost,
|
||||
@@ -162,11 +164,84 @@ class SmsScanner {
|
||||
websiteUrl: _extractWebsiteUrl(serviceName),
|
||||
currency: currency, // 통화 단위 설정
|
||||
);
|
||||
|
||||
final suggestion = _extractPaymentCardSuggestion(message);
|
||||
return SmsScanResult(
|
||||
model: model,
|
||||
cardSuggestion: suggestion,
|
||||
rawMessage: message,
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PaymentCardSuggestion? _extractPaymentCardSuggestion(String message) {
|
||||
if (message.isEmpty) return null;
|
||||
final issuer = _detectCardIssuer(message);
|
||||
final last4 = _detectCardLast4(message);
|
||||
if (issuer == null && last4 == null) {
|
||||
return null;
|
||||
}
|
||||
return PaymentCardSuggestion(
|
||||
issuerName: issuer ?? '결제수단',
|
||||
last4: last4,
|
||||
source: 'sms',
|
||||
);
|
||||
}
|
||||
|
||||
String? _detectCardIssuer(String message) {
|
||||
final normalized = message.toLowerCase();
|
||||
const issuerKeywords = {
|
||||
'KB국민카드': ['kb국민', '국민카드', 'kb card', 'kookmin'],
|
||||
'신한카드': ['신한', 'shinhan'],
|
||||
'우리카드': ['우리카드', 'woori'],
|
||||
'하나카드': ['하나카드', 'hana card', 'hana'],
|
||||
'농협카드': ['농협', 'nh', '농협카드'],
|
||||
'BC카드': ['bc카드', 'bc card'],
|
||||
'삼성카드': ['삼성카드', 'samsung card'],
|
||||
'롯데카드': ['롯데카드', 'lotte card'],
|
||||
'현대카드': ['현대카드', 'hyundai card'],
|
||||
'씨티카드': ['씨티카드', 'citi card', 'citibank'],
|
||||
'카카오뱅크': ['카카오뱅크', 'kakaobank'],
|
||||
'토스뱅크': ['토스뱅크', 'toss bank'],
|
||||
'Visa': ['visa'],
|
||||
'Mastercard': ['mastercard', 'master card'],
|
||||
'American Express': ['amex', 'american express'],
|
||||
};
|
||||
|
||||
for (final entry in issuerKeywords.entries) {
|
||||
final match = entry.value.any((keyword) => normalized.contains(keyword));
|
||||
if (match) {
|
||||
return entry.key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _detectCardLast4(String message) {
|
||||
final patterns = [
|
||||
RegExp(r'\*{3,}\s*(\d{4})'),
|
||||
RegExp(r'끝번호\s*(\d{4})'),
|
||||
RegExp(r'마지막\s*(\d{4})'),
|
||||
RegExp(r'\((\d{4})\)'),
|
||||
RegExp(r'ending(?: in)?\s*(\d{4})', caseSensitive: false),
|
||||
];
|
||||
|
||||
for (final pattern in patterns) {
|
||||
final match = pattern.firstMatch(message);
|
||||
if (match != null && match.groupCount >= 1) {
|
||||
final candidate = match.group(1);
|
||||
if (candidate != null && candidate.length == 4) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 다음 결제일 계산 (현재 날짜 기준으로 조정)
|
||||
DateTime _calculateNextBillingDate(
|
||||
DateTime billingDate, String billingCycle) {
|
||||
|
||||
Reference in New Issue
Block a user