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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user