import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../l10n/app_localizations.dart'; import '../models/payment_card_model.dart'; import '../models/subscription_model.dart'; import '../providers/payment_card_provider.dart'; import '../providers/subscription_provider.dart'; import '../providers/locale_provider.dart'; import '../utils/payment_card_utils.dart'; import '../widgets/native_ad_widget.dart'; import '../widgets/analysis/analysis_screen_spacer.dart'; import '../widgets/analysis/subscription_pie_chart_card.dart'; import '../widgets/analysis/total_expense_summary_card.dart'; import '../widgets/analysis/monthly_expense_chart_card.dart'; import '../widgets/analysis/event_analysis_card.dart'; import '../theme/ui_constants.dart'; enum AnalysisCardFilterType { all, unassigned, card } class AnalysisScreen extends StatefulWidget { const AnalysisScreen({super.key}); @override State createState() => _AnalysisScreenState(); } class _AnalysisScreenState extends State with TickerProviderStateMixin { late AnimationController _animationController; late ScrollController _scrollController; double _totalExpense = 0; List> _monthlyData = []; bool _isLoading = true; String _lastDataHash = ''; AnalysisCardFilterType _filterType = AnalysisCardFilterType.all; String? _selectedCardId; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, ); _scrollController = ScrollController(); _loadData(); } @override void didChangeDependencies() { super.didChangeDependencies(); // Provider 변경 감지 final provider = Provider.of(context); final filtered = _filterSubscriptions(provider.subscriptions); final currentHash = _calculateDataHash(provider, filtered: filtered); debugPrint('[AnalysisScreen] didChangeDependencies: ' '현재 해시=$currentHash, 이전 해시=$_lastDataHash, 로딩중=$_isLoading'); // 데이터가 변경되었고 현재 로딩 중이 아닌 경우에만 리로드 if (currentHash != _lastDataHash && !_isLoading && _lastDataHash.isNotEmpty) { debugPrint('[AnalysisScreen] 데이터 변경 감지됨, 리로드 시작'); _loadData(); } } @override void dispose() { _animationController.dispose(); _scrollController.dispose(); super.dispose(); } /// 구독 데이터의 해시값을 계산하여 변경 감지 String _calculateDataHash( SubscriptionProvider provider, { List? filtered, }) { final subscriptions = filtered ?? _filterSubscriptions(provider.subscriptions); final buffer = StringBuffer() ..write(_filterType.name) ..write('_${_selectedCardId ?? 'all'}') ..write('_${subscriptions.length}'); for (final sub in subscriptions) { buffer.write( '_${sub.id}_${sub.currentPrice.toStringAsFixed(2)}_${sub.currency}'); } return buffer.toString(); } List _filterSubscriptions( List subscriptions) { switch (_filterType) { case AnalysisCardFilterType.all: return subscriptions; case AnalysisCardFilterType.unassigned: return subscriptions.where((sub) => sub.paymentCardId == null).toList(); case AnalysisCardFilterType.card: final cardId = _selectedCardId; if (cardId == null) return subscriptions; return subscriptions .where((sub) => sub.paymentCardId == cardId) .toList(); } } Future _onFilterChanged(AnalysisCardFilterType type, {String? cardId}) async { if (_filterType == type) { if (type != AnalysisCardFilterType.card || _selectedCardId == cardId) { return; } } setState(() { _filterType = type; _selectedCardId = type == AnalysisCardFilterType.card ? cardId : null; }); await _loadData(); } Future _loadData() async { debugPrint('[AnalysisScreen] _loadData 호출됨'); setState(() { _isLoading = true; }); final provider = Provider.of(context, listen: false); final localeProvider = Provider.of(context, listen: false); final locale = localeProvider.locale.languageCode; final filteredSubscriptions = _filterSubscriptions(provider.subscriptions); // 총 지출 계산 (로케일별 기본 통화로 환산) _totalExpense = await provider.calculateTotalExpense( locale: locale, subset: filteredSubscriptions, ); debugPrint('[AnalysisScreen] 총 지출 계산 완료: $_totalExpense'); // 월별 데이터 계산 (로케일별 기본 통화로 환산) _monthlyData = await provider.getMonthlyExpenseData( locale: locale, subset: filteredSubscriptions, ); debugPrint('[AnalysisScreen] 월별 데이터 계산 완료: ${_monthlyData.length}개월'); // 현재 데이터 해시값 저장 _lastDataHash = _calculateDataHash(provider, filtered: filteredSubscriptions); debugPrint('[AnalysisScreen] 데이터 해시값 저장: $_lastDataHash'); setState(() { _isLoading = false; }); // 데이터 로드 완료 후 애니메이션 시작 _animationController.reset(); _animationController.forward(); } Widget _buildAnimatedAd() { return FadeTransition( opacity: CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.5, curve: Curves.easeOut), ), child: SlideTransition( position: Tween( begin: const Offset(0, 0.2), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.5, curve: Curves.easeOut), )), child: const NativeAdWidget(key: ValueKey('analysis_ad')), ), ); } Widget _buildCardFilterSection( BuildContext context, PaymentCardProvider cardProvider) { final loc = AppLocalizations.of(context); final chips = [ _buildGenericFilterChip( context: context, label: loc.analysisCardFilterAll, icon: Icons.credit_card, selected: _filterType == AnalysisCardFilterType.all, onTap: () => _onFilterChanged(AnalysisCardFilterType.all), ), const SizedBox(width: 8), _buildGenericFilterChip( context: context, label: loc.paymentCardUnassigned, icon: Icons.credit_card_off_rounded, selected: _filterType == AnalysisCardFilterType.unassigned, onTap: () => _onFilterChanged(AnalysisCardFilterType.unassigned), ), ]; for (final card in cardProvider.cards) { chips.add(const SizedBox(width: 8)); chips.add(_buildPaymentCardChip(context, card)); } return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( loc.analysisCardFilterLabel, style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row(children: chips), ), ], ), ), ); } Widget _buildGenericFilterChip({ required BuildContext context, required String label, required IconData icon, required bool selected, required VoidCallback onTap, }) { final cs = Theme.of(context).colorScheme; return Semantics( selected: selected, button: true, label: label, child: ChoiceChip( label: Text(label), avatar: Icon( icon, size: 16, color: selected ? cs.onPrimary : cs.onSurfaceVariant, ), selected: selected, onSelected: (_) => onTap(), selectedColor: cs.primary, labelStyle: TextStyle( color: selected ? cs.onPrimary : cs.onSurface, fontWeight: FontWeight.w600, ), backgroundColor: cs.surface, side: BorderSide( color: selected ? Colors.transparent : cs.outline.withValues(alpha: 0.5), ), ), ); } Widget _buildPaymentCardChip(BuildContext context, PaymentCardModel card) { final color = PaymentCardUtils.colorFromHex(card.colorHex); final icon = PaymentCardUtils.iconForName(card.iconName); final cs = Theme.of(context).colorScheme; final selected = _filterType == AnalysisCardFilterType.card && _selectedCardId == card.id; final labelText = '${card.issuerName} · ****${card.last4}'; return Semantics( label: labelText, selected: selected, button: true, child: ChoiceChip( avatar: CircleAvatar( backgroundColor: selected ? cs.onPrimary : color.withValues(alpha: 0.15), child: Icon( icon, size: 16, color: selected ? color : cs.onSurface, ), ), label: Text(labelText), selected: selected, onSelected: (_) => _onFilterChanged(AnalysisCardFilterType.card, cardId: card.id), selectedColor: color, backgroundColor: cs.surface, labelStyle: TextStyle( color: selected ? cs.onPrimary : cs.onSurface, fontWeight: FontWeight.w600, ), side: BorderSide( color: selected ? Colors.transparent : color.withValues(alpha: 0.5), ), ), ); } @override Widget build(BuildContext context) { // Provider를 직접 사용하여 변경 감지 final provider = Provider.of(context); final subscriptions = provider.subscriptions; if (_isLoading) { return const Center( child: CircularProgressIndicator(), ); } final cardProvider = Provider.of(context); final filteredSubscriptions = _filterSubscriptions(subscriptions); return CustomScrollView( controller: _scrollController, physics: const BouncingScrollPhysics(), slivers: [ SliverPadding( padding: const EdgeInsets.only(top: UIConstants.pageTopPadding), sliver: _buildCardFilterSection(context, cardProvider), ), const AnalysisScreenSpacer(), // 1. 구독 비율 파이 차트 SubscriptionPieChartCard( subscriptions: filteredSubscriptions, animationController: _animationController, ), const AnalysisScreenSpacer(), // 네이티브 광고 위젯 (구독 비율 차트 하단) SliverToBoxAdapter( child: _buildAnimatedAd(), ), const AnalysisScreenSpacer(), // 2. 총 지출 요약 카드 TotalExpenseSummaryCard( key: ValueKey('total_expense_$_lastDataHash'), subscriptions: filteredSubscriptions, totalExpense: _totalExpense, animationController: _animationController, ), const AnalysisScreenSpacer(), // 3. 월별 지출 차트 MonthlyExpenseChartCard( key: ValueKey('monthly_expense_$_lastDataHash'), monthlyData: _monthlyData, animationController: _animationController, ), const AnalysisScreenSpacer(), // 4. 이벤트 분석 EventAnalysisCard( animationController: _animationController, subscriptions: filteredSubscriptions, ), // FloatingNavigationBar를 위한 충분한 하단 여백 SliverToBoxAdapter( child: SizedBox( height: 120 + MediaQuery.of(context).padding.bottom, ), ), ], ); } }