/// 일정 카드 위젯 library; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import '../../../core/constants.dart'; import '../../../models/schedule.dart'; import 'member_chip.dart'; /// HTML 엔티티 디코딩 String decodeHtmlEntities(String text) { return text .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll(''', "'") .replaceAll(' ', ' '); } /// 카테고리 색상 파싱 Color parseColor(String? colorStr) { if (colorStr == null || colorStr.isEmpty) return AppColors.textTertiary; try { final hex = colorStr.replaceFirst('#', ''); return Color(int.parse('FF$hex', radix: 16)); } catch (_) { return AppColors.textTertiary; } } /// 애니메이션이 적용된 일정 카드 래퍼 class AnimatedScheduleCard extends StatefulWidget { final int index; final Schedule schedule; final Color categoryColor; const AnimatedScheduleCard({ super.key, required this.index, required this.schedule, required this.categoryColor, }); @override State createState() => _AnimatedScheduleCardState(); } class _AnimatedScheduleCardState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _fadeAnimation; late Animation _slideAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOut), ); _slideAnimation = Tween(begin: -10.0, end: 0.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic), ); // 순차적 애니메이션 (index * 30ms 딜레이) Future.delayed(Duration(milliseconds: widget.index * 30), () { if (mounted) _controller.forward(); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Opacity( opacity: _fadeAnimation.value, child: Transform.translate( offset: Offset(_slideAnimation.value, 0), child: child, ), ); }, child: ScheduleCard( schedule: widget.schedule, categoryColor: widget.categoryColor, ), ); } } /// 일정 카드 위젯 class ScheduleCard extends StatelessWidget { final Schedule schedule; final Color categoryColor; const ScheduleCard({ super.key, required this.schedule, required this.categoryColor, }); @override Widget build(BuildContext context) { final memberList = schedule.memberList; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 12, offset: const Offset(0, 2), ), ], border: Border.all( color: AppColors.border.withValues(alpha: 0.5), width: 1, ), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 시간 및 카테고리 뱃지 Row( children: [ // 시간 뱃지 if (schedule.formattedTime != null) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: categoryColor, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.access_time, size: 10, color: Colors.white, ), const SizedBox(width: 4), Text( schedule.formattedTime!, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: Colors.white, ), ), ], ), ), if (schedule.formattedTime != null) const SizedBox(width: 6), // 카테고리 뱃지 if (schedule.categoryName != null) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: categoryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( schedule.categoryName!, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: categoryColor, ), ), ), ], ), const SizedBox(height: 10), // 제목 Text( decodeHtmlEntities(schedule.title), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: AppColors.textPrimary, height: 1.4, ), ), // 출처 if (schedule.sourceName != null && schedule.sourceName!.isNotEmpty) ...[ const SizedBox(height: 6), Row( children: [ Icon( Icons.link, size: 11, color: AppColors.textTertiary, ), const SizedBox(width: 4), Text( schedule.sourceName!, style: const TextStyle( fontSize: 11, color: AppColors.textTertiary, ), ), ], ), ], // 멤버 if (memberList.isNotEmpty) ...[ const SizedBox(height: 12), // divider (전체 너비) Container( width: double.infinity, height: 1, color: AppColors.divider, ), const SizedBox(height: 12), Wrap( spacing: 6, runSpacing: 6, children: memberList.length >= 5 ? [ const MemberChip(name: '프로미스나인'), ] : memberList .map((name) => MemberChip(name: name)) .toList(), ), ], ], ), ), ); } } /// 생일 카드 위젯 class BirthdayCard extends StatelessWidget { final Schedule schedule; const BirthdayCard({super.key, required this.schedule}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( colors: [Color(0xFFF472B6), Color(0xFFA855F7), Color(0xFF6366F1)], ), boxShadow: [ BoxShadow( color: const Color(0xFFA855F7).withValues(alpha: 0.3), blurRadius: 12, offset: const Offset(0, 4), ), ], ), clipBehavior: Clip.antiAlias, child: Stack( children: [ // 배경 장식 Positioned( top: -12, right: -12, child: Container( width: 64, height: 64, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.1), ), ), ), Positioned( bottom: -16, left: -16, child: Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.1), ), ), ), const Positioned( bottom: 12, left: 32, child: Text('🎉', style: TextStyle(fontSize: 14)), ), // 내용 Padding( padding: const EdgeInsets.all(16), child: Row( children: [ // 멤버 사진 if (schedule.memberImage != null) Container( width: 56, height: 56, margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.white.withValues(alpha: 0.5), width: 2, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.2), blurRadius: 8, ), ], ), clipBehavior: Clip.antiAlias, child: CachedNetworkImage( imageUrl: schedule.memberImage!, fit: BoxFit.cover, placeholder: (context, url) => Container(color: Colors.white), ), ), // 제목 Expanded( child: Row( children: [ const Text('🎂', style: TextStyle(fontSize: 24)), const SizedBox(width: 8), Expanded( child: Text( decodeHtmlEntities(schedule.title), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 0.5, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ], ), ), ], ), ); } } /// 데뷔/기념일 카드 위젯 class DebutCard extends StatelessWidget { final Schedule schedule; const DebutCard({super.key, required this.schedule}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF7a99c8), Color(0xFF98b0d8), Color(0xFFb8c8e8)], ), boxShadow: [ BoxShadow( color: const Color(0xFF7a99c8).withValues(alpha: 0.3), blurRadius: 12, offset: const Offset(0, 4), ), ], ), clipBehavior: Clip.antiAlias, child: Stack( children: [ // 배경 장식 Positioned( top: -24, left: -24, child: Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.1), ), ), ), Positioned( bottom: -32, right: -32, child: Container( width: 96, height: 96, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.1), ), ), ), // 반짝이는 별 Positioned( top: 8, right: 16, child: Text('✦', style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.6))), ), Positioned( top: 16, right: 48, child: Text('✦', style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: 0.4))), ), Positioned( bottom: 12, right: 24, child: Text('✦', style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: 0.5))), ), // 내용 Padding( padding: const EdgeInsets.all(16), child: Row( children: [ // 아이콘 영역 Container( width: 56, height: 56, margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.3), ), child: Center( child: schedule.isDebut ? const Text( 'DEBUT', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 1, ), ) : Column( mainAxisSize: MainAxisSize.min, children: [ Text( '${schedule.anniversaryYear}', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w900, color: Colors.white, height: 1, ), ), Text( 'YEARS', style: TextStyle( fontSize: 8, fontWeight: FontWeight.bold, color: Colors.white.withValues(alpha: 0.8), ), ), ], ), ), ), // 제목 Expanded( child: Row( children: [ Text( schedule.isDebut ? '🍀' : '☘️', style: const TextStyle(fontSize: 18), ), const SizedBox(width: 6), Expanded( child: Text( schedule.title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 0.5, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ], ), ), ], ), ); } }