feat: 결제 금액 계산 유틸리티 추가

This commit is contained in:
JiWoong Sul
2026-01-14 00:18:12 +09:00
parent 0f92206833
commit da530a99b7
2 changed files with 303 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
import 'package:intl/intl.dart';
import '../models/subscription_model.dart';
import '../utils/billing_cost_util.dart';
import 'exchange_rate_service.dart';
import 'cache_manager.dart';
@@ -129,7 +130,8 @@ class CurrencyUtil {
return result;
}
/// 구독 목록의 총 비용을 계산 (언어별 기본 통화로)
/// 구독 목록의 이번 달 총 비용을 계산 (언어별 기본 통화로)
/// 이번 달에 결제가 발생하는 구독만 포함하며, 실제 결제 금액을 사용
static Future<double> calculateTotalMonthlyExpenseInDefaultCurrency(
List<SubscriptionModel> subscriptions,
String locale,
@@ -137,16 +139,33 @@ class CurrencyUtil {
final defaultCurrency = getDefaultCurrency(locale);
double total = 0.0;
final now = DateTime.now();
final currentYear = now.year;
final currentMonth = now.month;
for (var subscription in subscriptions) {
final price = subscription.currentPrice;
// 이번 달에 결제가 발생하는지 확인
final hasBilling = BillingCostUtil.hasBillingInMonth(
subscription.nextBillingDate,
subscription.billingCycle,
currentYear,
currentMonth,
);
if (!hasBilling) continue;
// 실제 결제 금액으로 역변환
final actualPrice = BillingCostUtil.convertFromMonthlyCost(
subscription.currentPrice,
subscription.billingCycle,
);
final converted = await _exchangeRateService.convertBetweenCurrencies(
price,
actualPrice,
subscription.currency,
defaultCurrency,
);
total += converted ?? price;
total += converted ?? actualPrice;
}
return total;
@@ -158,17 +177,46 @@ class CurrencyUtil {
return calculateTotalMonthlyExpenseInDefaultCurrency(subscriptions, 'ko');
}
/// 구독 목록의 예상 연간 총 비용을 계산 (언어별 기본 통화로)
/// 모든 구독의 연간 비용을 합산 (월 환산 비용 × 12)
static Future<double> calculateTotalAnnualExpenseInDefaultCurrency(
List<SubscriptionModel> subscriptions,
String locale,
) async {
final defaultCurrency = getDefaultCurrency(locale);
double total = 0.0;
for (var subscription in subscriptions) {
// 월 환산 비용 × 12 = 연간 비용
final annualPrice = subscription.currentPrice * 12;
final converted = await _exchangeRateService.convertBetweenCurrencies(
annualPrice,
subscription.currency,
defaultCurrency,
);
total += converted ?? annualPrice;
}
return total;
}
/// 구독의 월 비용을 표시 형식에 맞게 변환 (언어별 통화)
static Future<String> formatSubscriptionAmountWithLocale(
SubscriptionModel subscription, String locale) async {
final price = subscription.currentPrice;
// 구독 단위 캐시 키 (통화/가격/locale + id)
// 월 환산 금액을 실제 결제 금액으로 역변환
final price = BillingCostUtil.convertFromMonthlyCost(
subscription.currentPrice,
subscription.billingCycle,
);
// 구독 단위 캐시 키 (통화/가격/locale + id + billingCycle)
final decimals =
(subscription.currency == 'KRW' || subscription.currency == 'JPY')
? 0
: 2;
final key =
'subfmt:$locale:${subscription.currency}:${price.toStringAsFixed(decimals)}:${subscription.id}';
'subfmt:$locale:${subscription.currency}:${price.toStringAsFixed(decimals)}:${subscription.id}:${subscription.billingCycle}';
final cached = _fmtCache.get(key);
if (cached != null) return cached;

View File

@@ -0,0 +1,248 @@
/// 결제 주기에 따른 비용 변환 유틸리티
class BillingCostUtil {
/// 결제 주기별 비용을 월 비용으로 변환
///
/// [amount]: 입력된 비용
/// [billingCycle]: 결제 주기 ('monthly', 'yearly', 'quarterly', 'half-yearly' 등)
///
/// Returns: 월 환산 비용
static double convertToMonthlyCost(double amount, String billingCycle) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
switch (normalizedCycle) {
case 'monthly':
return amount;
case 'yearly':
return amount / 12;
case 'quarterly':
return amount / 3;
case 'half-yearly':
return amount / 6;
case 'weekly':
return amount * 4.33; // 평균 주당 4.33주
default:
return amount; // 알 수 없는 주기는 그대로 반환
}
}
/// 월 비용을 결제 주기별 비용으로 역변환
///
/// [monthlyCost]: 월 비용
/// [billingCycle]: 결제 주기
///
/// Returns: 해당 주기의 실제 결제 금액
static double convertFromMonthlyCost(double monthlyCost, String billingCycle) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
switch (normalizedCycle) {
case 'monthly':
return monthlyCost;
case 'yearly':
return monthlyCost * 12;
case 'quarterly':
return monthlyCost * 3;
case 'half-yearly':
return monthlyCost * 6;
case 'weekly':
return monthlyCost / 4.33;
default:
return monthlyCost;
}
}
/// 결제 주기를 정규화된 영어 키값으로 변환
static String _normalizeBillingCycle(String cycle) {
switch (cycle.toLowerCase()) {
case 'monthly':
case '월간':
case '매월':
case '月間':
case '月付':
case '每月':
case '毎月':
return 'monthly';
case 'yearly':
case 'annual':
case 'annually':
case '연간':
case '매년':
case '年間':
case '年付':
case '每年':
return 'yearly';
case 'quarterly':
case 'quarter':
case '분기별':
case '분기':
case '季付':
case '季度付':
case '四半期':
case '每季度':
return 'quarterly';
case 'half-yearly':
case 'half yearly':
case 'semiannual':
case 'semi-annual':
case '반기별':
case '半年付':
case '半年払い':
case '半年ごと':
case '每半年':
return 'half-yearly';
case 'weekly':
case '주간':
case '週間':
case '周付':
case '每周':
return 'weekly';
default:
return 'monthly';
}
}
/// 결제 주기의 배수 반환 (월 기준)
///
/// 예: yearly = 12, quarterly = 3
static double getBillingCycleMultiplier(String billingCycle) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
switch (normalizedCycle) {
case 'monthly':
return 1.0;
case 'yearly':
return 12.0;
case 'quarterly':
return 3.0;
case 'half-yearly':
return 6.0;
case 'weekly':
return 1 / 4.33;
default:
return 1.0;
}
}
/// 다음 결제일에서 이전 결제일 계산
///
/// [nextBillingDate]: 다음 결제 예정일
/// [billingCycle]: 결제 주기
///
/// Returns: 이전 결제일 (마지막으로 결제가 발생한 날짜)
static DateTime getLastBillingDate(
DateTime nextBillingDate, String billingCycle) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
switch (normalizedCycle) {
case 'yearly':
return DateTime(
nextBillingDate.year - 1,
nextBillingDate.month,
nextBillingDate.day,
);
case 'half-yearly':
return DateTime(
nextBillingDate.month <= 6
? nextBillingDate.year - 1
: nextBillingDate.year,
nextBillingDate.month <= 6
? nextBillingDate.month + 6
: nextBillingDate.month - 6,
nextBillingDate.day,
);
case 'quarterly':
return DateTime(
nextBillingDate.month <= 3
? nextBillingDate.year - 1
: nextBillingDate.year,
nextBillingDate.month <= 3
? nextBillingDate.month + 9
: nextBillingDate.month - 3,
nextBillingDate.day,
);
case 'monthly':
return DateTime(
nextBillingDate.month == 1
? nextBillingDate.year - 1
: nextBillingDate.year,
nextBillingDate.month == 1 ? 12 : nextBillingDate.month - 1,
nextBillingDate.day,
);
case 'weekly':
return nextBillingDate.subtract(const Duration(days: 7));
default:
return DateTime(
nextBillingDate.month == 1
? nextBillingDate.year - 1
: nextBillingDate.year,
nextBillingDate.month == 1 ? 12 : nextBillingDate.month - 1,
nextBillingDate.day,
);
}
}
/// 특정 월에 결제가 발생하는지 확인
///
/// [nextBillingDate]: 다음 결제 예정일
/// [billingCycle]: 결제 주기
/// [targetYear]: 확인할 연도
/// [targetMonth]: 확인할 월 (1-12)
///
/// Returns: 해당 월에 결제가 발생하면 true
static bool hasBillingInMonth(
DateTime nextBillingDate,
String billingCycle,
int targetYear,
int targetMonth,
) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
// 주간 결제는 매주 발생하므로 항상 true
if (normalizedCycle == 'weekly') {
return true;
}
// 월간 결제는 매월 발생하므로 항상 true
if (normalizedCycle == 'monthly') {
return true;
}
// 결제 주기에 따른 개월 수
final cycleMonths = _getCycleMonths(normalizedCycle);
// 결제 발생 월 계산 (nextBillingDate 기준으로 역산)
final billingMonth = nextBillingDate.month;
// 대상 월이 결제 발생 월과 일치하는지 확인
// 예: 연간 결제(1월), targetMonth = 1 → true
// 예: 연간 결제(1월), targetMonth = 2 → false
for (int i = 0; i < 12; i += cycleMonths) {
final checkMonth = ((billingMonth - 1 + i) % 12) + 1;
if (checkMonth == targetMonth) {
return true;
}
}
return false;
}
/// 결제 주기별 개월 수 반환
static int _getCycleMonths(String normalizedCycle) {
switch (normalizedCycle) {
case 'yearly':
return 12;
case 'half-yearly':
return 6;
case 'quarterly':
return 3;
case 'monthly':
return 1;
default:
return 1;
}
}
}