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:
JiWoong Sul
2025-07-10 18:36:57 +09:00
parent 8619e96739
commit 4731288622
55 changed files with 8219 additions and 2149 deletions

View File

@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../models/subscription_model.dart';
import '../../services/currency_util.dart';
/// 파이 차트에서 선택된 섹션에 표시되는 배지 위젯
class AnalysisBadge extends StatelessWidget {
final double size;
final Color borderColor;
final SubscriptionModel subscription;
const AnalysisBadge({
super.key,
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.withValues(alpha: 0.5),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
subscription.serviceName.length > 5
? '${subscription.serviceName.substring(0, 5)}...'
: subscription.serviceName,
style: const TextStyle(
fontSize: 8,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 2),
FutureBuilder<String>(
future: CurrencyUtil.formatAmount(
subscription.monthlyCost,
subscription.currency,
),
builder: (context, snapshot) {
if (snapshot.hasData) {
final amountText = snapshot.data!;
// 금액이 너무 길면 축약
final displayText = amountText.length > 8
? amountText.replaceAll('', '').trim()
: amountText;
return Text(
displayText,
style: const TextStyle(
fontSize: 7,
color: Colors.black54,
),
);
}
return const SizedBox();
},
),
],
),
),
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
/// 분석 화면에서 사용하는 간격 위젯
/// SliverToBoxAdapter 오류를 해결하기 위해 별도 컴포넌트로 분리
class AnalysisScreenSpacer extends StatelessWidget {
final double height;
const AnalysisScreenSpacer({
super.key,
this.height = 24,
});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: SizedBox(height: height),
);
}
}

View File

@@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import '../../providers/subscription_provider.dart';
import '../../services/currency_util.dart';
import '../glassmorphism_card.dart';
import '../themed_text.dart';
/// 이벤트 할인 현황을 보여주는 카드 위젯
class EventAnalysisCard extends StatelessWidget {
final AnimationController animationController;
const EventAnalysisCard({
super.key,
required this.animationController,
});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Consumer<SubscriptionProvider>(
builder: (context, provider, child) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: provider.activeEventSubscriptions.isNotEmpty
? FadeTransition(
opacity: CurvedAnimation(
parent: animationController,
curve: const Interval(0.6, 1.0, 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.6, 1.0, 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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ThemedText.headline(
text: '이벤트 할인 현황',
style: const TextStyle(
fontSize: 18,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFFFF6B6B),
Color(0xFFFE7E7E),
],
),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const FaIcon(
FontAwesomeIcons.fire,
size: 12,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
'${provider.activeEventSubscriptions.length}개 진행중',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFFFF6B6B).withValues(alpha: 0.1),
const Color(0xFFFF8787).withValues(alpha: 0.1),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFFFF6B6B).withValues(alpha: 0.3),
),
),
child: Row(
children: [
const Icon(
Icons.savings,
color: Color(0xFFFF6B6B),
size: 32,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ThemedText(
'월간 절약 금액',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
ThemedText(
CurrencyUtil.formatTotalAmount(
provider.calculateTotalSavings(),
),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFFFF6B6B),
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
const ThemedText(
'진행중인 이벤트',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
...provider.activeEventSubscriptions.map((sub) {
final savings = sub.originalPrice - (sub.eventPrice ?? sub.originalPrice);
final discountRate =
((savings / sub.originalPrice) * 100).round();
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ThemedText(
sub.serviceName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Row(
children: [
FutureBuilder<String>(
future: CurrencyUtil
.formatAmount(
sub.originalPrice,
sub.currency),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ThemedText(
snapshot.data!,
style: const TextStyle(
fontSize: 12,
decoration: TextDecoration
.lineThrough,
color: Colors.grey,
),
);
}
return const SizedBox();
},
),
const SizedBox(width: 8),
const Icon(
Icons.arrow_forward,
size: 12,
color: Colors.grey,
),
const SizedBox(width: 8),
FutureBuilder<String>(
future: CurrencyUtil
.formatAmount(
sub.eventPrice ?? sub.originalPrice,
sub.currency),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ThemedText(
snapshot.data!,
style: const TextStyle(
fontSize: 12,
fontWeight:
FontWeight.bold,
color:
Color(0xFF10B981),
),
);
}
return const SizedBox();
},
),
],
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFFF6B6B)
.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$discountRate% 할인',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFFFF6B6B),
),
),
),
],
),
);
}).toList(),
],
),
),
),
),
)
: const SizedBox.shrink(),
);
},
),
);
}
}

View 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,
),
),
),
],
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,294 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../models/subscription_model.dart';
import '../../services/currency_util.dart';
import '../glassmorphism_card.dart';
import '../themed_text.dart';
import 'analysis_badge.dart';
/// 구독 서비스 비율을 파이 차트로 보여주는 카드 위젯
class SubscriptionPieChartCard extends StatelessWidget {
final List<SubscriptionModel> subscriptions;
final AnimationController animationController;
final int touchedIndex;
final Function(int) onPieTouch;
const SubscriptionPieChartCard({
super.key,
required this.subscriptions,
required this.animationController,
required this.touchedIndex,
required this.onPieTouch,
});
// 파이 차트 섹션 데이터
List<PieChartSectionData> _getPieSections() {
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<double> 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
? AnalysisBadge(
size: 40,
borderColor: colors[index],
subscription: subscription,
)
: null,
badgePositionPercentageOffset: .98,
);
});
}
@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.0, 0.7, 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.0, 0.7, 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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ThemedText.headline(
text: '구독 서비스 비율',
style: const TextStyle(
fontSize: 18,
),
),
FutureBuilder<String>(
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),
ThemedText.subtitle(
text: '월 지출 기준',
style: const TextStyle(
fontSize: 14,
),
),
const SizedBox(height: 16),
Center(
child: subscriptions.isEmpty
? const SizedBox(
height: 250,
child: Center(
child: ThemedText(
'구독중인 서비스가 없습니다',
style: TextStyle(
fontSize: 16,
),
),
),
)
: SizedBox(
height: 250,
child: PieChart(
PieChartData(
borderData: FlBorderData(show: false),
sectionsSpace: 2,
centerSpaceRadius: 60,
sections: _getPieSections(),
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event,
pieTouchResponse) {
if (!event
.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse
.touchedSection ==
null) {
onPieTouch(-1);
return;
}
onPieTouch(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: ThemedText(
subscription.serviceName,
style: const TextStyle(
fontSize: 14,
),
overflow:
TextOverflow.ellipsis,
),
),
FutureBuilder<String>(
future: CurrencyUtil
.formatSubscriptionAmount(
subscription),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ThemedText(
snapshot.data!,
style: const TextStyle(
fontSize: 14,
fontWeight:
FontWeight.bold,
),
);
}
return const SizedBox(
width: 20,
height: 20,
child:
CircularProgressIndicator(
strokeWidth: 2,
),
);
},
),
],
),
);
},
),
),
],
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../../models/subscription_model.dart';
import '../../services/currency_util.dart';
import '../../utils/haptic_feedback_helper.dart';
import '../../theme/app_colors.dart';
import '../glassmorphism_card.dart';
import '../themed_text.dart';
/// 총 지출 요약을 보여주는 카드 위젯
class TotalExpenseSummaryCard extends StatelessWidget {
final List<SubscriptionModel> subscriptions;
final double totalExpense;
final AnimationController animationController;
const TotalExpenseSummaryCard({
super.key,
required this.subscriptions,
required this.totalExpense,
required this.animationController,
});
@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.2, 0.8, 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.2, 0.8, 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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ThemedText.headline(
text: '총 지출 요약',
style: const TextStyle(
fontSize: 18,
),
),
IconButton(
icon: const Icon(Icons.content_copy),
iconSize: 20,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
final totalExpenseText = CurrencyUtil.formatTotalAmount(totalExpense);
await Clipboard.setData(
ClipboardData(text: totalExpenseText));
HapticFeedbackHelper.lightImpact();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('총 지출액이 복사되었습니다: $totalExpenseText'),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
backgroundColor: AppColors.glassBackground.withValues(alpha: 0.3),
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
);
},
),
],
),
const SizedBox(height: 8),
ThemedText.subtitle(
text: '월 단위 총액',
style: const TextStyle(
fontSize: 14,
),
),
const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ThemedText.caption(
text: '총 지출',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
ThemedText(
CurrencyUtil.formatTotalAmount(totalExpense),
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
),
],
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.glassBackground.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.glassBorder.withValues(alpha: 0.2),
),
),
child: const FaIcon(
FontAwesomeIcons.listCheck,
size: 16,
color: Colors.blue,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
ThemedText.caption(
text: '총 서비스',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
ThemedText(
'${subscriptions.length}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
const SizedBox(height: 12),
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.glassBackground.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.glassBorder.withValues(alpha: 0.2),
),
),
child: const FaIcon(
FontAwesomeIcons.chartLine,
size: 16,
color: Colors.green,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
ThemedText.caption(
text: '평균 요금',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
ThemedText(
CurrencyUtil.formatTotalAmount(
subscriptions.isEmpty
? 0
: totalExpense / subscriptions.length),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
],
),
),
],
),
],
),
),
),
),
),
),
);
}
}