Files
submanager/lib/screens/analysis_screen.dart
JiWoong Sul 8619e96739 Initial commit: SubManager Flutter App
주요 구현 완료 기능:
- 구독 관리 (추가/편집/삭제/카테고리 분류)
- 이벤트 할인 시스템 (기본값 자동 설정)
- SMS 자동 스캔 및 구독 정보 추출
- 알림 시스템 (타임존 처리 안정화)
- 환율 변환 지원 (KRW/USD)
- 반응형 UI 및 애니메이션
- 다국어 지원 (한국어/영어)

버그 수정:
- NotificationService tz.local 초기화 오류 해결
- MainScreenSummaryCard 레이아웃 오버플로우 수정
- 구독 추가 시 LateInitializationError 완전 해결

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-09 14:29:53 +09:00

1144 lines
51 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fl_chart/fl_chart.dart';
import 'dart:math' as math;
import '../providers/subscription_provider.dart';
import '../models/subscription_model.dart';
import '../temp/test_sms_data.dart';
import '../services/currency_util.dart';
import '../services/exchange_rate_service.dart';
import '../widgets/native_ad_widget.dart';
class AnalysisScreen extends StatefulWidget {
const AnalysisScreen({super.key});
@override
State<AnalysisScreen> createState() => _AnalysisScreenState();
}
class _AnalysisScreenState extends State<AnalysisScreen>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late ScrollController _scrollController;
double _scrollOffset = 0;
int _touchedIndex = -1;
// 최근 6개월 데이터
late List<Map<String, dynamic>> _monthlyData;
// 총 지출액 (원화 환산)
double _totalExpense = 0.0;
// 로딩 상태
bool _isLoading = true;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600), // 애니메이션 속도 조정
)..forward();
_scrollController = ScrollController()
..addListener(() {
setState(() {
_scrollOffset = _scrollController.offset;
});
});
// 월간 지출 데이터 초기화
_monthlyData = TestSmsData.getMonthlyExpenseData();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_calculateTotalExpense();
}
// 총 지출 금액 계산 (USD는 원화로 환산)
Future<void> _calculateTotalExpense() async {
setState(() => _isLoading = true);
try {
final provider =
Provider.of<SubscriptionProvider>(context, listen: false);
final subscriptions = provider.subscriptions;
if (subscriptions.isEmpty) {
setState(() {
_totalExpense = 0.0;
_isLoading = false;
});
return;
}
// 모든 구독의 월 비용을 원화로 환산하여 계산
final total =
await CurrencyUtil.calculateTotalMonthlyExpense(subscriptions);
setState(() {
_totalExpense = total;
_isLoading = false;
});
} catch (e) {
debugPrint('총 지출 계산 오류: $e');
setState(() => _isLoading = false);
}
}
@override
void dispose() {
_animationController.dispose();
_scrollController.dispose();
super.dispose();
}
// 총 지출 금액 바 차트 데이터
List<BarChartGroupData> _getBarGroups(List<SubscriptionModel> subscriptions) {
return [
BarChartGroupData(
x: 0,
barRods: [
BarChartRodData(
toY: _totalExpense,
gradient: const LinearGradient(
colors: [Color(0xFF3B82F6), Color(0xFF60A5FA)],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
width: 20,
borderRadius: BorderRadius.circular(4),
backDrawRodData: BackgroundBarChartRodData(
show: true,
toY: _totalExpense + (_totalExpense * 0.2),
color: Colors.grey.withOpacity(0.1),
),
),
],
),
];
}
// 파이 차트 섹션 데이터
List<PieChartSectionData> _getPieSections(
List<SubscriptionModel> subscriptions) {
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
? _Badge(
size: 40,
borderColor: colors[index],
subscription: subscription,
)
: null,
badgePositionPercentageOffset: .98,
);
});
}
// 월간 지출 차트 데이터
List<BarChartGroupData> _getMonthlyBarGroups() {
final List<BarChartGroupData> barGroups = [];
final maxAmount = _monthlyData.fold<double>(
0, (max, data) => math.max(max, data['totalExpense'] as double));
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).withOpacity(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.withOpacity(0.1),
),
),
],
),
);
}
return barGroups;
}
@override
Widget build(BuildContext context) {
final double appBarOpacity = math.max(0, math.min(1, _scrollOffset / 100));
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(appBarOpacity),
boxShadow: appBarOpacity > 0.6
? [
BoxShadow(
color: Colors.black.withOpacity(0.1 * appBarOpacity),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 4),
)
]
: null,
),
child: SafeArea(
child: AppBar(
title: Text(
'지출 분석',
style: TextStyle(
fontFamily: 'Montserrat',
fontSize: 24,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
color: const Color(0xFF1E293B),
shadows: appBarOpacity > 0.6
? [
Shadow(
color: Colors.black.withOpacity(0.2),
offset: const Offset(0, 1),
blurRadius: 2,
)
]
: null,
),
),
elevation: 0,
backgroundColor: Colors.transparent,
),
),
),
),
body: Consumer<SubscriptionProvider>(
builder: (context, provider, child) {
final subscriptions = provider.subscriptions;
if (_isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return SingleChildScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: MediaQuery.of(context).padding.top + 60),
// 네이티브 광고 위젯 추가
FadeTransition(
opacity: CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, 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.5, curve: Curves.easeOut),
)),
child: const NativeAdWidget(key: ValueKey('analysis_ad')),
),
),
const SizedBox(height: 24),
// 1. 구독 비율 파이 차트 (처음으로 위치 변경)
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: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'구독 서비스 비율',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1E293B),
),
),
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),
Text(
'월 지출 기준',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 16),
Center(
child: subscriptions.isEmpty
? const SizedBox(
height: 250,
child: Center(
child: Text(
'구독중인 서비스가 없습니다',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
),
)
: SizedBox(
height: 250,
child: PieChart(
PieChartData(
borderData: FlBorderData(show: false),
sectionsSpace: 2,
centerSpaceRadius: 60,
sections:
_getPieSections(subscriptions),
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event,
pieTouchResponse) {
setState(() {
if (!event
.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse
.touchedSection ==
null) {
_touchedIndex = -1;
return;
}
_touchedIndex = 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: Text(
subscription.serviceName,
style: const TextStyle(
fontSize: 14,
),
overflow:
TextOverflow.ellipsis,
),
),
FutureBuilder<String>(
future: CurrencyUtil
.formatSubscriptionAmount(
subscription),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
snapshot.data!,
style: const TextStyle(
fontSize: 14,
fontWeight:
FontWeight.bold,
),
);
}
return const SizedBox(
width: 20,
height: 20,
child:
CircularProgressIndicator(
strokeWidth: 2,
),
);
},
),
],
),
);
},
),
),
],
),
),
),
),
),
const SizedBox(height: 24),
// 2. 총 지출 요약 카드
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: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 헤더 텍스트
const Text(
'총 지출 현황',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1E293B),
),
),
const SizedBox(height: 8),
Text(
'이번 달',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 24),
// 총 지출 금액 (강조 표시)
Center(
child: Column(
children: [
Text(
CurrencyUtil.formatTotalAmount(
_totalExpense),
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Color(0xFF3B82F6),
letterSpacing: -1,
),
),
const SizedBox(height: 4),
const Text(
'월 구독 지출',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
const SizedBox(height: 24),
// 서비스 건수 및 평균 요금
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'총 서비스',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
'${subscriptions.length}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'평균 요금',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
subscriptions.isEmpty
? '₩0'
: CurrencyUtil.formatTotalAmount(
_totalExpense /
subscriptions.length),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
// 바 차트 추가
if (subscriptions.isNotEmpty) ...[
const SizedBox(height: 24),
SizedBox(
height: 150,
child: BarChart(
BarChartData(
barGroups: _getBarGroups(subscriptions),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return const Text(
'총 지출',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFF64748B),
),
);
},
),
),
leftTitles: const AxisTitles(
sideTitles:
SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles:
SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles:
SideTitles(showTitles: false),
),
),
),
),
),
],
],
),
),
),
),
),
const SizedBox(height: 24),
// 3. 월간 지출 차트
FadeTransition(
opacity: CurvedAnimation(
parent: _animationController,
curve: const Interval(0.4, 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.4, 1.0, curve: Curves.easeOut),
)),
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'월간 지출 추이',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1E293B),
),
),
const SizedBox(height: 8),
Text(
'최근 6개월',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 24),
SizedBox(
height: 250,
child: BarChart(
BarChartData(
gridData: FlGridData(
show: false,
),
borderData: FlBorderData(
show: false,
),
titlesData: FlTitlesData(
show: true,
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
topTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
if (value.toInt() >=
_monthlyData.length) {
return const SizedBox.shrink();
}
return Padding(
padding:
const EdgeInsets.only(top: 8.0),
child: Text(
_monthlyData[value.toInt()]
['monthName'],
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFF64748B),
),
),
);
},
),
),
),
barGroups: _getMonthlyBarGroups(),
barTouchData: BarTouchData(
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.white,
fontWeight: FontWeight.w500,
),
),
],
);
},
),
),
),
),
),
],
),
),
),
),
),
const SizedBox(height: 24),
// 4. 이벤트 분석
if (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: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'이벤트 할인 현황',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1E293B),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFFFF6B6B),
const Color(0xFFFF8787),
],
),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.local_offer_rounded,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
'${provider.activeEventSubscriptions.length}개 진행중',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// 총 절약액 표시
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFFFF6B6B).withOpacity(0.1),
const Color(0xFFFF8787).withOpacity(0.1),
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFFF6B6B).withOpacity(0.3),
width: 1,
),
),
child: Column(
children: [
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.savings_rounded,
size: 24,
color: Color(0xFFFF6B6B),
),
SizedBox(width: 8),
Text(
'월간 총 절약액',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
],
),
const SizedBox(height: 8),
FutureBuilder<double>(
future: CurrencyUtil.calculateTotalEventSavings(
provider.subscriptions),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
CurrencyUtil.formatTotalAmount(
snapshot.data!),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: Color(0xFFFF6B6B),
),
);
}
return const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFFFF6B6B)),
);
},
),
],
),
),
const SizedBox(height: 16),
// 이벤트 중인 구독 목록
const Text(
'이벤트 진행 중인 구독',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
const SizedBox(height: 12),
...provider.activeEventSubscriptions.map((subscription) {
final daysRemaining = subscription.eventEndDate != null
? subscription.eventEndDate!.difference(DateTime.now()).inDays
: 0;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFFE5E7EB),
width: 1,
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subscription.serviceName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
const SizedBox(height: 4),
FutureBuilder<String>(
future: CurrencyUtil.formatEventSavings(
subscription),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
'${snapshot.data} 절약',
style: const TextStyle(
fontSize: 12,
color: Color(0xFFFF6B6B),
fontWeight: FontWeight.w500,
),
);
}
return const SizedBox();
},
),
],
),
),
if (daysRemaining > 0)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFFEF3C7),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'$daysRemaining일 남음',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFFF59E0B),
),
),
),
],
),
);
}).toList(),
],
),
),
),
),
),
],
const SizedBox(height: 32),
],
),
);
},
),
);
}
}
class _Badge extends StatelessWidget {
final double size;
final Color borderColor;
final SubscriptionModel subscription;
const _Badge({
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.withOpacity(0.5),
blurRadius: 10,
spreadRadius: 2,
),
],
),
padding: const EdgeInsets.all(1),
child: Center(
child: Text(
subscription.serviceName
.substring(0, math.min(1, subscription.serviceName.length)),
style: TextStyle(
color: borderColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
}
}