import 'package:flutter/material.dart'; import '../services/sms_scanner.dart'; import '../providers/subscription_provider.dart'; import '../providers/navigation_provider.dart'; import 'package:provider/provider.dart'; import '../models/subscription.dart'; import '../models/subscription_model.dart'; import '../services/subscription_url_matcher.dart'; import 'package:intl/intl.dart'; // NumberFormat을 사용하기 위한 import 추가 import '../widgets/glassmorphism_card.dart'; import '../widgets/themed_text.dart'; import '../theme/app_colors.dart'; import '../widgets/common/snackbar/app_snackbar.dart'; import '../widgets/common/buttons/primary_button.dart'; import '../widgets/common/buttons/secondary_button.dart'; import '../widgets/common/form_fields/base_text_field.dart'; import '../providers/category_provider.dart'; import '../models/category_model.dart'; import '../widgets/common/form_fields/category_selector.dart'; import '../widgets/native_ad_widget.dart'; class SmsScanScreen extends StatefulWidget { const SmsScanScreen({super.key}); @override State createState() => _SmsScanScreenState(); } class _SmsScanScreenState extends State { bool _isLoading = false; String? _errorMessage; final SmsScanner _smsScanner = SmsScanner(); // 스캔한 구독 목록 List _scannedSubscriptions = []; // 현재 표시 중인 구독 인덱스 int _currentIndex = 0; // 웹사이트 URL 컨트롤러 final TextEditingController _websiteUrlController = TextEditingController(); // 선택된 카테고리 ID 저장 String? _selectedCategoryId; @override void dispose() { _websiteUrlController.dispose(); super.dispose(); } // SMS 스캔 실행 Future _scanSms() async { setState(() { _isLoading = true; _errorMessage = null; _scannedSubscriptions = []; _currentIndex = 0; }); try { // SMS 스캔 실행 print('SMS 스캔 시작'); final scannedSubscriptionModels = await _smsScanner.scanForSubscriptions(); print('스캔된 구독: ${scannedSubscriptionModels.length}개'); if (scannedSubscriptionModels.isNotEmpty) { print( '첫 번째 구독: ${scannedSubscriptionModels[0].serviceName}, 반복 횟수: ${scannedSubscriptionModels[0].repeatCount}'); } if (!mounted) return; if (scannedSubscriptionModels.isEmpty) { print('스캔된 구독이 없음'); setState(() { _errorMessage = '구독 정보를 찾을 수 없습니다.'; _isLoading = false; }); return; } // SubscriptionModel을 Subscription으로 변환 final scannedSubscriptions = _convertModelsToSubscriptions(scannedSubscriptionModels); // 2회 이상 반복 결제된 구독만 필터링 final repeatSubscriptions = scannedSubscriptions.where((sub) => sub.repeatCount >= 2).toList(); print('반복 결제된 구독: ${repeatSubscriptions.length}개'); if (repeatSubscriptions.isNotEmpty) { print( '첫 번째 반복 구독: ${repeatSubscriptions[0].serviceName}, 반복 횟수: ${repeatSubscriptions[0].repeatCount}'); } if (repeatSubscriptions.isEmpty) { print('반복 결제된 구독이 없음'); setState(() { _errorMessage = '반복 결제된 구독 정보를 찾을 수 없습니다.'; _isLoading = false; }); return; } // 구독 목록 가져오기 final provider = Provider.of(context, listen: false); final existingSubscriptions = provider.subscriptions; print('기존 구독: ${existingSubscriptions.length}개'); // 중복 구독 필터링 final filteredSubscriptions = _filterDuplicates(repeatSubscriptions, existingSubscriptions); print('중복 제거 후 구독: ${filteredSubscriptions.length}개'); if (filteredSubscriptions.isNotEmpty) { print( '첫 번째 필터링된 구독: ${filteredSubscriptions[0].serviceName}, 반복 횟수: ${filteredSubscriptions[0].repeatCount}'); } // 중복 제거 후 신규 구독이 없는 경우 if (filteredSubscriptions.isEmpty) { print('중복 제거 후 신규 구독이 없음'); setState(() { _isLoading = false; }); // 스낵바로 안내 메시지 표시 if (mounted) { AppSnackBar.showInfo( context: context, message: '신규 구독 관련 SMS를 찾을 수 없습니다', icon: Icons.search_off_rounded, ); } return; } setState(() { _scannedSubscriptions = filteredSubscriptions; _isLoading = false; _websiteUrlController.text = ''; // URL 입력 필드 초기화 }); } catch (e) { print('SMS 스캔 중 오류 발생: $e'); if (mounted) { setState(() { _errorMessage = 'SMS 스캔 중 오류가 발생했습니다: $e'; _isLoading = false; }); } } } // SubscriptionModel 리스트를 Subscription 리스트로 변환 List _convertModelsToSubscriptions( List models) { final result = []; for (var model in models) { try { // 모델의 필드가 null인 경우 기본값 사용 result.add(Subscription( id: model.id, serviceName: model.serviceName, monthlyCost: model.monthlyCost, billingCycle: model.billingCycle, nextBillingDate: model.nextBillingDate, category: model.categoryId, // categoryId를 category로 올바르게 매핑 repeatCount: model.repeatCount > 0 ? model.repeatCount : 1, // 반복 횟수가 0 이하인 경우 기본값 1 사용 lastPaymentDate: model.lastPaymentDate, websiteUrl: model.websiteUrl, currency: model.currency, // 통화 단위 정보 추가 )); print( '모델 변환 성공: ${model.serviceName}, 카테고리ID: ${model.categoryId}, URL: ${model.websiteUrl}, 통화: ${model.currency}'); } catch (e) { print('모델 변환 중 오류 발생: $e'); } } return result; } // 중복 구독 필터링 (서비스명과 금액이 같으면 중복으로 간주) List _filterDuplicates( List scanned, List existing) { print( '_filterDuplicates: 스캔된 구독 ${scanned.length}개, 기존 구독 ${existing.length}개'); // 중복되지 않은 구독만 필터링 final nonDuplicates = scanned.where((scannedSub) { // 서비스명과 금액이 동일한 기존 구독 찾기 final hasDuplicate = existing.any((existingSub) => existingSub.serviceName.toLowerCase() == scannedSub.serviceName.toLowerCase() && existingSub.monthlyCost == scannedSub.monthlyCost); if (hasDuplicate) { print('_filterDuplicates: 중복 발견 - ${scannedSub.serviceName}'); } // 중복이 없으면 true 반환 return !hasDuplicate; }).toList(); print('_filterDuplicates: 중복 제거 후 ${nonDuplicates.length}개'); // 각 구독에 웹사이트 URL 자동 매칭 시도 final result = []; for (int i = 0; i < nonDuplicates.length; i++) { final subscription = nonDuplicates[i]; String? websiteUrl = subscription.websiteUrl; if (websiteUrl == null || websiteUrl.isEmpty) { websiteUrl = SubscriptionUrlMatcher.suggestUrl(subscription.serviceName); print( '_filterDuplicates: URL 자동 매칭 시도 - ${subscription.serviceName}, 결과: ${websiteUrl ?? "매칭 실패"}'); } try { // 유효성 검사 if (subscription.serviceName.isEmpty) { print('_filterDuplicates: 서비스명이 비어 있습니다. 건너뜁니다.'); continue; } if (subscription.monthlyCost <= 0) { print('_filterDuplicates: 월 비용이 0 이하입니다. 건너뜁니다.'); continue; } // Subscription 객체에 URL 설정 (새 객체 생성) result.add(Subscription( id: subscription.id, serviceName: subscription.serviceName, monthlyCost: subscription.monthlyCost, billingCycle: subscription.billingCycle, nextBillingDate: subscription.nextBillingDate, category: subscription.category, notes: subscription.notes, repeatCount: subscription.repeatCount > 0 ? subscription.repeatCount : 1, lastPaymentDate: subscription.lastPaymentDate, websiteUrl: websiteUrl, currency: subscription.currency, // 통화 단위 정보 추가 )); print( '_filterDuplicates: URL 설정 - ${subscription.serviceName}, URL: ${websiteUrl ?? "없음"}, 카테고리: ${subscription.category ?? "없음"}, 통화: ${subscription.currency}'); } catch (e) { print('_filterDuplicates: 구독 객체 생성 중 오류 발생: $e'); } } print('_filterDuplicates: URL 설정 완료, 최종 ${result.length}개 구독'); return result; } // 현재 구독 추가 Future _addCurrentSubscription() async { if (_scannedSubscriptions.isEmpty || _currentIndex >= _scannedSubscriptions.length) { print( '오류: 인덱스가 범위를 벗어났습니다. (index: $_currentIndex, size: ${_scannedSubscriptions.length})'); return; } final subscription = _scannedSubscriptions[_currentIndex]; final provider = Provider.of(context, listen: false); // 날짜가 과거면 다음 결제일을 조정 final now = DateTime.now(); DateTime nextBillingDate = subscription.nextBillingDate; if (nextBillingDate.isBefore(now)) { // 주기에 따라 다음 결제일 조정 if (subscription.billingCycle == '월간') { // 현재 달의 결제일 int day = nextBillingDate.day; // 현재 월의 마지막 날을 초과하는 경우 조정 final lastDay = DateTime(now.year, now.month + 1, 0).day; if (day > lastDay) { day = lastDay; } DateTime adjustedDate = DateTime(now.year, now.month, day); // 현재 날짜보다 이전이라면 다음 달로 설정 if (adjustedDate.isBefore(now)) { // 다음 달의 마지막 날을 초과하는 경우 조정 final nextMonthLastDay = DateTime(now.year, now.month + 2, 0).day; if (day > nextMonthLastDay) { day = nextMonthLastDay; } adjustedDate = DateTime(now.year, now.month + 1, day); } nextBillingDate = adjustedDate; } else if (subscription.billingCycle == '연간') { // 현재 년도의 결제일 int day = nextBillingDate.day; // 해당 월의 마지막 날을 초과하는 경우 조정 final lastDay = DateTime(now.year, nextBillingDate.month + 1, 0).day; if (day > lastDay) { day = lastDay; } DateTime adjustedDate = DateTime(now.year, nextBillingDate.month, day); // 현재 날짜보다 이전이라면 다음 해로 설정 if (adjustedDate.isBefore(now)) { // 다음 해 해당 월의 마지막 날을 초과하는 경우 조정 final nextYearLastDay = DateTime(now.year + 1, nextBillingDate.month + 1, 0).day; if (day > nextYearLastDay) { day = nextYearLastDay; } adjustedDate = DateTime(now.year + 1, nextBillingDate.month, day); } nextBillingDate = adjustedDate; } else if (subscription.billingCycle == '주간') { // 현재 날짜에서 가장 가까운 다음 주 같은 요일 final daysUntilNext = 7 - (now.weekday - nextBillingDate.weekday) % 7; nextBillingDate = now.add(Duration(days: daysUntilNext == 0 ? 7 : daysUntilNext)); } } // 웹사이트 URL이 비어있으면 자동 매칭 시도 String? websiteUrl = _websiteUrlController.text.trim(); if (websiteUrl.isEmpty && subscription.websiteUrl != null) { websiteUrl = subscription.websiteUrl; print('구독 추가: 기존 URL 사용 - ${websiteUrl ?? "없음"}'); } else if (websiteUrl.isEmpty) { try { websiteUrl = SubscriptionUrlMatcher.suggestUrl(subscription.serviceName); print( '구독 추가: URL 자동 매칭 - ${subscription.serviceName} -> ${websiteUrl ?? "매칭 실패"}'); } catch (e) { print('구독 추가: URL 자동 매칭 실패 - $e'); websiteUrl = null; } } else { print('구독 추가: 사용자 입력 URL 사용 - $websiteUrl'); } try { print( '구독 추가 시도 - 서비스명: ${subscription.serviceName}, 비용: ${subscription.monthlyCost}, 반복 횟수: ${subscription.repeatCount}'); // 반복 횟수가 0 이하인 경우 기본값 1 사용 final int safeRepeatCount = subscription.repeatCount > 0 ? subscription.repeatCount : 1; await provider.addSubscription( serviceName: subscription.serviceName, monthlyCost: subscription.monthlyCost, billingCycle: subscription.billingCycle, nextBillingDate: nextBillingDate, websiteUrl: websiteUrl, isAutoDetected: true, repeatCount: safeRepeatCount, lastPaymentDate: subscription.lastPaymentDate, categoryId: _selectedCategoryId ?? subscription.category, currency: subscription.currency, // 통화 단위 정보 추가 ); print('구독 추가 성공'); // 성공 메시지 표시 if (mounted) { AppSnackBar.showSuccess( context: context, message: '${subscription.serviceName} 구독이 추가되었습니다.', ); } // 다음 구독으로 이동 _moveToNextSubscription(); } catch (e) { print('구독 추가 중 오류 발생: $e'); if (mounted) { AppSnackBar.showError( context: context, message: '구독 추가 중 오류가 발생했습니다: $e', ); // 오류가 있어도 다음 구독으로 이동 _moveToNextSubscription(); } } } // 현재 구독 건너뛰기 void _skipCurrentSubscription() { final subscription = _scannedSubscriptions[_currentIndex]; if (mounted) { AppSnackBar.showInfo( context: context, message: '${subscription.serviceName} 구독을 건너뛰었습니다.', icon: Icons.skip_next_rounded, ); } _moveToNextSubscription(); } // 다음 구독으로 이동 void _moveToNextSubscription() { setState(() { _currentIndex++; _websiteUrlController.text = ''; // URL 입력 필드 초기화 _selectedCategoryId = null; // 카테고리 선택 초기화 // 모든 구독을 처리했으면 홈 화면으로 이동 if (_currentIndex >= _scannedSubscriptions.length) { _navigateToHome(); } }); } // 홈 화면으로 이동 void _navigateToHome() { // NavigationProvider를 사용하여 홈 화면으로 이동 final navigationProvider = Provider.of(context, listen: false); navigationProvider.updateCurrentIndex(0); // 완료 메시지 표시 AppSnackBar.showSuccess( context: context, message: '모든 구독이 처리되었습니다.', ); } // 날짜 상태 텍스트 가져오기 String _getNextBillingText(DateTime date) { final now = DateTime.now(); if (date.isBefore(now)) { // 주기에 따라 다음 결제일 예측 if (_currentIndex >= _scannedSubscriptions.length) { return '다음 결제일 확인 필요'; } final subscription = _scannedSubscriptions[_currentIndex]; if (subscription.billingCycle == '월간') { // 이번 달 또는 다음 달 같은 날짜 int day = date.day; // 현재 월의 마지막 날을 초과하는 경우 조정 final lastDay = DateTime(now.year, now.month + 1, 0).day; if (day > lastDay) { day = lastDay; } DateTime adjusted = DateTime(now.year, now.month, day); if (adjusted.isBefore(now)) { // 다음 달의 마지막 날을 초과하는 경우 조정 final nextMonthLastDay = DateTime(now.year, now.month + 2, 0).day; if (day > nextMonthLastDay) { day = nextMonthLastDay; } adjusted = DateTime(now.year, now.month + 1, day); } final daysUntil = adjusted.difference(now).inDays; return '다음 예상 결제일: ${_formatDate(adjusted)} ($daysUntil일 후)'; } else if (subscription.billingCycle == '연간') { // 올해 또는 내년 같은 날짜 int day = date.day; // 해당 월의 마지막 날을 초과하는 경우 조정 final lastDay = DateTime(now.year, date.month + 1, 0).day; if (day > lastDay) { day = lastDay; } DateTime adjusted = DateTime(now.year, date.month, day); if (adjusted.isBefore(now)) { // 다음 해 해당 월의 마지막 날을 초과하는 경우 조정 final nextYearLastDay = DateTime(now.year + 1, date.month + 1, 0).day; if (day > nextYearLastDay) { day = nextYearLastDay; } adjusted = DateTime(now.year + 1, date.month, day); } final daysUntil = adjusted.difference(now).inDays; return '다음 예상 결제일: ${_formatDate(adjusted)} ($daysUntil일 후)'; } else { return '다음 결제일 확인 필요 (과거 날짜)'; } } else { // 미래 날짜인 경우 final daysUntil = date.difference(now).inDays; return '다음 결제일: ${_formatDate(date)} ($daysUntil일 후)'; } } // 날짜 포맷 함수 String _formatDate(DateTime date) { return '${date.year}년 ${date.month}월 ${date.day}일'; } // 결제 반복 횟수 텍스트 String _getRepeatCountText(int count) { return '$count회 결제 감지됨'; } // 카테고리 칩 빌드 Widget _buildCategoryChip(String? categoryId, CategoryProvider categoryProvider) { final category = categoryId != null ? categoryProvider.getCategoryById(categoryId) : null; // 카테고리가 없으면 기타 카테고리 찾기 final defaultCategory = category ?? categoryProvider.categories.firstWhere( (cat) => cat.name == '기타', orElse: () => categoryProvider.categories.first, ); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: AppColors.navyGray.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ // 카테고리 아이콘 표시 Icon( _getCategoryIcon(defaultCategory), size: 16, color: AppColors.darkNavy, ), const SizedBox(width: 6), ThemedText( defaultCategory.name, fontSize: 14, fontWeight: FontWeight.w500, forceDark: true, ), ], ), ); } // 카테고리 아이콘 반환 IconData _getCategoryIcon(CategoryModel category) { switch (category.name) { case '음악': return Icons.music_note_rounded; case 'OTT(동영상)': return Icons.movie_filter_rounded; case '저장/클라우드': return Icons.cloud_outlined; case '통신 · 인터넷 · TV': return Icons.wifi_rounded; case '생활/라이프스타일': return Icons.home_outlined; case '쇼핑/이커머스': return Icons.shopping_cart_outlined; case '프로그래밍': return Icons.code_rounded; case '협업/오피스': return Icons.business_center_outlined; case 'AI 서비스': return Icons.smart_toy_outlined; case '기타': default: return Icons.category_outlined; } } @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( children: [ _isLoading ? _buildLoadingState() : (_scannedSubscriptions.isEmpty ? _buildInitialState() : _buildSubscriptionState()), // FloatingNavigationBar를 위한 충분한 하단 여백 SizedBox( height: 120 + MediaQuery.of(context).padding.bottom, ), ], ), ); } // 로딩 상태 UI Widget _buildLoadingState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(AppColors.primaryColor), ), const SizedBox(height: 16), const ThemedText('SMS 메시지를 스캔 중입니다...', forceDark: true), const SizedBox(height: 8), const ThemedText('구독 서비스를 찾고 있습니다', opacity: 0.7, forceDark: true), ], ), ); } // 초기 상태 UI Widget _buildInitialState() { return Column( children: [ // 광고 위젯 추가 const NativeAdWidget(key: ValueKey('sms_scan_start_ad')), const SizedBox(height: 48), Padding( padding: const EdgeInsets.symmetric(vertical: 32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_errorMessage != null) Padding( padding: const EdgeInsets.all(16.0), child: ThemedText( _errorMessage!, color: Colors.red, textAlign: TextAlign.center, ), ), const SizedBox(height: 24), const ThemedText( '2회 이상 결제된 구독 서비스 찾기', fontSize: 20, fontWeight: FontWeight.bold, forceDark: true, ), const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 32.0), child: ThemedText( '문자 메시지를 스캔하여 반복적으로 결제된 구독 서비스를 자동으로 찾습니다. 서비스명과 금액을 추출하여 쉽게 구독을 추가할 수 있습니다.', textAlign: TextAlign.center, opacity: 0.7, forceDark: true, ), ), const SizedBox(height: 32), PrimaryButton( text: '스캔 시작하기', icon: Icons.search_rounded, onPressed: _scanSms, width: 200, height: 56, backgroundColor: AppColors.primaryColor, ), ], ), ), ], ); } // 구독 표시 상태 UI Widget _buildSubscriptionState() { if (_currentIndex >= _scannedSubscriptions.length) { // 처리 완료 후 초기 상태로 복귀 _scannedSubscriptions = []; _currentIndex = 0; return _buildInitialState(); // 스캔 버튼이 있는 초기 화면으로 돌아감 } final subscription = _scannedSubscriptions[_currentIndex]; final categoryProvider = Provider.of(context, listen: false); // 구독 리스트 카드를 표시할 때 URL 필드 자동 설정 if (_websiteUrlController.text.isEmpty && subscription.websiteUrl != null) { _websiteUrlController.text = subscription.websiteUrl!; } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 광고 위젯 추가 const NativeAdWidget(key: ValueKey('sms_scan_result_ad')), const SizedBox(height: 16), // 진행 상태 표시 LinearProgressIndicator( value: (_currentIndex + 1) / _scannedSubscriptions.length, backgroundColor: AppColors.navyGray.withValues(alpha: 0.2), valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary), ), const SizedBox(height: 8), ThemedText( '${_currentIndex + 1}/${_scannedSubscriptions.length}', fontWeight: FontWeight.w500, opacity: 0.7, forceDark: true, ), const SizedBox(height: 24), // 구독 정보 카드 GlassmorphismCard( width: double.infinity, padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const ThemedText( '다음 구독을 찾았습니다', fontSize: 18, fontWeight: FontWeight.bold, forceDark: true, ), const SizedBox(height: 24), // 서비스명 const ThemedText( '서비스명', fontWeight: FontWeight.w500, opacity: 0.7, forceDark: true, ), const SizedBox(height: 4), ThemedText( subscription.serviceName, fontSize: 22, fontWeight: FontWeight.bold, forceDark: true, ), const SizedBox(height: 16), // 금액 및 결제 주기 Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const ThemedText( '월 비용', fontWeight: FontWeight.w500, opacity: 0.7, forceDark: true, ), const SizedBox(height: 4), ThemedText( subscription.currency == 'USD' ? NumberFormat.currency( locale: 'en_US', symbol: '\$', decimalDigits: 2, ).format(subscription.monthlyCost) : NumberFormat.currency( locale: 'ko_KR', symbol: '₩', decimalDigits: 0, ).format(subscription.monthlyCost), fontSize: 18, fontWeight: FontWeight.bold, forceDark: true, ), ], ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const ThemedText( '결제 주기', fontWeight: FontWeight.w500, opacity: 0.7, forceDark: true, ), const SizedBox(height: 4), ThemedText( subscription.billingCycle, fontSize: 16, fontWeight: FontWeight.w500, forceDark: true, ), ], ), ), ], ), const SizedBox(height: 16), // 다음 결제일 const ThemedText( '다음 결제일', fontWeight: FontWeight.w500, opacity: 0.7, forceDark: true, ), const SizedBox(height: 4), ThemedText( _getNextBillingText(subscription.nextBillingDate), fontSize: 16, fontWeight: FontWeight.w500, forceDark: true, ), const SizedBox(height: 16), // 카테고리 선택 const ThemedText( '카테고리', fontWeight: FontWeight.w500, opacity: 0.7, forceDark: true, ), const SizedBox(height: 8), CategorySelector( categories: categoryProvider.categories, selectedCategoryId: _selectedCategoryId ?? subscription.category, onChanged: (categoryId) { setState(() { _selectedCategoryId = categoryId; }); }, baseColor: (() { final categoryId = _selectedCategoryId ?? subscription.category; if (categoryId == null) return null; final category = categoryProvider.getCategoryById(categoryId); if (category == null) return null; return Color(int.parse(category.color.replaceFirst('#', '0xFF'))); })(), isGlassmorphism: true, ), const SizedBox(height: 24), // 웹사이트 URL 입력 필드 추가/수정 BaseTextField( controller: _websiteUrlController, label: '웹사이트 URL (자동 추출됨)', hintText: '웹사이트 URL을 수정하거나 비워두세요', prefixIcon: Icon( Icons.language, color: AppColors.navyGray, ), style: TextStyle( color: AppColors.darkNavy, ), fillColor: AppColors.pureWhite.withValues(alpha: 0.8), ), const SizedBox(height: 32), // 작업 버튼 Row( children: [ Expanded( child: SecondaryButton( text: '건너뛰기', onPressed: _skipCurrentSubscription, height: 48, ), ), const SizedBox(width: 16), Expanded( child: PrimaryButton( text: '추가하기', onPressed: _addCurrentSubscription, height: 48, ), ), ], ), ], ), ), ], ); } @override void didChangeDependencies() { super.didChangeDependencies(); if (_scannedSubscriptions.isNotEmpty && _currentIndex < _scannedSubscriptions.length) { final currentSub = _scannedSubscriptions[_currentIndex]; if (_websiteUrlController.text.isEmpty && currentSub.websiteUrl != null) { _websiteUrlController.text = currentSub.websiteUrl!; } } } }