Files
submanager/lib/widgets/analysis/subscription_pie_chart_card.dart
JiWoong Sul 4731288622 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>
2025-07-10 18:36:57 +09:00

294 lines
12 KiB
Dart

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