- @doc/color.md 가이드라인에 따른 색상 시스템 전면 개편 - 딥 블루(#2563EB), 스카이 블루(#60A5FA) 메인 컬러로 변경 - 모든 화면과 위젯에 글래스모피즘 효과 일관성 있게 적용 - darkNavy, navyGray 등 새로운 텍스트 색상 체계 도입 - 공통 스낵바 및 다이얼로그 컴포넌트 추가 - Claude AI 프로젝트 컨텍스트 파일(CLAUDE.md) 추가 영향받은 컴포넌트: - 10개 스크린 (main, settings, detail, splash 등) - 30개 이상 위젯 (buttons, cards, forms 등) - 테마 시스템 (AppColors, AppTheme) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
8.6 KiB
Dart
215 lines
8.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'dart:math' as math;
|
|
import '../../services/currency_util.dart';
|
|
import '../../theme/app_colors.dart';
|
|
import '../glassmorphism_card.dart';
|
|
import '../themed_text.dart';
|
|
|
|
/// 월별 지출 현황을 차트로 보여주는 카드 위젯
|
|
class MonthlyExpenseChartCard extends StatelessWidget {
|
|
final List<Map<String, dynamic>> monthlyData;
|
|
final AnimationController animationController;
|
|
|
|
const MonthlyExpenseChartCard({
|
|
super.key,
|
|
required this.monthlyData,
|
|
required this.animationController,
|
|
});
|
|
|
|
// 월간 지출 차트 데이터
|
|
List<BarChartGroupData> _getMonthlyBarGroups() {
|
|
final List<BarChartGroupData> barGroups = [];
|
|
final calculatedMax = monthlyData.fold<double>(
|
|
0, (max, data) => math.max(max, data['totalExpense'] as double));
|
|
final maxAmount = calculatedMax > 0 ? calculatedMax : 100000.0; // 기본값 10만원
|
|
|
|
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 + (maxAmount * 0.1),
|
|
color: AppColors.navyGray.withValues(alpha: 0.1),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return barGroups;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
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<Offset>(
|
|
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: '월별 지출 현황',
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
ThemedText.subtitle(
|
|
text: '최근 6개월간 추이',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
// 바 차트
|
|
AspectRatio(
|
|
aspectRatio: 1.6,
|
|
child: BarChart(
|
|
BarChartData(
|
|
alignment: BarChartAlignment.spaceAround,
|
|
maxY: math.max(
|
|
monthlyData.fold<double>(
|
|
0,
|
|
(max, data) => math.max(
|
|
max, data['totalExpense'] as double)) *
|
|
1.2,
|
|
100000.0 // 최소값 10만원
|
|
),
|
|
barGroups: _getMonthlyBarGroups(),
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
horizontalInterval: math.max(
|
|
monthlyData.fold<double>(
|
|
0,
|
|
(max, data) => math.max(max,
|
|
data['totalExpense'] as double)) /
|
|
4,
|
|
25000.0 // 최소 간격 2.5만원
|
|
),
|
|
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.formatTotalAmount(
|
|
monthlyData[group.x]['totalExpense']
|
|
as double),
|
|
style: const TextStyle(
|
|
color: Color(0xFFFBBF24),
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Center(
|
|
child: ThemedText.caption(
|
|
text: '월 구독 지출',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |