- 설정 화면에 SMS 권한 카드 추가: 상태 표시(허용/미허용/영구 거부), 권한 요청/설정 이동 지원\n- 기존 알림 권한 카드 스타일과 일관성 유지 feat(permissions): 최초 실행 시 SMS 권한 온보딩 화면 추가 및 Splash에서 라우팅 (Android) - 권한 필요 이유/수집 범위 현지화 문구 추가\n- 거부/영구거부 케이스 처리 및 설정 이동 chore(codex): AGENTS.md/체크 스크립트/CI/프롬프트 템플릿 추가 - AGENTS.md, scripts/check.sh, scripts/fix.sh, .github/workflows/flutter_ci.yml, .claude/agents/codex.md, 문서 템플릿 추가 refactor(logging): 경로별 print 제거 후 경량 로거(Log) 도입 - SMS 스캐너/컨트롤러, URL 매처, 데이터 리포지토리, 내비게이션, 메모리/성능 유틸 등 핵심 경로 치환 feat(exchange): 환율 API URL을 --dart-define로 오버라이드 가능 + 폴백 로깅 강화 test: URL 매처/환율 스모크 테스트 추가 chore(android): RECEIVE_SMS 권한 제거 (READ_SMS만 유지) fix(lints): dart fix + 수동 정리로 경고 대폭 감소, 비동기 context(mounted) 보강 fix(deprecations):\n- flutter_local_notifications의 androidAllowWhileIdle → androidScheduleMode 전환\n- WillPopScope → PopScope 교체 i18n: SMS 권한 온보딩/설정 문구 현지화 키 추가
344 lines
14 KiB
Dart
344 lines
14 KiB
Dart
// 테스트용 SMS 데이터
|
|
// 실제 SMS를 스캔하는 대신 이 테스트 데이터를 사용합니다.
|
|
|
|
class TestSmsData {
|
|
static List<Map<String, dynamic>> getTestData() {
|
|
// 현재 날짜 기준으로 미래 날짜 계산
|
|
final now = DateTime.now();
|
|
final nextMonth = DateTime(now.year, now.month + 1, now.day);
|
|
final prevMonth = DateTime(now.year, now.month - 1, now.day);
|
|
|
|
final String formattedNextMonth =
|
|
'${nextMonth.year}-${nextMonth.month.toString().padLeft(2, '0')}-${nextMonth.day.toString().padLeft(2, '0')}';
|
|
final String formattedPrevMonth =
|
|
'${prevMonth.year}-${prevMonth.month.toString().padLeft(2, '0')}-${prevMonth.day.toString().padLeft(2, '0')}';
|
|
final String formattedNow =
|
|
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
|
|
|
// 1년 후 날짜 계산 (연간 구독용)
|
|
final nextYear = DateTime(now.year + 1, now.month, now.day);
|
|
final prevYear = DateTime(now.year - 1, now.month, now.day);
|
|
final String formattedNextYear =
|
|
'${nextYear.year}-${nextYear.month.toString().padLeft(2, '0')}-${nextYear.day.toString().padLeft(2, '0')}';
|
|
final String formattedPrevYear =
|
|
'${prevYear.year}-${prevYear.month.toString().padLeft(2, '0')}-${prevYear.day.toString().padLeft(2, '0')}';
|
|
|
|
// 기본 테스트 데이터 정의
|
|
final baseTestData = [
|
|
{
|
|
'serviceName': '넷플릭스',
|
|
'monthlyCost': 9900.0,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate': formattedNextMonth,
|
|
'isRecurring': true,
|
|
'repeatCount': 5,
|
|
'sender': '15885000',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate': formattedPrevMonth,
|
|
'message': '[넷플릭스] 정기 결제 완료: 9,900원이 결제되었습니다. 다음 결제일: ${nextMonth.day}일'
|
|
},
|
|
{
|
|
'serviceName': '유튜브프리미엄',
|
|
'monthlyCost': 14900.0,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate':
|
|
'${DateTime(now.year, now.month + 1, 20).year}-${DateTime(now.year, now.month + 1, 20).month.toString().padLeft(2, '0')}-20',
|
|
'isRecurring': true,
|
|
'repeatCount': 7,
|
|
'sender': '15882018',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate':
|
|
'${DateTime(now.year, now.month - 1, 20).year}-${DateTime(now.year, now.month - 1, 20).month.toString().padLeft(2, '0')}-20',
|
|
'message': '[Google] 유튜브프리미엄 구독료 14,900원이 자동 결제되었습니다.'
|
|
},
|
|
{
|
|
'serviceName': '디즈니플러스',
|
|
'monthlyCost': 8900.0,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate':
|
|
'${DateTime(now.year, now.month + 1, 10).year}-${DateTime(now.year, now.month + 1, 10).month.toString().padLeft(2, '0')}-10',
|
|
'isRecurring': true,
|
|
'repeatCount': 3,
|
|
'sender': '15771055',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate':
|
|
'${DateTime(now.year, now.month - 1, 10).year}-${DateTime(now.year, now.month - 1, 10).month.toString().padLeft(2, '0')}-10',
|
|
'message': '[디즈니플러스] 8,900원 정기결제가 완료되었습니다.'
|
|
},
|
|
{
|
|
'serviceName': '애플 iCloud',
|
|
'monthlyCost': 2900.0,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate':
|
|
'${DateTime(now.year, now.month + 1, 5).year}-${DateTime(now.year, now.month + 1, 5).month.toString().padLeft(2, '0')}-05',
|
|
'isRecurring': true,
|
|
'repeatCount': 12,
|
|
'sender': '0802011900',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate':
|
|
'${DateTime(now.year, now.month - 1, 5).year}-${DateTime(now.year, now.month - 1, 5).month.toString().padLeft(2, '0')}-05',
|
|
'message': 'Apple: iCloud+ 50GB 월 구독(₩2,900)이 자동으로 갱신되었습니다.'
|
|
},
|
|
{
|
|
'serviceName': '멜론',
|
|
'monthlyCost': 10900.0,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate':
|
|
'${DateTime(now.year, now.month + 1, 22).year}-${DateTime(now.year, now.month + 1, 22).month.toString().padLeft(2, '0')}-22',
|
|
'isRecurring': true,
|
|
'repeatCount': 4,
|
|
'sender': '16001950',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate':
|
|
'${DateTime(now.year, now.month - 1, 22).year}-${DateTime(now.year, now.month - 1, 22).month.toString().padLeft(2, '0')}-22',
|
|
'message':
|
|
'[멜론] 스트리밍클럽 정기결제 10,900원 완료. 다음 결제일: ${DateTime(now.year, now.month + 1, 22).day}일'
|
|
},
|
|
{
|
|
'serviceName': 'Microsoft 365',
|
|
'monthlyCost': 12800.0,
|
|
'billingCycle': '연간',
|
|
'nextBillingDate': formattedNextYear,
|
|
'isRecurring': true,
|
|
'repeatCount': 2,
|
|
'sender': '0801136532',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate': formattedPrevYear,
|
|
'message': '[Microsoft] Microsoft 365 연간 구독료 12,800원이 결제되었습니다.'
|
|
},
|
|
{
|
|
'serviceName': '웨이브',
|
|
'monthlyCost': 7900.0,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate':
|
|
'${DateTime(now.year, now.month + 1, 15).year}-${DateTime(now.year, now.month + 1, 15).month.toString().padLeft(2, '0')}-15',
|
|
'isRecurring': true,
|
|
'repeatCount': 2,
|
|
'sender': '1800-1234',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate':
|
|
'${DateTime(now.year, now.month - 1, 15).year}-${DateTime(now.year, now.month - 1, 15).month.toString().padLeft(2, '0')}-15',
|
|
'message': '[웨이브] 구독료 7,900원이 정기결제 되었습니다. 감사합니다.'
|
|
},
|
|
// 달러 결제 서비스 추가
|
|
{
|
|
'serviceName': 'Netflix US',
|
|
'monthlyCost': 9.99,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate':
|
|
'${DateTime(now.year, now.month + 1, 7).year}-${DateTime(now.year, now.month + 1, 7).month.toString().padLeft(2, '0')}-07',
|
|
'isRecurring': true,
|
|
'repeatCount': 6,
|
|
'sender': '334455',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate':
|
|
'${DateTime(now.year, now.month - 1, 7).year}-${DateTime(now.year, now.month - 1, 7).month.toString().padLeft(2, '0')}-07',
|
|
'message':
|
|
'[Netflix US] Your subscription has been renewed. \$9.99 has been charged to your account. Next billing date: ${DateTime(now.year, now.month + 1, 7).day}'
|
|
},
|
|
{
|
|
'serviceName': 'Spotify Premium',
|
|
'monthlyCost': 10.99,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate':
|
|
'${DateTime(now.year, now.month + 1, 12).year}-${DateTime(now.year, now.month + 1, 12).month.toString().padLeft(2, '0')}-12',
|
|
'isRecurring': true,
|
|
'repeatCount': 4,
|
|
'sender': '223344',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate':
|
|
'${DateTime(now.year, now.month - 1, 12).year}-${DateTime(now.year, now.month - 1, 12).month.toString().padLeft(2, '0')}-12',
|
|
'message':
|
|
'[Spotify] Your premium subscription was automatically renewed. USD 10.99 charged to your card. Your next payment will be on ${DateTime(now.year, now.month + 1, 12).day}'
|
|
},
|
|
{
|
|
'serviceName': 'GitHub Pro',
|
|
'monthlyCost': 4.00,
|
|
'billingCycle': '월간',
|
|
'nextBillingDate':
|
|
'${DateTime(now.year, now.month + 1, 3).year}-${DateTime(now.year, now.month + 1, 3).month.toString().padLeft(2, '0')}-03',
|
|
'isRecurring': true,
|
|
'repeatCount': 8,
|
|
'sender': '112233',
|
|
'messageDate': formattedNow,
|
|
'previousPaymentDate':
|
|
'${DateTime(now.year, now.month - 1, 3).year}-${DateTime(now.year, now.month - 1, 3).month.toString().padLeft(2, '0')}-03',
|
|
'message':
|
|
'[GitHub] Your Pro plan has been renewed for \$4.00 USD. View your receipt at github.com/receipt. Next bill on ${DateTime(now.year, now.month + 1, 3).day}'
|
|
},
|
|
];
|
|
|
|
// 각 서비스별로 여러 개의 메시지 생성 (그룹화를 위해)
|
|
final List<Map<String, dynamic>> resultData = [];
|
|
|
|
// 각 기본 데이터를 복제하여 여러 개의 메시지 생성
|
|
for (final service in baseTestData) {
|
|
// 원본 메시지 추가
|
|
resultData.add(Map<String, dynamic>.from(service));
|
|
|
|
// 각 서비스에 대해 과거 메시지 추가 (2개 이상의 메시지가 있어야 반복 구독으로 인식)
|
|
for (int i = 2; i <= (service['repeatCount'] as int); i++) {
|
|
// 과거 결제일 계산
|
|
final pastMonth = DateTime(
|
|
now.year,
|
|
now.month - i + 1 > 0 ? now.month - i + 1 : now.month - i + 1 + 12,
|
|
service['billingCycle'] == '월간' ? now.day : now.day);
|
|
|
|
final formattedPastMonth =
|
|
'${pastMonth.year}-${pastMonth.month.toString().padLeft(2, '0')}-${pastMonth.day.toString().padLeft(2, '0')}';
|
|
|
|
// 과거 메시지 생성
|
|
final pastMessage = Map<String, dynamic>.from(service);
|
|
pastMessage['messageDate'] = formattedPastMonth;
|
|
pastMessage['nextBillingDate'] =
|
|
service['previousPaymentDate']; // 이전 메시지의 다음 결제일은 현재의 이전 결제일
|
|
|
|
// 과거 메시지의 이전 결제일 계산
|
|
final veryPastMonth = DateTime(
|
|
pastMonth.year,
|
|
pastMonth.month - 1 > 0
|
|
? pastMonth.month - 1
|
|
: pastMonth.month - 1 + 12,
|
|
pastMonth.day);
|
|
|
|
final formattedVeryPastMonth =
|
|
'${veryPastMonth.year}-${veryPastMonth.month.toString().padLeft(2, '0')}-${veryPastMonth.day.toString().padLeft(2, '0')}';
|
|
|
|
pastMessage['previousPaymentDate'] = formattedVeryPastMonth;
|
|
|
|
resultData.add(pastMessage);
|
|
}
|
|
}
|
|
|
|
// ignore: avoid_print
|
|
print('TestSmsData: 생성된 테스트 메시지 수: ${resultData.length}개');
|
|
return resultData;
|
|
}
|
|
|
|
// 최근 6개월의 월간 지출 데이터를 반환하는 메서드
|
|
static List<Map<String, dynamic>> getMonthlyExpenseData() {
|
|
final now = DateTime.now();
|
|
final List<Map<String, dynamic>> monthlyData = [];
|
|
|
|
// 기본 구독 서비스와 금액 (현재 구독 중인 서비스)
|
|
final baseServices = [
|
|
{'serviceName': '넷플릭스', 'cost': 9900.0},
|
|
{'serviceName': '유튜브프리미엄', 'cost': 14900.0},
|
|
{'serviceName': '디즈니플러스', 'cost': 8900.0},
|
|
{'serviceName': '애플 iCloud', 'cost': 2900.0},
|
|
{'serviceName': '멜론', 'cost': 10900.0},
|
|
{'serviceName': '웨이브', 'cost': 7900.0},
|
|
{'serviceName': 'Netflix US', 'cost': 9.99}, // 달러 결제 서비스 추가
|
|
{'serviceName': 'Spotify Premium', 'cost': 10.99}, // 달러 결제 서비스 추가
|
|
{'serviceName': 'GitHub Pro', 'cost': 4.00}, // 달러 결제 서비스 추가
|
|
];
|
|
|
|
// Microsoft 365는 연간 구독이므로 월별 비용으로 환산 (1년에 1번만 결제)
|
|
const microsoftMonthlyCost = 12800.0 / 12;
|
|
|
|
// 최근 6개월 데이터 생성
|
|
for (int i = 0; i < 6; i++) {
|
|
// i개월 전 날짜
|
|
final targetMonth = DateTime(now.month - i <= 0 ? now.year - 1 : now.year,
|
|
now.month - i <= 0 ? now.month - i + 12 : now.month - i, 1);
|
|
|
|
// 해당 월의 모든 서비스 데이터 리스트
|
|
final List<Map<String, dynamic>> servicesForMonth = [];
|
|
|
|
// 해당 월의 총 지출
|
|
double totalExpense = 0.0;
|
|
|
|
// 기본 서비스 추가 (일부 서비스는 구독 시작 시점이 다를 수 있음)
|
|
for (final service in baseServices) {
|
|
// 3개월 전부터 웨이브 구독 시작
|
|
if (service['serviceName'] == '웨이브' && i > 3) {
|
|
continue;
|
|
}
|
|
|
|
// 2개월 전부터 디즈니플러스 구독 시작
|
|
if (service['serviceName'] == '디즈니플러스' && i > 2) {
|
|
continue;
|
|
}
|
|
|
|
// 서비스 정보 추가
|
|
final Map<String, dynamic> serviceData = {
|
|
'serviceName': service['serviceName'],
|
|
'cost': service['cost'],
|
|
'date':
|
|
'${targetMonth.year}-${targetMonth.month.toString().padLeft(2, '0')}-15',
|
|
};
|
|
|
|
servicesForMonth.add(serviceData);
|
|
totalExpense += service['cost'] as double;
|
|
}
|
|
|
|
// Microsoft 365는 정확히 4개월 전에 결제됨
|
|
if (i == 4) {
|
|
servicesForMonth.add({
|
|
'serviceName': 'Microsoft 365',
|
|
'cost': 12800.0, // 연간 결제 비용
|
|
'date':
|
|
'${targetMonth.year}-${targetMonth.month.toString().padLeft(2, '0')}-10',
|
|
});
|
|
totalExpense += 12800.0;
|
|
} else {
|
|
// 다른 달에는 월 환산 비용으로 계산
|
|
totalExpense += microsoftMonthlyCost;
|
|
}
|
|
|
|
// 3개월 전에는 Spotify를 일시적으로 구독했다가 해지
|
|
if (i == 3) {
|
|
servicesForMonth.add({
|
|
'serviceName': 'Spotify',
|
|
'cost': 10900.0,
|
|
'date':
|
|
'${targetMonth.year}-${targetMonth.month.toString().padLeft(2, '0')}-05',
|
|
});
|
|
totalExpense += 10900.0;
|
|
}
|
|
|
|
// 5개월 전과 4개월 전에는 쿠팡플레이를 구독했다가 해지
|
|
if (i == 4 || i == 5) {
|
|
servicesForMonth.add({
|
|
'serviceName': '쿠팡플레이',
|
|
'cost': 4990.0,
|
|
'date':
|
|
'${targetMonth.year}-${targetMonth.month.toString().padLeft(2, '0')}-18',
|
|
});
|
|
totalExpense += 4990.0;
|
|
}
|
|
|
|
// 월별 총 지출 데이터 추가
|
|
monthlyData.add({
|
|
'year': targetMonth.year,
|
|
'month': targetMonth.month,
|
|
'totalExpense': totalExpense,
|
|
'services': servicesForMonth,
|
|
'monthName': _getMonthName(targetMonth.month),
|
|
});
|
|
}
|
|
|
|
// 최신 달이 먼저 오도록 reverse
|
|
return monthlyData.reversed.toList();
|
|
}
|
|
|
|
// 월 숫자를 한글 월 이름으로 변환
|
|
static String _getMonthName(int month) {
|
|
const monthNames = [
|
|
'1월',
|
|
'2월',
|
|
'3월',
|
|
'4월',
|
|
'5월',
|
|
'6월',
|
|
'7월',
|
|
'8월',
|
|
'9월',
|
|
'10월',
|
|
'11월',
|
|
'12월'
|
|
];
|
|
return monthNames[month - 1];
|
|
}
|
|
}
|