import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:uuid/uuid.dart'; import '../models/subscription_model.dart'; import '../services/notification_service.dart'; import 'category_provider.dart'; class SubscriptionProvider extends ChangeNotifier { late Box _subscriptionBox; List _subscriptions = []; bool _isLoading = true; List get subscriptions => _subscriptions; bool get isLoading => _isLoading; double get totalMonthlyExpense { return _subscriptions.fold( 0.0, (sum, subscription) => sum + subscription.currentPrice, // 이벤트 가격 반영 ); } /// 월간 총 비용을 반환합니다. double getTotalMonthlyCost() { return totalMonthlyExpense; } /// 이벤트로 인한 총 절약액을 반환합니다. double get totalEventSavings { return _subscriptions.fold( 0.0, (sum, subscription) => sum + subscription.eventSavings, ); } /// 현재 이벤트 중인 구독 목록을 반환합니다. List get activeEventSubscriptions { return _subscriptions.where((sub) => sub.isCurrentlyInEvent).toList(); } Future init() async { try { _isLoading = true; notifyListeners(); _subscriptionBox = await Hive.openBox('subscriptions'); await refreshSubscriptions(); // categoryId 마이그레이션 await _migrateCategoryIds(); // 앱 시작 시 이벤트 상태 확인 await checkAndUpdateEventStatus(); _isLoading = false; notifyListeners(); } catch (e) { debugPrint('구독 초기화 중 오류 발생: $e'); _isLoading = false; notifyListeners(); rethrow; } } Future refreshSubscriptions() async { try { _subscriptions = _subscriptionBox.values.toList() ..sort((a, b) => a.nextBillingDate.compareTo(b.nextBillingDate)); notifyListeners(); } catch (e) { debugPrint('구독 목록 새로고침 중 오류 발생: $e'); rethrow; } } Future addSubscription({ required String serviceName, required double monthlyCost, required String billingCycle, required DateTime nextBillingDate, String? websiteUrl, String? categoryId, bool isAutoDetected = false, int repeatCount = 1, DateTime? lastPaymentDate, String currency = 'KRW', bool isEventActive = false, DateTime? eventStartDate, DateTime? eventEndDate, double? eventPrice, }) async { try { final subscription = SubscriptionModel( id: const Uuid().v4(), serviceName: serviceName, monthlyCost: monthlyCost, billingCycle: billingCycle, nextBillingDate: nextBillingDate, websiteUrl: websiteUrl, categoryId: categoryId, isAutoDetected: isAutoDetected, repeatCount: repeatCount, lastPaymentDate: lastPaymentDate, currency: currency, isEventActive: isEventActive, eventStartDate: eventStartDate, eventEndDate: eventEndDate, eventPrice: eventPrice, ); await _subscriptionBox.put(subscription.id, subscription); await refreshSubscriptions(); // 이벤트가 활성화된 경우 알림 스케줄 재설정 if (isEventActive && eventEndDate != null) { await _scheduleEventEndNotification(subscription); } } catch (e) { debugPrint('구독 추가 중 오류 발생: $e'); rethrow; } } Future updateSubscription(SubscriptionModel subscription) async { try { notifyListeners(); await _subscriptionBox.put(subscription.id, subscription); // 이벤트 관련 알림 업데이트 if (subscription.isEventActive && subscription.eventEndDate != null) { await _scheduleEventEndNotification(subscription); } else { // 이벤트가 비활성화된 경우 이벤트 종료 알림 취소 await NotificationService.cancelNotification( '${subscription.id}_event_end'.hashCode, ); } await refreshSubscriptions(); notifyListeners(); } catch (e) { debugPrint('구독 업데이트 중 오류 발생: $e'); rethrow; } } Future deleteSubscription(String id) async { try { await _subscriptionBox.delete(id); await refreshSubscriptions(); } catch (e) { debugPrint('구독 삭제 중 오류 발생: $e'); rethrow; } } Future clearAllSubscriptions() async { _isLoading = true; notifyListeners(); try { // 모든 알림 취소 for (var subscription in _subscriptions) { await NotificationService.cancelSubscriptionNotification(subscription); } // 모든 데이터 삭제 await _subscriptionBox.clear(); _subscriptions = []; notifyListeners(); } catch (e) { debugPrint('모든 구독 정보 삭제 중 오류 발생: $e'); rethrow; } finally { _isLoading = false; notifyListeners(); } } /// 이벤트 종료 알림을 스케줄링합니다. Future _scheduleEventEndNotification(SubscriptionModel subscription) async { if (subscription.eventEndDate != null && subscription.eventEndDate!.isAfter(DateTime.now())) { await NotificationService.scheduleNotification( id: '${subscription.id}_event_end'.hashCode, title: '이벤트 종료 알림', body: '${subscription.serviceName}의 할인 이벤트가 종료되었습니다.', scheduledDate: subscription.eventEndDate!, ); } } /// 모든 구독의 이벤트 상태를 확인하고 업데이트합니다. Future checkAndUpdateEventStatus() async { bool hasChanges = false; for (var subscription in _subscriptions) { // 이벤트가 종료되었지만 아직 활성화되어 있는 경우 if (subscription.isEventActive && subscription.eventEndDate != null && subscription.eventEndDate!.isBefore(DateTime.now())) { subscription.isEventActive = false; await _subscriptionBox.put(subscription.id, subscription); hasChanges = true; } } if (hasChanges) { await refreshSubscriptions(); } } /// 총 월간 지출을 계산합니다. Future calculateTotalExpense() async { // 이미 존재하는 totalMonthlyExpense getter를 사용 return totalMonthlyExpense; } /// 최근 6개월의 월별 지출 데이터를 반환합니다. Future>> getMonthlyExpenseData() async { final now = DateTime.now(); final List> monthlyData = []; // 최근 6개월 데이터 생성 for (int i = 5; i >= 0; i--) { final month = DateTime(now.year, now.month - i, 1); double monthTotal = 0.0; // 해당 월에 활성화된 구독 계산 for (final subscription in _subscriptions) { // 구독이 해당 월에 활성화되어 있었는지 확인 final subscriptionStartDate = subscription.nextBillingDate.subtract( Duration(days: _getBillingCycleDays(subscription.billingCycle)), ); if (subscriptionStartDate.isBefore(DateTime(month.year, month.month + 1, 1)) && subscription.nextBillingDate.isAfter(month)) { // 해당 월의 비용 계산 (이벤트 가격 고려) if (subscription.isEventActive && subscription.eventStartDate != null && subscription.eventEndDate != null && month.isAfter(subscription.eventStartDate!) && month.isBefore(subscription.eventEndDate!)) { monthTotal += subscription.eventPrice ?? subscription.monthlyCost; } else { monthTotal += subscription.monthlyCost; } } } monthlyData.add({ 'month': month, 'totalExpense': monthTotal, 'monthName': _getMonthLabel(month), }); } return monthlyData; } /// 이벤트로 인한 총 절약액을 계산합니다. double calculateTotalSavings() { // 이미 존재하는 totalEventSavings getter를 사용 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) { final months = [ '1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월' ]; return months[month.month - 1]; } /// categoryId가 없는 기존 구독들에 대해 자동으로 카테고리 할당 Future _migrateCategoryIds() async { debugPrint('❎ CategoryId 마이그레이션 시작...'); final categoryProvider = CategoryProvider(); await categoryProvider.init(); final categories = categoryProvider.categories; int migratedCount = 0; for (var subscription in _subscriptions) { if (subscription.categoryId == null) { final serviceName = subscription.serviceName.toLowerCase(); String? categoryId; debugPrint('🔍 ${subscription.serviceName} 카테고리 매칭 시도...'); // OTT 서비스 if (serviceName.contains('netflix') || serviceName.contains('youtube') || serviceName.contains('disney') || serviceName.contains('왓차') || serviceName.contains('티빙') || serviceName.contains('디즈니') || serviceName.contains('넷플릭스')) { categoryId = categories.firstWhere( (cat) => cat.name == 'OTT 서비스', orElse: () => categories.first, ).id; } // 음악 서비스 else if (serviceName.contains('spotify') || serviceName.contains('apple music') || serviceName.contains('멜론') || serviceName.contains('지니') || serviceName.contains('플로') || serviceName.contains('벡스')) { categoryId = categories.firstWhere( (cat) => cat.name == '음악 서비스', orElse: () => categories.first, ).id; } // AI 서비스 else if (serviceName.contains('chatgpt') || serviceName.contains('claude') || serviceName.contains('midjourney') || serviceName.contains('copilot')) { categoryId = categories.firstWhere( (cat) => cat.name == 'AI 서비스', orElse: () => categories.first, ).id; } // 프로그래밍/개발 else if (serviceName.contains('github') || serviceName.contains('intellij') || serviceName.contains('webstorm') || serviceName.contains('jetbrains')) { categoryId = categories.firstWhere( (cat) => cat.name == '프로그래밍/개발', orElse: () => categories.first, ).id; } // 오피스/협업 툴 else if (serviceName.contains('notion') || serviceName.contains('microsoft') || serviceName.contains('office') || serviceName.contains('slack') || serviceName.contains('figma') || serviceName.contains('icloud') || serviceName.contains('아이클라우드')) { categoryId = categories.firstWhere( (cat) => cat.name == '오피스/협업 툴', orElse: () => categories.first, ).id; } // 기타 서비스 (기본값) else { categoryId = categories.firstWhere( (cat) => cat.name == '기타 서비스', orElse: () => categories.first, ).id; } if (categoryId != null) { subscription.categoryId = categoryId; await subscription.save(); migratedCount++; final categoryName = categories.firstWhere((cat) => cat.id == categoryId).name; debugPrint('✅ ${subscription.serviceName} → $categoryName'); } } } if (migratedCount > 0) { debugPrint('❎ 총 ${migratedCount}개의 구독에 categoryId 할당 완료'); await refreshSubscriptions(); } else { debugPrint('❎ 모든 구독이 이미 categoryId를 가지고 있습니다'); } } }