feat: SubscriptionProvider 결제 금액 계산 로직 추가

This commit is contained in:
JiWoong Sul
2026-01-14 00:18:25 +09:00
parent 58c00443c1
commit a0b24f9a75

View File

@@ -10,6 +10,7 @@ import '../services/currency_util.dart';
import 'category_provider.dart';
import '../l10n/app_localizations.dart';
import '../navigator_key.dart';
import '../utils/billing_cost_util.dart';
class SubscriptionProvider extends ChangeNotifier {
late Box<SubscriptionModel> _subscriptionBox;
@@ -24,18 +25,40 @@ class SubscriptionProvider extends ChangeNotifier {
final rate = exchangeRateService.cachedUsdToKrwRate ??
ExchangeRateService.DEFAULT_USD_TO_KRW_RATE;
final now = DateTime.now();
final currentYear = now.year;
final currentMonth = now.month;
final total = _subscriptions.fold(
0.0,
(sum, subscription) {
final price = subscription.currentPrice;
// 이번 달에 결제가 발생하는지 확인
final hasBilling = BillingCostUtil.hasBillingInMonth(
subscription.nextBillingDate,
subscription.billingCycle,
currentYear,
currentMonth,
);
if (!hasBilling) {
debugPrint('[SubscriptionProvider] ${subscription.serviceName}: '
'이번 달 결제 없음, 제외');
return sum;
}
// 실제 결제 금액으로 역변환
final actualPrice = BillingCostUtil.convertFromMonthlyCost(
subscription.currentPrice,
subscription.billingCycle,
);
if (subscription.currency == 'USD') {
debugPrint('[SubscriptionProvider] ${subscription.serviceName}: '
'\$$price ×$rate = ₩${price * rate}');
return sum + (price * rate);
'\$$actualPrice ×$rate = ₩${actualPrice * rate}');
return sum + (actualPrice * rate);
}
debugPrint(
'[SubscriptionProvider] ${subscription.serviceName}: ₩$price');
return sum + price;
'[SubscriptionProvider] ${subscription.serviceName}: ₩$actualPrice');
return sum + actualPrice;
},
);
@@ -76,6 +99,9 @@ class SubscriptionProvider extends ChangeNotifier {
// categoryId 마이그레이션
await _migrateCategoryIds();
// billingCycle별 비용 마이그레이션 (연간/분기별 구독 월 비용 변환)
await _migrateBillingCosts();
// 앱 시작 시 이벤트 상태 확인
await checkAndUpdateEventStatus();
@@ -274,7 +300,9 @@ class SubscriptionProvider extends ChangeNotifier {
}
}
/// 총 월간 지출을 계산합니다. (로케일별 기본 통화로 환산)
/// 이번 달 총 지출을 계산합니다. (로케일별 기본 통화로 환산)
/// - 이번 달에 결제가 발생하는 구독만 포함
/// - 실제 결제 금액으로 계산 (연간이면 연간 금액)
Future<double> calculateTotalExpense({
String? locale,
List<SubscriptionModel>? subset,
@@ -282,26 +310,50 @@ class SubscriptionProvider extends ChangeNotifier {
final targetSubscriptions = subset ?? _subscriptions;
if (targetSubscriptions.isEmpty) return 0.0;
final now = DateTime.now();
final currentYear = now.year;
final currentMonth = now.month;
// locale이 제공되지 않으면 현재 로케일 사용
final targetCurrency =
locale != null ? CurrencyUtil.getDefaultCurrency(locale) : 'KRW'; // 기본값
debugPrint('[calculateTotalExpense] 계산 시작 - 타겟 통화: $targetCurrency, '
'대상 구독: ${targetSubscriptions.length}');
'대상 구독: ${targetSubscriptions.length}, 현재 월: $currentYear-$currentMonth');
double total = 0.0;
for (final subscription in targetSubscriptions) {
final currentPrice = subscription.currentPrice;
// 이번 달에 결제가 발생하는지 확인
final hasBilling = BillingCostUtil.hasBillingInMonth(
subscription.nextBillingDate,
subscription.billingCycle,
currentYear,
currentMonth,
);
if (!hasBilling) {
debugPrint('[calculateTotalExpense] ${subscription.serviceName}: '
'이번 달 결제 없음 - 제외');
continue;
}
// 실제 결제 금액으로 변환 (월 비용 → 실제 결제 금액)
final actualPrice = BillingCostUtil.convertFromMonthlyCost(
subscription.currentPrice,
subscription.billingCycle,
);
debugPrint('[calculateTotalExpense] ${subscription.serviceName}: '
'$currentPrice ${subscription.currency} (이벤트 적용: ${subscription.isCurrentlyInEvent})');
'실제 결제 금액 $actualPrice ${subscription.currency} '
'(월 비용: ${subscription.currentPrice}, 주기: ${subscription.billingCycle})');
final converted = await ExchangeRateService().convertBetweenCurrencies(
currentPrice,
actualPrice,
subscription.currency,
targetCurrency,
);
total += converted ?? currentPrice;
total += converted ?? actualPrice;
}
debugPrint('[calculateTotalExpense] 총 지출 계산 완료: $total '
@@ -310,6 +362,8 @@ class SubscriptionProvider extends ChangeNotifier {
}
/// 최근 6개월의 월별 지출 데이터를 반환합니다. (로케일별 기본 통화로 환산)
/// - 각 월에 결제가 발생하는 구독만 포함
/// - 실제 결제 금액으로 계산 (연간이면 연간 금액)
Future<List<Map<String, dynamic>>> getMonthlyExpenseData({
String? locale,
List<SubscriptionModel>? subset,
@@ -336,60 +390,63 @@ class SubscriptionProvider extends ChangeNotifier {
'[getMonthlyExpenseData] 현재 월(${month.year}-${month.month}) 계산 중...');
}
// 해당 월에 활성화된 구독 계산
// 해당 월에 결제가 발생하는 구독 계산
for (final subscription in targetSubscriptions) {
// 해당 월에 결제가 발생하는지 확인
final hasBilling = BillingCostUtil.hasBillingInMonth(
subscription.nextBillingDate,
subscription.billingCycle,
month.year,
month.month,
);
if (!hasBilling) {
continue; // 해당 월에 결제가 없으면 제외
}
// 실제 결제 금액으로 변환 (월 비용 → 실제 결제 금액)
double actualCost;
if (isCurrentMonth) {
// 현재 월: 이벤트 가격 반영
actualCost = BillingCostUtil.convertFromMonthlyCost(
subscription.currentPrice,
subscription.billingCycle,
);
} else {
// 과거 월: 이벤트 기간 확인 후 적용
double monthlyCost;
if (subscription.isEventActive &&
subscription.eventStartDate != null &&
subscription.eventEndDate != null &&
subscription.eventStartDate!
.isBefore(DateTime(month.year, month.month + 1, 1)) &&
subscription.eventEndDate!.isAfter(month)) {
monthlyCost = subscription.eventPrice ?? subscription.monthlyCost;
} else {
monthlyCost = subscription.monthlyCost;
}
actualCost = BillingCostUtil.convertFromMonthlyCost(
monthlyCost,
subscription.billingCycle,
);
}
if (isCurrentMonth) {
// 현재 월인 경우: 모든 활성 구독 포함 (calculateTotalExpense와 동일하게)
final cost = subscription.currentPrice;
debugPrint(
'[getMonthlyExpenseData] 현재 월 - ${subscription.serviceName}: '
'$cost ${subscription.currency} (이벤트 적용: ${subscription.isCurrentlyInEvent})');
// 통화 변환
final converted =
await ExchangeRateService().convertBetweenCurrencies(
cost,
subscription.currency,
targetCurrency,
);
monthTotal += converted ?? cost;
} else {
// 과거 월인 경우: 기존 로직 유지
// 구독이 해당 월에 활성화되어 있었는지 확인
final subscriptionStartDate = subscription.nextBillingDate.subtract(
Duration(days: _getBillingCycleDays(subscription.billingCycle)),
);
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 &&
// 이벤트 기간과 해당 월이 겹치는지 확인
subscription.eventStartDate!
.isBefore(DateTime(month.year, month.month + 1, 1)) &&
subscription.eventEndDate!.isAfter(month)) {
cost = subscription.eventPrice ?? subscription.monthlyCost;
} else {
cost = subscription.monthlyCost;
}
// 통화 변환
final converted =
await ExchangeRateService().convertBetweenCurrencies(
cost,
subscription.currency,
targetCurrency,
);
monthTotal += converted ?? cost;
}
'실제 결제 금액 $actualCost ${subscription.currency}');
}
// 통화 변환
final converted =
await ExchangeRateService().convertBetweenCurrencies(
actualCost,
subscription.currency,
targetCurrency,
);
monthTotal += converted ?? actualCost;
}
if (isCurrentMonth) {
@@ -413,22 +470,6 @@ class SubscriptionProvider extends ChangeNotifier {
return totalEventSavings;
}
/// 결제 주기를 일 단위로 변환합니다.
int _getBillingCycleDays(String billingCycle) {
switch (billingCycle) {
case 'monthly':
return 30;
case 'yearly':
return 365;
case 'weekly':
return 7;
case 'quarterly':
return 90;
default:
return 30;
}
}
/// 월 라벨을 생성합니다.
String _getMonthLabel(DateTime month, String locale) {
if (locale == 'ko') {
@@ -557,4 +598,59 @@ class SubscriptionProvider extends ChangeNotifier {
debugPrint('❎ 모든 구독이 이미 categoryId를 가지고 있습니다');
}
}
/// billingCycle별 비용 마이그레이션
/// 기존 연간/분기별 구독의 monthlyCost를 월 환산 비용으로 변환
Future<void> _migrateBillingCosts() async {
debugPrint('💰 BillingCost 마이그레이션 시작...');
int migratedCount = 0;
for (var subscription in _subscriptions) {
final cycle = subscription.billingCycle.toLowerCase();
// 월간 구독이 아닌 경우에만 변환 필요
if (cycle != 'monthly' && cycle != '월간' && cycle != '매월') {
// 현재 monthlyCost가 실제 월 비용인지 확인
// 연간 구독인데 monthlyCost가 12배 이상 크면 변환 안됨 상태로 판단
final multiplier = BillingCostUtil.getBillingCycleMultiplier(cycle);
// 변환이 필요한 경우: monthlyCost가 비정상적으로 큰 경우
// (예: 연간 129,000원이 monthlyCost에 그대로 저장된 경우)
if (multiplier > 1.5) {
// 원래 monthlyCost를 백업
final originalCost = subscription.monthlyCost;
// 월 비용으로 변환
final convertedCost = BillingCostUtil.convertToMonthlyCost(
originalCost,
cycle,
);
// 이벤트 가격도 있다면 변환
if (subscription.eventPrice != null) {
final convertedEventPrice = BillingCostUtil.convertToMonthlyCost(
subscription.eventPrice!,
cycle,
);
subscription.eventPrice = convertedEventPrice;
}
subscription.monthlyCost = convertedCost;
await subscription.save();
migratedCount++;
debugPrint('${subscription.serviceName} ($cycle): '
'${originalCost.toInt()} → ₩${convertedCost.toInt()}/월');
}
}
}
if (migratedCount > 0) {
debugPrint('💰 총 $migratedCount개의 구독 비용 변환 완료');
await refreshSubscriptions();
} else {
debugPrint('💰 모든 구독이 이미 월 비용으로 저장되어 있습니다');
}
}
}