import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:fl_chart/fl_chart.dart'; import 'dart:math' as math; import '../providers/subscription_provider.dart'; import '../models/subscription_model.dart'; import '../temp/test_sms_data.dart'; import '../services/currency_util.dart'; import '../services/exchange_rate_service.dart'; import '../widgets/native_ad_widget.dart'; class AnalysisScreen extends StatefulWidget { const AnalysisScreen({super.key}); @override State createState() => _AnalysisScreenState(); } class _AnalysisScreenState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late ScrollController _scrollController; double _scrollOffset = 0; int _touchedIndex = -1; // 최근 6개월 데이터 late List> _monthlyData; // 총 지출액 (원화 환산) double _totalExpense = 0.0; // 로딩 상태 bool _isLoading = true; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), // 애니메이션 속도 조정 )..forward(); _scrollController = ScrollController() ..addListener(() { setState(() { _scrollOffset = _scrollController.offset; }); }); // 월간 지출 데이터 초기화 _monthlyData = TestSmsData.getMonthlyExpenseData(); } @override void didChangeDependencies() { super.didChangeDependencies(); _calculateTotalExpense(); } // 총 지출 금액 계산 (USD는 원화로 환산) Future _calculateTotalExpense() async { setState(() => _isLoading = true); try { final provider = Provider.of(context, listen: false); final subscriptions = provider.subscriptions; if (subscriptions.isEmpty) { setState(() { _totalExpense = 0.0; _isLoading = false; }); return; } // 모든 구독의 월 비용을 원화로 환산하여 계산 final total = await CurrencyUtil.calculateTotalMonthlyExpense(subscriptions); setState(() { _totalExpense = total; _isLoading = false; }); } catch (e) { debugPrint('총 지출 계산 오류: $e'); setState(() => _isLoading = false); } } @override void dispose() { _animationController.dispose(); _scrollController.dispose(); super.dispose(); } // 총 지출 금액 바 차트 데이터 List _getBarGroups(List subscriptions) { return [ BarChartGroupData( x: 0, barRods: [ BarChartRodData( toY: _totalExpense, gradient: const LinearGradient( colors: [Color(0xFF3B82F6), Color(0xFF60A5FA)], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), width: 20, borderRadius: BorderRadius.circular(4), backDrawRodData: BackgroundBarChartRodData( show: true, toY: _totalExpense + (_totalExpense * 0.2), color: Colors.grey.withOpacity(0.1), ), ), ], ), ]; } // 파이 차트 섹션 데이터 List _getPieSections( List subscriptions) { if (subscriptions.isEmpty) return []; final colors = [ const Color(0xFF3B82F6), const Color(0xFF10B981), const Color(0xFFF59E0B), const Color(0xFFEF4444), const Color(0xFF8B5CF6), const Color(0xFF0EA5E9), const Color(0xFFEC4899), ]; // 개별 구독의 비율 계산을 위한 값들 List sectionValues = []; // 각 구독의 원화 환산 금액 또는 원화 금액을 계산 for (var subscription in subscriptions) { double value = subscription.monthlyCost; if (subscription.currency == 'USD') { // USD의 경우 마지막으로 조회된 환율로 대략적인 계산 // (정확한 계산은 비동기로 이루어지므로 UI 표시용으로만 사용) const rate = 1350.0; // 기본 환율 (실제 값은 API로 별도로 가져옴) value = value * rate; } sectionValues.add(value); } // 총합 계산 double sectionsTotal = sectionValues.fold(0, (sum, value) => sum + value); // 섹션 데이터 생성 return List.generate(subscriptions.length, (i) { final subscription = subscriptions[i]; final percentage = (sectionValues[i] / sectionsTotal) * 100; final index = i % colors.length; final isTouched = _touchedIndex == i; final fontSize = isTouched ? 16.0 : 12.0; final radius = isTouched ? 105.0 : 100.0; return PieChartSectionData( value: sectionValues[i], title: '${percentage.toStringAsFixed(1)}%', titleStyle: TextStyle( fontSize: fontSize, fontWeight: FontWeight.bold, color: Colors.white, shadows: const [ Shadow(color: Colors.black26, blurRadius: 2, offset: Offset(0, 1)) ], ), color: colors[index], radius: radius, titlePositionPercentageOffset: 0.6, badgeWidget: isTouched ? _Badge( size: 40, borderColor: colors[index], subscription: subscription, ) : null, badgePositionPercentageOffset: .98, ); }); } // 월간 지출 차트 데이터 List _getMonthlyBarGroups() { final List barGroups = []; final maxAmount = _monthlyData.fold( 0, (max, data) => math.max(max, data['totalExpense'] as double)); for (int i = 0; i < _monthlyData.length; i++) { final data = _monthlyData[i]; barGroups.add( BarChartGroupData( x: i, barRods: [ BarChartRodData( toY: data['totalExpense'], gradient: LinearGradient( colors: [ const Color(0xFF3B82F6).withOpacity(0.7), const Color(0xFF60A5FA), ], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), width: 18, borderRadius: BorderRadius.circular(4), backDrawRodData: BackgroundBarChartRodData( show: true, toY: maxAmount + (maxAmount * 0.1), color: Colors.grey.withOpacity(0.1), ), ), ], ), ); } return barGroups; } @override Widget build(BuildContext context) { final double appBarOpacity = math.max(0, math.min(1, _scrollOffset / 100)); return Scaffold( backgroundColor: const Color(0xFFF8FAFC), extendBodyBehindAppBar: true, appBar: PreferredSize( preferredSize: const Size.fromHeight(60), child: Container( decoration: BoxDecoration( color: Colors.white.withOpacity(appBarOpacity), boxShadow: appBarOpacity > 0.6 ? [ BoxShadow( color: Colors.black.withOpacity(0.1 * appBarOpacity), spreadRadius: 1, blurRadius: 8, offset: const Offset(0, 4), ) ] : null, ), child: SafeArea( child: AppBar( title: Text( '지출 분석', style: TextStyle( fontFamily: 'Montserrat', fontSize: 24, fontWeight: FontWeight.w800, letterSpacing: -0.5, color: const Color(0xFF1E293B), shadows: appBarOpacity > 0.6 ? [ Shadow( color: Colors.black.withOpacity(0.2), offset: const Offset(0, 1), blurRadius: 2, ) ] : null, ), ), elevation: 0, backgroundColor: Colors.transparent, ), ), ), ), body: Consumer( builder: (context, provider, child) { final subscriptions = provider.subscriptions; if (_isLoading) { return const Center( child: CircularProgressIndicator(), ); } return SingleChildScrollView( controller: _scrollController, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: MediaQuery.of(context).padding.top + 60), // 네이티브 광고 위젯 추가 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')), ), ), const SizedBox(height: 24), // 1. 구독 비율 파이 차트 (처음으로 위치 변경) FadeTransition( opacity: CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.7, 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.7, curve: Curves.easeOut), )), child: Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '구독 서비스 비율', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1E293B), ), ), FutureBuilder( future: CurrencyUtil.getExchangeRateInfo(), builder: (context, snapshot) { if (snapshot.hasData && snapshot.data!.isNotEmpty) { return Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFFE5F2FF), borderRadius: BorderRadius.circular(4), border: Border.all( color: const Color(0xFFBFDBFE), width: 1, ), ), child: Text( snapshot.data!, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF3B82F6), ), ), ); } return const SizedBox.shrink(); }, ), ], ), const SizedBox(height: 8), Text( '월 지출 기준', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), const SizedBox(height: 16), Center( child: subscriptions.isEmpty ? const SizedBox( height: 250, child: Center( child: Text( '구독중인 서비스가 없습니다', style: TextStyle( fontSize: 16, color: Colors.grey, ), ), ), ) : SizedBox( height: 250, child: PieChart( PieChartData( borderData: FlBorderData(show: false), sectionsSpace: 2, centerSpaceRadius: 60, sections: _getPieSections(subscriptions), pieTouchData: PieTouchData( touchCallback: (FlTouchEvent event, pieTouchResponse) { setState(() { if (!event .isInterestedForInteractions || pieTouchResponse == null || pieTouchResponse .touchedSection == null) { _touchedIndex = -1; return; } _touchedIndex = pieTouchResponse .touchedSection! .touchedSectionIndex; }); }, ), ), ), ), ), const SizedBox(height: 16), // 서비스 목록 (비동기 처리로 수정) Column( children: subscriptions.isEmpty ? [] : List.generate( subscriptions.length, (index) { final subscription = subscriptions[index]; final color = [ const Color(0xFF3B82F6), const Color(0xFF10B981), const Color(0xFFF59E0B), const Color(0xFFEF4444), const Color(0xFF8B5CF6), const Color(0xFF0EA5E9), const Color(0xFFEC4899), ][index % 7]; return Padding( padding: const EdgeInsets.only( bottom: 4.0), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 8), Expanded( child: Text( subscription.serviceName, style: const TextStyle( fontSize: 14, ), overflow: TextOverflow.ellipsis, ), ), FutureBuilder( future: CurrencyUtil .formatSubscriptionAmount( subscription), builder: (context, snapshot) { if (snapshot.hasData) { return Text( snapshot.data!, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), ); } return const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, ), ); }, ), ], ), ); }, ), ), ], ), ), ), ), ), const SizedBox(height: 24), // 2. 총 지출 요약 카드 FadeTransition( opacity: CurvedAnimation( parent: _animationController, curve: const Interval(0.2, 0.8, curve: Curves.easeOut), ), child: SlideTransition( position: Tween( begin: const Offset(0, 0.2), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.2, 0.8, curve: Curves.easeOut), )), child: Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 헤더 텍스트 const Text( '총 지출 현황', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1E293B), ), ), const SizedBox(height: 8), Text( '이번 달', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), const SizedBox(height: 24), // 총 지출 금액 (강조 표시) Center( child: Column( children: [ Text( CurrencyUtil.formatTotalAmount( _totalExpense), style: const TextStyle( fontSize: 36, fontWeight: FontWeight.bold, color: Color(0xFF3B82F6), letterSpacing: -1, ), ), const SizedBox(height: 4), const Text( '월 구독 지출', style: TextStyle( fontSize: 14, color: Colors.grey, ), ), ], ), ), const SizedBox(height: 24), // 서비스 건수 및 평균 요금 Row( children: [ Expanded( child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '총 서비스', style: TextStyle( fontSize: 14, color: Colors.grey, ), ), const SizedBox(height: 4), Text( '${subscriptions.length}개', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), ), ), const SizedBox(width: 8), Expanded( child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '평균 요금', style: TextStyle( fontSize: 14, color: Colors.grey, ), ), const SizedBox(height: 4), Text( subscriptions.isEmpty ? '₩0' : CurrencyUtil.formatTotalAmount( _totalExpense / subscriptions.length), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), ), ), ], ), // 바 차트 추가 if (subscriptions.isNotEmpty) ...[ const SizedBox(height: 24), SizedBox( height: 150, child: BarChart( BarChartData( barGroups: _getBarGroups(subscriptions), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { return const Text( '총 지출', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF64748B), ), ); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), ), ), ), ], ], ), ), ), ), ), const SizedBox(height: 24), // 3. 월간 지출 차트 FadeTransition( opacity: CurvedAnimation( parent: _animationController, curve: const Interval(0.4, 1.0, curve: Curves.easeOut), ), child: SlideTransition( position: Tween( begin: const Offset(0, 0.2), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.4, 1.0, curve: Curves.easeOut), )), child: Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '월간 지출 추이', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1E293B), ), ), const SizedBox(height: 8), Text( '최근 6개월', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), const SizedBox(height: 24), SizedBox( height: 250, child: BarChart( BarChartData( gridData: FlGridData( show: false, ), borderData: FlBorderData( show: false, ), titlesData: FlTitlesData( show: true, leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: false, ), ), topTitles: AxisTitles( sideTitles: SideTitles( showTitles: false, ), ), rightTitles: AxisTitles( sideTitles: SideTitles( showTitles: false, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { if (value.toInt() >= _monthlyData.length) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( _monthlyData[value.toInt()] ['monthName'], style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF64748B), ), ), ); }, ), ), ), barGroups: _getMonthlyBarGroups(), barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( tooltipBgColor: Colors.blueGrey.shade800, tooltipRoundedRadius: 8, getTooltipItem: (group, groupIndex, rod, rodIndex) { return BarTooltipItem( '${_monthlyData[group.x]['monthName']}\n', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), children: [ TextSpan( text: CurrencyUtil .formatTotalAmount( _monthlyData[group.x] ['totalExpense'] as double), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w500, ), ), ], ); }, ), ), ), ), ), ], ), ), ), ), ), const SizedBox(height: 24), // 4. 이벤트 분석 if (provider.activeEventSubscriptions.isNotEmpty) ...[ FadeTransition( opacity: CurvedAnimation( parent: _animationController, curve: const Interval(0.6, 1.0, curve: Curves.easeOut), ), child: SlideTransition( position: Tween( begin: const Offset(0, 0.2), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.6, 1.0, curve: Curves.easeOut), )), child: Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( '이벤트 할인 현황', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1E293B), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( gradient: LinearGradient( colors: [ const Color(0xFFFF6B6B), const Color(0xFFFF8787), ], ), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.local_offer_rounded, size: 14, color: Colors.white, ), const SizedBox(width: 4), Text( '${provider.activeEventSubscriptions.length}개 진행중', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ], ), const SizedBox(height: 16), // 총 절약액 표시 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [ const Color(0xFFFF6B6B).withOpacity(0.1), const Color(0xFFFF8787).withOpacity(0.1), ], ), borderRadius: BorderRadius.circular(12), border: Border.all( color: const Color(0xFFFF6B6B).withOpacity(0.3), width: 1, ), ), child: Column( children: [ const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.savings_rounded, size: 24, color: Color(0xFFFF6B6B), ), SizedBox(width: 8), Text( '월간 총 절약액', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), ], ), const SizedBox(height: 8), FutureBuilder( future: CurrencyUtil.calculateTotalEventSavings( provider.subscriptions), builder: (context, snapshot) { if (snapshot.hasData) { return Text( CurrencyUtil.formatTotalAmount( snapshot.data!), style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w800, color: Color(0xFFFF6B6B), ), ); } return const CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Color(0xFFFF6B6B)), ); }, ), ], ), ), const SizedBox(height: 16), // 이벤트 중인 구독 목록 const Text( '이벤트 진행 중인 구독', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), const SizedBox(height: 12), ...provider.activeEventSubscriptions.map((subscription) { final daysRemaining = subscription.eventEndDate != null ? subscription.eventEndDate!.difference(DateTime.now()).inDays : 0; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all( color: const Color(0xFFE5E7EB), width: 1, ), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( subscription.serviceName, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), const SizedBox(height: 4), FutureBuilder( future: CurrencyUtil.formatEventSavings( subscription), builder: (context, snapshot) { if (snapshot.hasData) { return Text( '${snapshot.data} 절약', style: const TextStyle( fontSize: 12, color: Color(0xFFFF6B6B), fontWeight: FontWeight.w500, ), ); } return const SizedBox(); }, ), ], ), ), if (daysRemaining > 0) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFFFEF3C7), borderRadius: BorderRadius.circular(12), ), child: Text( '$daysRemaining일 남음', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFFF59E0B), ), ), ), ], ), ); }).toList(), ], ), ), ), ), ), ], const SizedBox(height: 32), ], ), ); }, ), ); } } class _Badge extends StatelessWidget { final double size; final Color borderColor; final SubscriptionModel subscription; const _Badge({ required this.size, required this.borderColor, required this.subscription, }); @override Widget build(BuildContext context) { return AnimatedContainer( duration: PieChart.defaultDuration, width: size, height: size, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, border: Border.all( color: borderColor, width: 2, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.5), blurRadius: 10, spreadRadius: 2, ), ], ), padding: const EdgeInsets.all(1), child: Center( child: Text( subscription.serviceName .substring(0, math.min(1, subscription.serviceName.length)), style: TextStyle( color: borderColor, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ); } }