import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'dart:math' as math; import 'package:provider/provider.dart'; import '../../services/currency_util.dart'; import '../../providers/locale_provider.dart'; import '../../theme/app_colors.dart'; import '../glassmorphism_card.dart'; import '../themed_text.dart'; import '../../l10n/app_localizations.dart'; /// 월별 지출 현황을 차트로 보여주는 카드 위젯 class MonthlyExpenseChartCard extends StatelessWidget { final List> monthlyData; final AnimationController animationController; const MonthlyExpenseChartCard({ super.key, required this.monthlyData, required this.animationController, }); /// Y축 최대값을 계산합니다 (언어별 통화 단위에 맞춰) double _calculateChartMaxY(double maxValue, String locale) { final currency = CurrencyUtil.getDefaultCurrency(locale); if (currency == 'KRW' || currency == 'JPY') { // 소수점이 없는 통화 (원화, 엔화) if (maxValue <= 0) return 100000; if (maxValue <= 10000) return 10000; if (maxValue <= 50000) return 50000; if (maxValue <= 100000) return 100000; if (maxValue <= 200000) return 200000; if (maxValue <= 500000) return 500000; if (maxValue <= 1000000) return 1000000; // 큰 금액은 자릿수에 맞춰 반올림 final magnitude = math.pow(10, maxValue.toString().split('.')[0].length - 1).toDouble(); return ((maxValue / magnitude).ceil() * magnitude).toDouble(); } else { // 소수점이 있는 통화 (달러, 위안) if (maxValue <= 0) return 100.0; if (maxValue <= 10) return 10.0; if (maxValue <= 25) return 25.0; if (maxValue <= 50) return 50.0; if (maxValue <= 100) return 100.0; if (maxValue <= 250) return 250.0; if (maxValue <= 500) return 500.0; if (maxValue <= 1000) return 1000.0; // 큰 금액은 100 단위로 반올림 return ((maxValue / 100).ceil() * 100).toDouble(); } } /// 그리드 라인 간격을 계산합니다 double _calculateGridInterval(double maxY, String currency) { if (currency == 'KRW' || currency == 'JPY') { // 4등분하되 깔끔한 숫자로 if (maxY <= 40000) return 10000; if (maxY <= 100000) return 25000; if (maxY <= 200000) return 50000; if (maxY <= 400000) return 100000; return maxY / 4; } else { // 달러 등은 4등분 if (maxY <= 40) return 10; if (maxY <= 100) return 25; if (maxY <= 200) return 50; if (maxY <= 400) return 100; return maxY / 4; } } // 월간 지출 차트 데이터 List _getMonthlyBarGroups(String locale) { final List barGroups = []; final calculatedMax = monthlyData.fold( 0, (max, data) => math.max(max, data['totalExpense'] as double)); final maxAmount = _calculateChartMaxY(calculatedMax, locale); 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).withValues(alpha: 0.7), const Color(0xFF60A5FA), ], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), width: 18, borderRadius: BorderRadius.circular(4), backDrawRodData: BackgroundBarChartRodData( show: true, toY: maxAmount, color: AppColors.navyGray.withValues(alpha: 0.1), ), ), ], ), ); } return barGroups; } @override Widget build(BuildContext context) { final locale = context.watch().locale.languageCode; return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: FadeTransition( opacity: CurvedAnimation( parent: animationController, curve: const Interval(0.4, 0.9, 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, 0.9, curve: Curves.easeOut), )), child: GlassmorphismCard( blur: 10, opacity: 0.1, borderRadius: 16, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ThemedText.headline( text: AppLocalizations.of(context).monthlyExpenseTitle, style: const TextStyle( fontSize: 18, ), ), const SizedBox(height: 8), ThemedText.subtitle( text: AppLocalizations.of(context).recentSixMonthsTrend, style: const TextStyle( fontSize: 14, ), ), const SizedBox(height: 20), // 바 차트 AspectRatio( aspectRatio: 1.6, child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: _calculateChartMaxY( monthlyData.fold( 0, (max, data) => math.max( max, data['totalExpense'] as double)), locale), barGroups: _getMonthlyBarGroups(locale), gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: _calculateGridInterval( _calculateChartMaxY( monthlyData.fold( 0, (max, data) => math.max(max, data['totalExpense'] as double)), locale), CurrencyUtil.getDefaultCurrency(locale)), getDrawingHorizontalLine: (value) { return FlLine( color: AppColors.navyGray.withValues(alpha: 0.1), strokeWidth: 1, ); }, ), titlesData: FlTitlesData( show: true, topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { return Padding( padding: const EdgeInsets.only(top: 8), child: ThemedText.caption( text: monthlyData[value.toInt()] ['monthName'], style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData(show: false), barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( tooltipBgColor: AppColors.darkNavy, tooltipRoundedRadius: 8, getTooltipItem: (group, groupIndex, rod, rodIndex) { return BarTooltipItem( '${monthlyData[group.x]['monthName']}\n', const TextStyle( color: AppColors.pureWhite, fontWeight: FontWeight.bold, ), children: [ TextSpan( text: CurrencyUtil .formatTotalAmountWithLocale( monthlyData[group.x] ['totalExpense'] as double, locale), style: const TextStyle( color: Color(0xFFFBBF24), fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ); }, ), ), ), ), ), const SizedBox(height: 16), Center( child: ThemedText.caption( text: AppLocalizations.of(context) .monthlySubscriptionExpense, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), ), ), ), ), ), ); } }