Major UI/UX and architecture improvements
- Implemented new navigation system with NavigationProvider and route management - Added adaptive theme system with ThemeProvider for better theme handling - Introduced glassmorphism design elements (app bars, scaffolds, cards) - Added advanced animations (spring animations, page transitions, staggered lists) - Implemented performance optimizations (memory manager, lazy loading) - Refactored Analysis screen into modular components - Added floating navigation bar with haptic feedback - Improved subscription cards with swipe actions - Enhanced skeleton loading with better animations - Added cached network image support - Improved overall app architecture and code organization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
214
lib/widgets/analysis/monthly_expense_chart_card.dart
Normal file
214
lib/widgets/analysis/monthly_expense_chart_card.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'dart:math' as math;
|
||||
import '../../services/currency_util.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: Colors.grey.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: Colors.grey.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: 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.yellow,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: ThemedText.caption(
|
||||
text: '월 구독 지출',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user