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/billing_cost_util.dart'; import '../utils/subscription_grouping_helper.dart'; import 'native_ad_widget.dart'; import 'package:google_mobile_ads/google_mobile_ads.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; int itemCounter = 0; final List children = []; for (final group in sections) { final subscriptions = group.subscriptions; final List subscriptionItems = []; for (var subIndex = 0; subIndex < subscriptions.length; 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); subscriptionItems.add( FadeTransition( opacity: Tween(begin: animationBegin, end: animationEnd) .animate(CurvedAnimation( parent: fadeController, curve: Interval( intervalStartNormalized, intervalEndNormalized, curve: Curves.easeOut))), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6.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, ); } } }, ), ), ), ), ), ); itemCounter++; if ((itemCounter - 1) % 10 == 0) { subscriptionItems.add( NativeAdWidget( key: ValueKey('home_list_ad_$itemCounter'), aspectRatioOverride: 320 / 80, mediaAspectRatioOverride: MediaAspectRatio.landscape, templateTypeOverride: TemplateType.small, ), ); } } children.add( 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'), ), ...subscriptionItems, ], ), ), ); } return SliverList( delegate: SliverChildListDelegate(children), ); } /// 특정 통화의 실제 결제 금액 총 합계를 계산합니다. double _calculateTotalByCurrency( List subscriptions, String currency) { return subscriptions.where((sub) => sub.currency == currency).fold( 0.0, (sum, sub) => sum + BillingCostUtil.convertFromMonthlyCost( sub.currentPrice, sub.billingCycle, )); } } /// 여러 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, ), ); } }