feat: SubscriptionProvider 결제 금액 계산 로직 추가
This commit is contained in:
@@ -10,6 +10,7 @@ import '../services/currency_util.dart';
|
|||||||
import 'category_provider.dart';
|
import 'category_provider.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../navigator_key.dart';
|
import '../navigator_key.dart';
|
||||||
|
import '../utils/billing_cost_util.dart';
|
||||||
|
|
||||||
class SubscriptionProvider extends ChangeNotifier {
|
class SubscriptionProvider extends ChangeNotifier {
|
||||||
late Box<SubscriptionModel> _subscriptionBox;
|
late Box<SubscriptionModel> _subscriptionBox;
|
||||||
@@ -24,18 +25,40 @@ class SubscriptionProvider extends ChangeNotifier {
|
|||||||
final rate = exchangeRateService.cachedUsdToKrwRate ??
|
final rate = exchangeRateService.cachedUsdToKrwRate ??
|
||||||
ExchangeRateService.DEFAULT_USD_TO_KRW_RATE;
|
ExchangeRateService.DEFAULT_USD_TO_KRW_RATE;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final currentYear = now.year;
|
||||||
|
final currentMonth = now.month;
|
||||||
|
|
||||||
final total = _subscriptions.fold(
|
final total = _subscriptions.fold(
|
||||||
0.0,
|
0.0,
|
||||||
(sum, subscription) {
|
(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') {
|
if (subscription.currency == 'USD') {
|
||||||
debugPrint('[SubscriptionProvider] ${subscription.serviceName}: '
|
debugPrint('[SubscriptionProvider] ${subscription.serviceName}: '
|
||||||
'\$$price × ₩$rate = ₩${price * rate}');
|
'\$$actualPrice × ₩$rate = ₩${actualPrice * rate}');
|
||||||
return sum + (price * rate);
|
return sum + (actualPrice * rate);
|
||||||
}
|
}
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[SubscriptionProvider] ${subscription.serviceName}: ₩$price');
|
'[SubscriptionProvider] ${subscription.serviceName}: ₩$actualPrice');
|
||||||
return sum + price;
|
return sum + actualPrice;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,6 +99,9 @@ class SubscriptionProvider extends ChangeNotifier {
|
|||||||
// categoryId 마이그레이션
|
// categoryId 마이그레이션
|
||||||
await _migrateCategoryIds();
|
await _migrateCategoryIds();
|
||||||
|
|
||||||
|
// billingCycle별 비용 마이그레이션 (연간/분기별 구독 월 비용 변환)
|
||||||
|
await _migrateBillingCosts();
|
||||||
|
|
||||||
// 앱 시작 시 이벤트 상태 확인
|
// 앱 시작 시 이벤트 상태 확인
|
||||||
await checkAndUpdateEventStatus();
|
await checkAndUpdateEventStatus();
|
||||||
|
|
||||||
@@ -274,7 +300,9 @@ class SubscriptionProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 총 월간 지출을 계산합니다. (로케일별 기본 통화로 환산)
|
/// 이번 달 총 지출을 계산합니다. (로케일별 기본 통화로 환산)
|
||||||
|
/// - 이번 달에 결제가 발생하는 구독만 포함
|
||||||
|
/// - 실제 결제 금액으로 계산 (연간이면 연간 금액)
|
||||||
Future<double> calculateTotalExpense({
|
Future<double> calculateTotalExpense({
|
||||||
String? locale,
|
String? locale,
|
||||||
List<SubscriptionModel>? subset,
|
List<SubscriptionModel>? subset,
|
||||||
@@ -282,26 +310,50 @@ class SubscriptionProvider extends ChangeNotifier {
|
|||||||
final targetSubscriptions = subset ?? _subscriptions;
|
final targetSubscriptions = subset ?? _subscriptions;
|
||||||
if (targetSubscriptions.isEmpty) return 0.0;
|
if (targetSubscriptions.isEmpty) return 0.0;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final currentYear = now.year;
|
||||||
|
final currentMonth = now.month;
|
||||||
|
|
||||||
// locale이 제공되지 않으면 현재 로케일 사용
|
// locale이 제공되지 않으면 현재 로케일 사용
|
||||||
final targetCurrency =
|
final targetCurrency =
|
||||||
locale != null ? CurrencyUtil.getDefaultCurrency(locale) : 'KRW'; // 기본값
|
locale != null ? CurrencyUtil.getDefaultCurrency(locale) : 'KRW'; // 기본값
|
||||||
|
|
||||||
debugPrint('[calculateTotalExpense] 계산 시작 - 타겟 통화: $targetCurrency, '
|
debugPrint('[calculateTotalExpense] 계산 시작 - 타겟 통화: $targetCurrency, '
|
||||||
'대상 구독: ${targetSubscriptions.length}개');
|
'대상 구독: ${targetSubscriptions.length}개, 현재 월: $currentYear-$currentMonth');
|
||||||
double total = 0.0;
|
double total = 0.0;
|
||||||
|
|
||||||
for (final subscription in targetSubscriptions) {
|
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}: '
|
debugPrint('[calculateTotalExpense] ${subscription.serviceName}: '
|
||||||
'$currentPrice ${subscription.currency} (이벤트 적용: ${subscription.isCurrentlyInEvent})');
|
'실제 결제 금액 $actualPrice ${subscription.currency} '
|
||||||
|
'(월 비용: ${subscription.currentPrice}, 주기: ${subscription.billingCycle})');
|
||||||
|
|
||||||
final converted = await ExchangeRateService().convertBetweenCurrencies(
|
final converted = await ExchangeRateService().convertBetweenCurrencies(
|
||||||
currentPrice,
|
actualPrice,
|
||||||
subscription.currency,
|
subscription.currency,
|
||||||
targetCurrency,
|
targetCurrency,
|
||||||
);
|
);
|
||||||
|
|
||||||
total += converted ?? currentPrice;
|
total += converted ?? actualPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('[calculateTotalExpense] 총 지출 계산 완료: $total '
|
debugPrint('[calculateTotalExpense] 총 지출 계산 완료: $total '
|
||||||
@@ -310,6 +362,8 @@ class SubscriptionProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 최근 6개월의 월별 지출 데이터를 반환합니다. (로케일별 기본 통화로 환산)
|
/// 최근 6개월의 월별 지출 데이터를 반환합니다. (로케일별 기본 통화로 환산)
|
||||||
|
/// - 각 월에 결제가 발생하는 구독만 포함
|
||||||
|
/// - 실제 결제 금액으로 계산 (연간이면 연간 금액)
|
||||||
Future<List<Map<String, dynamic>>> getMonthlyExpenseData({
|
Future<List<Map<String, dynamic>>> getMonthlyExpenseData({
|
||||||
String? locale,
|
String? locale,
|
||||||
List<SubscriptionModel>? subset,
|
List<SubscriptionModel>? subset,
|
||||||
@@ -336,60 +390,63 @@ class SubscriptionProvider extends ChangeNotifier {
|
|||||||
'[getMonthlyExpenseData] 현재 월(${month.year}-${month.month}) 계산 중...');
|
'[getMonthlyExpenseData] 현재 월(${month.year}-${month.month}) 계산 중...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 해당 월에 활성화된 구독 계산
|
// 해당 월에 결제가 발생하는 구독만 계산
|
||||||
for (final subscription in targetSubscriptions) {
|
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) {
|
if (isCurrentMonth) {
|
||||||
// 현재 월인 경우: 모든 활성 구독 포함 (calculateTotalExpense와 동일하게)
|
|
||||||
final cost = subscription.currentPrice;
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[getMonthlyExpenseData] 현재 월 - ${subscription.serviceName}: '
|
'[getMonthlyExpenseData] 현재 월 - ${subscription.serviceName}: '
|
||||||
'$cost ${subscription.currency} (이벤트 적용: ${subscription.isCurrentlyInEvent})');
|
'실제 결제 금액 $actualCost ${subscription.currency}');
|
||||||
|
|
||||||
// 통화 변환
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 통화 변환
|
||||||
|
final converted =
|
||||||
|
await ExchangeRateService().convertBetweenCurrencies(
|
||||||
|
actualCost,
|
||||||
|
subscription.currency,
|
||||||
|
targetCurrency,
|
||||||
|
);
|
||||||
|
|
||||||
|
monthTotal += converted ?? actualCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCurrentMonth) {
|
if (isCurrentMonth) {
|
||||||
@@ -413,22 +470,6 @@ class SubscriptionProvider extends ChangeNotifier {
|
|||||||
return totalEventSavings;
|
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) {
|
String _getMonthLabel(DateTime month, String locale) {
|
||||||
if (locale == 'ko') {
|
if (locale == 'ko') {
|
||||||
@@ -557,4 +598,59 @@ class SubscriptionProvider extends ChangeNotifier {
|
|||||||
debugPrint('❎ 모든 구독이 이미 categoryId를 가지고 있습니다');
|
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('💰 모든 구독이 이미 월 비용으로 저장되어 있습니다');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user