feat: 다국어 지원 및 다중 통화 환율 변환 기능 확대

- ExchangeRateService에 JPY, CNY 환율 지원 추가
- 구독 서비스별 다국어 표시 이름 지원
- 분석 화면 차트 및 UI/UX 개선
- 설정 화면 전면 리팩토링
- SMS 스캔 기능 사용성 개선
- 전체 앱 다국어 번역 확대

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-07-16 17:34:32 +09:00
parent 4d1c0f5dab
commit 0f0b02bf08
55 changed files with 4100 additions and 1197 deletions

View File

@@ -5,6 +5,7 @@ import 'package:uuid/uuid.dart';
import '../models/subscription_model.dart';
import '../services/notification_service.dart';
import '../services/exchange_rate_service.dart';
import '../services/currency_util.dart';
import 'category_provider.dart';
class SubscriptionProvider extends ChangeNotifier {
@@ -20,16 +21,23 @@ class SubscriptionProvider extends ChangeNotifier {
final rate = exchangeRateService.cachedUsdToKrwRate ??
ExchangeRateService.DEFAULT_USD_TO_KRW_RATE;
return _subscriptions.fold(
final total = _subscriptions.fold(
0.0,
(sum, subscription) {
final price = subscription.currentPrice;
if (subscription.currency == 'USD') {
debugPrint('[SubscriptionProvider] ${subscription.serviceName}: '
'\$${price} ×$rate = ₩${price * rate}');
return sum + (price * rate);
}
debugPrint('[SubscriptionProvider] ${subscription.serviceName}: ₩$price');
return sum + price;
},
);
debugPrint('[SubscriptionProvider] totalMonthlyExpense 계산 완료: '
'${_subscriptions.length}개 구독, 총액 ₩$total');
return total;
}
/// 월간 총 비용을 반환합니다.
@@ -81,6 +89,11 @@ class SubscriptionProvider extends ChangeNotifier {
try {
_subscriptions = _subscriptionBox.values.toList()
..sort((a, b) => a.nextBillingDate.compareTo(b.nextBillingDate));
debugPrint('[SubscriptionProvider] refreshSubscriptions 완료: '
'${_subscriptions.length}개 구독, '
'총 월간 지출: ${totalMonthlyExpense.toStringAsFixed(2)}');
notifyListeners();
} catch (e) {
debugPrint('구독 목록 새로고침 중 오류 발생: $e');
@@ -138,9 +151,13 @@ class SubscriptionProvider extends ChangeNotifier {
Future<void> updateSubscription(SubscriptionModel subscription) async {
try {
notifyListeners();
debugPrint('[SubscriptionProvider] updateSubscription 호출됨: '
'${subscription.serviceName}, '
'금액: ${subscription.monthlyCost} ${subscription.currency}, '
'현재가격: ${subscription.currentPrice} ${subscription.currency}');
await _subscriptionBox.put(subscription.id, subscription);
debugPrint('[SubscriptionProvider] Hive에 저장 완료');
// 이벤트 관련 알림 업데이트
if (subscription.isEventActive && subscription.eventEndDate != null) {
@@ -154,6 +171,8 @@ class SubscriptionProvider extends ChangeNotifier {
await refreshSubscriptions();
debugPrint('[SubscriptionProvider] 구독 업데이트 완료, '
'현재 총 월간 지출: ${totalMonthlyExpense.toStringAsFixed(2)}');
notifyListeners();
} catch (e) {
debugPrint('구독 업데이트 중 오류 발생: $e');
@@ -230,17 +249,59 @@ class SubscriptionProvider extends ChangeNotifier {
}
}
/// 총 월간 지출을 계산합니다.
Future<double> calculateTotalExpense() async {
// 이미 존재하는 totalMonthlyExpense getter를 사용
return totalMonthlyExpense;
/// 총 월간 지출을 계산합니다. (로케일별 기본 통화로 환산)
Future<double> calculateTotalExpense({String? locale}) async {
if (_subscriptions.isEmpty) return 0.0;
// locale이 제공되지 않으면 현재 로케일 사용
final targetCurrency = locale != null
? CurrencyUtil.getDefaultCurrency(locale)
: 'KRW'; // 기본값
double total = 0.0;
for (final subscription in _subscriptions) {
final currentPrice = subscription.currentPrice;
if (subscription.currency == targetCurrency) {
// 이미 타겟 통화인 경우
total += currentPrice;
} else if (subscription.currency == 'USD') {
// USD를 타겟 통화로 변환
final converted = await ExchangeRateService().convertUsdToTarget(currentPrice, targetCurrency);
total += converted ?? currentPrice;
} else if (targetCurrency == 'USD') {
// 타겟이 USD인 경우 다른 통화를 USD로 변환
final converted = await ExchangeRateService().convertTargetToUsd(currentPrice, subscription.currency);
total += converted ?? currentPrice;
} else {
// USD를 거쳐서 변환 (예: KRW → USD → JPY)
// 1단계: 구독 통화를 USD로 변환
final usdAmount = await ExchangeRateService().convertTargetToUsd(currentPrice, subscription.currency);
if (usdAmount != null) {
// 2단계: USD를 타겟 통화로 변환
final converted = await ExchangeRateService().convertUsdToTarget(usdAmount, targetCurrency);
total += converted ?? currentPrice;
} else {
// 변환 실패 시 원래 값 사용
total += currentPrice;
}
}
}
return total;
}
/// 최근 6개월의 월별 지출 데이터를 반환합니다.
Future<List<Map<String, dynamic>>> getMonthlyExpenseData() async {
/// 최근 6개월의 월별 지출 데이터를 반환합니다. (로케일별 기본 통화로 환산)
Future<List<Map<String, dynamic>>> getMonthlyExpenseData({String? locale}) async {
final now = DateTime.now();
final List<Map<String, dynamic>> monthlyData = [];
// locale이 제공되지 않으면 현재 로케일 사용
final targetCurrency = locale != null
? CurrencyUtil.getDefaultCurrency(locale)
: 'KRW'; // 기본값
// 최근 6개월 데이터 생성
for (int i = 5; i >= 0; i--) {
final month = DateTime(now.year, now.month - i, 1);
@@ -256,14 +317,38 @@ class SubscriptionProvider extends ChangeNotifier {
if (subscriptionStartDate.isBefore(DateTime(month.year, month.month + 1, 1)) &&
subscription.nextBillingDate.isAfter(month)) {
// 해당 월의 비용 계산 (이벤트 가격 고려)
double cost;
if (subscription.isEventActive &&
subscription.eventStartDate != null &&
subscription.eventEndDate != null &&
month.isAfter(subscription.eventStartDate!) &&
month.isBefore(subscription.eventEndDate!)) {
monthTotal += subscription.eventPrice ?? subscription.monthlyCost;
cost = subscription.eventPrice ?? subscription.monthlyCost;
} else {
monthTotal += subscription.monthlyCost;
cost = subscription.monthlyCost;
}
// 통화 변환
if (subscription.currency == targetCurrency) {
monthTotal += cost;
} else if (subscription.currency == 'USD') {
final converted = await ExchangeRateService().convertUsdToTarget(cost, targetCurrency);
monthTotal += converted ?? cost;
} else if (targetCurrency == 'USD') {
final converted = await ExchangeRateService().convertTargetToUsd(cost, subscription.currency);
monthTotal += converted ?? cost;
} else {
// USD를 거쳐서 변환 (예: KRW → USD → JPY)
// 1단계: 구독 통화를 USD로 변환
final usdAmount = await ExchangeRateService().convertTargetToUsd(cost, subscription.currency);
if (usdAmount != null) {
// 2단계: USD를 타겟 통화로 변환
final converted = await ExchangeRateService().convertUsdToTarget(usdAmount, targetCurrency);
monthTotal += converted ?? cost;
} else {
// 변환 실패 시 원래 값 사용
monthTotal += cost;
}
}
}
}
@@ -347,7 +432,7 @@ class SubscriptionProvider extends ChangeNotifier {
serviceName.contains('플로') ||
serviceName.contains('벡스')) {
categoryId = categories.firstWhere(
(cat) => cat.name == '음악 서비스',
(cat) => cat.name == 'music',
orElse: () => categories.first,
).id;
}
@@ -357,7 +442,7 @@ class SubscriptionProvider extends ChangeNotifier {
serviceName.contains('midjourney') ||
serviceName.contains('copilot')) {
categoryId = categories.firstWhere(
(cat) => cat.name == 'AI 서비스',
(cat) => cat.name == 'aiService',
orElse: () => categories.first,
).id;
}
@@ -367,7 +452,7 @@ class SubscriptionProvider extends ChangeNotifier {
serviceName.contains('webstorm') ||
serviceName.contains('jetbrains')) {
categoryId = categories.firstWhere(
(cat) => cat.name == '프로그래밍/개발',
(cat) => cat.name == 'programming',
orElse: () => categories.first,
).id;
}
@@ -380,14 +465,14 @@ class SubscriptionProvider extends ChangeNotifier {
serviceName.contains('icloud') ||
serviceName.contains('아이클라우드')) {
categoryId = categories.firstWhere(
(cat) => cat.name == '오피스/협업 툴',
(cat) => cat.name == 'collaborationOffice',
orElse: () => categories.first,
).id;
}
// 기타 서비스 (기본값)
else {
categoryId = categories.firstWhere(
(cat) => cat.name == '기타 서비스',
(cat) => cat.name == 'other',
orElse: () => categories.first,
).id;
}