import 'package:flutter/material.dart'; import '../models/subscription_model.dart'; import '../widgets/subscription_group_header.dart'; import '../widgets/swipeable_subscription_card.dart'; import '../widgets/staggered_list_animation.dart'; import '../widgets/app_navigator.dart'; import 'package:provider/provider.dart'; import '../providers/subscription_provider.dart'; import '../providers/locale_provider.dart'; import '../services/subscription_url_matcher.dart'; import './dialogs/delete_confirmation_dialog.dart'; import './common/snackbar/app_snackbar.dart'; import '../l10n/app_localizations.dart'; import '../utils/logger.dart'; import '../utils/subscription_grouping_helper.dart'; /// 카테고리별로 구독 목록을 표시하는 위젯 class SubscriptionListWidget extends StatelessWidget { final List groups; final AnimationController fadeController; const SubscriptionListWidget({ super.key, required this.groups, required this.fadeController, }); @override Widget build(BuildContext context) { final sections = groups; return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final group = sections[index]; final subscriptions = group.subscriptions; return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SubscriptionGroupHeader( group: group, subscriptionCount: subscriptions.length, totalCostUSD: _calculateTotalByCurrency(subscriptions, 'USD'), totalCostKRW: _calculateTotalByCurrency(subscriptions, 'KRW'), totalCostJPY: _calculateTotalByCurrency(subscriptions, 'JPY'), totalCostCNY: _calculateTotalByCurrency(subscriptions, 'CNY'), ), // 카테고리별 구독 목록 FadeTransition( opacity: Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: fadeController, curve: Curves.easeIn)), child: ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, padding: const EdgeInsets.symmetric(horizontal: 16), cacheExtent: 500, prototypeItem: const SizedBox(height: 156), itemCount: subscriptions.length, itemBuilder: (context, subIndex) { // 각 구독의 지연값 계산 (순차적으로 나타나도록) final delay = 0.05 * subIndex; const animationBegin = 0.2; const animationEnd = 1.0; final intervalStart = delay; final intervalEnd = intervalStart + 0.4; // 간격 계산 (0.0~1.0 사이의 값으로 정규화) final intervalStartNormalized = intervalStart.clamp(0.0, 0.9); final intervalEndNormalized = intervalEnd.clamp(0.1, 1.0); return FadeTransition( opacity: Tween( begin: animationBegin, end: animationEnd) .animate(CurvedAnimation( parent: fadeController, curve: Interval(intervalStartNormalized, intervalEndNormalized, curve: Curves.easeOut))), child: Padding( padding: const EdgeInsets.only(bottom: 12.0), child: StaggeredAnimationItem( index: subIndex, delay: const Duration(milliseconds: 50), child: RepaintBoundary( child: SwipeableSubscriptionCard( subscription: subscriptions[subIndex], keepAlive: true, onTap: () { Log.d( '[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨'); AppNavigator.toDetail( context, subscriptions[subIndex]); }, onDelete: () async { // 현재 로케일에 맞는 서비스명 가져오기 final localeProvider = Provider.of( context, listen: false, ); final locale = localeProvider.locale.languageCode; final displayName = await SubscriptionUrlMatcher .getServiceDisplayName( serviceName: subscriptions[subIndex].serviceName, locale: locale, ); // 삭제 확인 다이얼로그 표시 if (!context.mounted) return; final shouldDelete = await DeleteConfirmationDialog.show( context: context, serviceName: displayName, ); if (!context.mounted) return; if (shouldDelete) { // 사용자가 확인한 경우에만 삭제 진행 final provider = Provider.of( context, listen: false, ); await provider.deleteSubscription( subscriptions[subIndex].id, ); if (context.mounted) { AppSnackBar.showError( context: context, message: AppLocalizations.of(context) .subscriptionDeleted(displayName), icon: Icons.delete_forever_rounded, ); } } }, ), ), ), ), ); }, ), ), ], ), ); }, childCount: sections.length, ), ); } /// 특정 통화의 총 합계를 계산합니다. double _calculateTotalByCurrency( List subscriptions, String currency) { return subscriptions .where((sub) => sub.currency == currency) .fold(0.0, (sum, sub) => sum + sub.monthlyCost); } } /// 여러 Sliver 위젯을 하나의 위젯으로 감싸는 도우미 위젯 class MultiSliver extends StatelessWidget { final List children; const MultiSliver({ super.key, required this.children, }); @override Widget build(BuildContext context) { return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { if (index >= children.length) return null; return children[index]; }, childCount: children.length, ), ); } }