/// 일정 카드 위젯 library; 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(), ), ], ], ), ), ); } }