import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'dart:math' as math; import '../models/subscription_model.dart'; import '../models/category_model.dart'; import '../providers/subscription_provider.dart'; import '../providers/category_provider.dart'; import 'package:intl/intl.dart'; import '../widgets/website_icon.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../services/subscription_url_matcher.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter/services.dart'; // TextInputFormatter 사용을 위한 import 추가 import '../services/exchange_rate_service.dart'; // 환율 서비스만 사용 class DetailScreen extends StatefulWidget { final SubscriptionModel subscription; const DetailScreen({ super.key, required this.subscription, }); @override State createState() => _DetailScreenState(); } class _DetailScreenState extends State with SingleTickerProviderStateMixin { late TextEditingController _serviceNameController; late TextEditingController _monthlyCostController; late TextEditingController _websiteUrlController; late String _billingCycle; late DateTime _nextBillingDate; late AnimationController _animationController; late Animation _fadeAnimation; late Animation _slideAnimation; late Animation _rotateAnimation; String? _selectedCategoryId; // 선택된 카테고리 ID late String _currency; // 통화 단위: '원화' 또는 '달러' bool _isLoading = false; // 로딩 상태 // 이벤트 관련 상태 변수 late bool _isEventActive; DateTime? _eventStartDate; DateTime? _eventEndDate; late TextEditingController _eventPriceController; // 포커스 노드 추가 final _serviceNameFocus = FocusNode(); final _monthlyCostFocus = FocusNode(); final _billingCycleFocus = FocusNode(); final _nextBillingDateFocus = FocusNode(); final _websiteUrlFocus = FocusNode(); final _categoryFocus = FocusNode(); // 카테고리 포커스 노드 final _currencyFocus = FocusNode(); // 통화 단위 포커스 노드 final ScrollController _scrollController = ScrollController(); double _scrollOffset = 0; // 현재 편집 중인 필드 int _currentEditingField = -1; // 호버 상태 bool _isDeleteHovered = false; bool _isSaveHovered = false; bool _isCancelHovered = false; @override void initState() { super.initState(); _serviceNameController = TextEditingController(text: widget.subscription.serviceName); _monthlyCostController = TextEditingController(text: widget.subscription.monthlyCost.toString()); _websiteUrlController = TextEditingController(text: widget.subscription.websiteUrl ?? ''); _billingCycle = widget.subscription.billingCycle; _nextBillingDate = widget.subscription.nextBillingDate; _selectedCategoryId = widget.subscription.categoryId; // 카테고리 ID 설정 _currency = widget.subscription.currency; // 통화 단위 설정 // 이벤트 관련 초기화 _isEventActive = widget.subscription.isEventActive; _eventStartDate = widget.subscription.eventStartDate; _eventEndDate = widget.subscription.eventEndDate; _eventPriceController = TextEditingController(); // 이벤트 가격 초기화 if (widget.subscription.eventPrice != null) { if (_currency == 'KRW') { _eventPriceController.text = NumberFormat.decimalPattern() .format(widget.subscription.eventPrice!.toInt()); } else { _eventPriceController.text = NumberFormat('#,##0.00').format(widget.subscription.eventPrice!); } } // 통화 단위에 따른 금액 표시 형식 조정 if (_currency == 'KRW') { // 원화: 정수 형식으로 표시 (콤마 포함) _monthlyCostController.text = NumberFormat.decimalPattern() .format(widget.subscription.monthlyCost.toInt()); } else { // 달러: 소수점 2자리까지 표시 (콤마 포함) _monthlyCostController.text = NumberFormat('#,##0.00').format(widget.subscription.monthlyCost); } // 카테고리 ID가 없으면 서비스명 기반으로 자동 선택 시도 if (_selectedCategoryId == null) { _autoSelectCategory(); } // 서비스명 컨트롤러에 리스너 추가 _serviceNameController.addListener(_onServiceNameChanged); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.6, curve: Curves.easeIn), )); _slideAnimation = Tween( begin: const Offset(0.0, 0.2), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.easeOutCubic, )); _rotateAnimation = Tween( begin: 0.0, end: 2 * math.pi, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.5, curve: Curves.easeOutBack), )); _scrollController.addListener(() { setState(() { _scrollOffset = _scrollController.offset; }); }); _animationController.forward(); } @override void dispose() { // 서비스명 컨트롤러 리스너 제거 _serviceNameController.removeListener(_onServiceNameChanged); // 카테고리가 변경되었으면 구독 정보 업데이트 if (_selectedCategoryId != widget.subscription.categoryId) { widget.subscription.categoryId = _selectedCategoryId; final provider = Provider.of(context, listen: false); provider.updateSubscription(widget.subscription); } _serviceNameController.dispose(); _monthlyCostController.dispose(); _websiteUrlController.dispose(); _eventPriceController.dispose(); _animationController.dispose(); _scrollController.dispose(); // 포커스 노드 해제 _serviceNameFocus.dispose(); _monthlyCostFocus.dispose(); _billingCycleFocus.dispose(); _nextBillingDateFocus.dispose(); _websiteUrlFocus.dispose(); _categoryFocus.dispose(); _currencyFocus.dispose(); super.dispose(); } // 서비스명이 변경될 때 호출되는 콜백 함수 void _onServiceNameChanged() { // 웹사이트 URL이 비어있거나 기존 URL이 서비스와 매칭되지 않는 경우에만 자동 매칭 if (_serviceNameController.text.isNotEmpty && (_websiteUrlController.text.isEmpty || SubscriptionUrlMatcher.findMatchingUrl( _serviceNameController.text) != _websiteUrlController.text)) { // 자동 URL 매칭 시도 final suggestedUrl = SubscriptionUrlMatcher.suggestUrl(_serviceNameController.text); // 매칭된 URL이 있으면 텍스트 컨트롤러에 설정 if (suggestedUrl != null && suggestedUrl.isNotEmpty) { setState(() { _websiteUrlController.text = suggestedUrl; }); } } } Future _updateSubscription() async { final provider = Provider.of(context, listen: false); // 웹사이트 URL이 비어있는 경우 자동 매칭 다시 시도 String? websiteUrl = _websiteUrlController.text; if (websiteUrl.isEmpty) { websiteUrl = SubscriptionUrlMatcher.suggestUrl(_serviceNameController.text); } // 구독 정보 업데이트 final oldCategoryId = widget.subscription.categoryId; final newCategoryId = _selectedCategoryId; // 콤마 제거하고 숫자만 추출 double monthlyCost = 0.0; try { monthlyCost = double.parse(_monthlyCostController.text.replaceAll(',', '')); } catch (e) { // 파싱 오류 발생 시 기본값 사용 monthlyCost = widget.subscription.monthlyCost; } widget.subscription.serviceName = _serviceNameController.text; widget.subscription.monthlyCost = monthlyCost; widget.subscription.websiteUrl = websiteUrl; widget.subscription.billingCycle = _billingCycle; widget.subscription.nextBillingDate = _nextBillingDate; widget.subscription.categoryId = _selectedCategoryId; // 카테고리 업데이트 widget.subscription.currency = _currency; // 통화 단위 업데이트 // 이벤트 정보 업데이트 widget.subscription.isEventActive = _isEventActive; widget.subscription.eventStartDate = _eventStartDate; widget.subscription.eventEndDate = _eventEndDate; // 이벤트 가격 파싱 if (_isEventActive && _eventPriceController.text.isNotEmpty) { try { widget.subscription.eventPrice = double.parse(_eventPriceController.text.replaceAll(',', '')); } catch (e) { widget.subscription.eventPrice = null; } } else { widget.subscription.eventPrice = null; } // 구독 업데이트 await provider.updateSubscription(widget.subscription); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.check_circle_rounded, color: Colors.white), const SizedBox(width: 12), const Text('구독 정보가 업데이트되었습니다.'), ], ), behavior: SnackBarBehavior.floating, backgroundColor: const Color(0xFF10B981), duration: const Duration(seconds: 2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ); // 변경 사항이 반영될 시간을 주기 위해 짧게 지연 후 결과 반환 // 카테고리가 변경된 경우에만 true를 반환 final categoryChanged = oldCategoryId != newCategoryId; await Future.delayed(const Duration(milliseconds: 100)); Navigator.of(context).pop(true); } } Future _deleteSubscription() async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: const Text( '구독 삭제', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 20, ), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFFEF2F2), borderRadius: BorderRadius.circular(16), ), child: const Icon( Icons.warning_amber_rounded, color: Color(0xFFDC2626), size: 48, ), ), const SizedBox(height: 16), const Text('이 구독을 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.'), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('취소'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFDC2626), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text( '삭제', style: TextStyle(color: Colors.white), ), ), ], ), ); if (confirmed == true && mounted) { final provider = Provider.of(context, listen: false); await provider.deleteSubscription(widget.subscription.id); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.delete_forever_rounded, color: Colors.white), const SizedBox(width: 12), const Text('구독이 삭제되었습니다.'), ], ), behavior: SnackBarBehavior.floating, backgroundColor: const Color(0xFFDC2626), duration: const Duration(seconds: 2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ); Navigator.of(context).pop(); } } } // 배경 그라데이션과 색상 가져오기 Color _getCardColor() { // 서비스 이름에 따라 일관된 색상 생성 final int hash = widget.subscription.serviceName.hashCode.abs(); final List colors = [ const Color(0xFF3B82F6), // 파랑 const Color(0xFF10B981), // 초록 const Color(0xFF8B5CF6), // 보라 const Color(0xFFF59E0B), // 노랑 const Color(0xFFEF4444), // 빨강 const Color(0xFF0EA5E9), // 하늘 const Color(0xFFEC4899), // 분홍 ]; return colors[hash % colors.length]; } LinearGradient _getGradient(Color baseColor) { return LinearGradient( colors: [ baseColor, baseColor.withOpacity(0.7), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ); } // 서비스명을 기반으로 카테고리 자동 선택 함수 void _autoSelectCategory() { if (_serviceNameController.text.isEmpty) return; final serviceName = _serviceNameController.text.toLowerCase(); final categoryProvider = Provider.of(context, listen: false); // 카테고리가 없으면 리턴 if (categoryProvider.categories.isEmpty) return; // OTT 서비스 확인 if (SubscriptionUrlMatcher.ottServices.keys.any((key) => serviceName.contains(key.toLowerCase()) || key.toLowerCase().contains(serviceName))) { // OTT 관련 카테고리 찾기 try { final ottCategory = categoryProvider.categories.firstWhere( (cat) => cat.name.contains('OTT') || cat.name.contains('미디어') || cat.name.contains('영상'), ); setState(() { _selectedCategoryId = ottCategory.id; }); return; } catch (_) { // OTT 카테고리가 없으면 첫 번째 카테고리 사용 if (categoryProvider.categories.isNotEmpty) { setState(() { _selectedCategoryId = categoryProvider.categories.first.id; }); } } } // 음악 서비스 확인 if (SubscriptionUrlMatcher.musicServices.keys.any((key) => serviceName.contains(key.toLowerCase()) || key.toLowerCase().contains(serviceName))) { // 음악 관련 카테고리 찾기 try { final musicCategory = categoryProvider.categories.firstWhere( (cat) => cat.name.contains('음악') || cat.name.contains('스트리밍'), ); setState(() { _selectedCategoryId = musicCategory.id; }); return; } catch (_) { // 음악 카테고리가 없으면 첫 번째 카테고리 사용 if (categoryProvider.categories.isNotEmpty) { setState(() { _selectedCategoryId = categoryProvider.categories.first.id; }); } } } // AI 서비스 확인 if (SubscriptionUrlMatcher.aiServices.keys.any((key) => serviceName.contains(key.toLowerCase()) || key.toLowerCase().contains(serviceName))) { // AI 관련 카테고리 찾기 try { final aiCategory = categoryProvider.categories.firstWhere( (cat) => cat.name.contains('AI') || cat.name.contains('인공지능'), ); setState(() { _selectedCategoryId = aiCategory.id; }); return; } catch (_) { // AI 카테고리가 없으면 첫 번째 카테고리 사용 if (categoryProvider.categories.isNotEmpty) { setState(() { _selectedCategoryId = categoryProvider.categories.first.id; }); } } } // 프로그래밍/개발 서비스 확인 if (SubscriptionUrlMatcher.programmingServices.keys.any((key) => serviceName.contains(key.toLowerCase()) || key.toLowerCase().contains(serviceName))) { // 개발 관련 카테고리 찾기 try { final devCategory = categoryProvider.categories.firstWhere( (cat) => cat.name.contains('개발') || cat.name.contains('프로그래밍'), ); setState(() { _selectedCategoryId = devCategory.id; }); return; } catch (_) { // 개발 카테고리가 없으면 첫 번째 카테고리 사용 if (categoryProvider.categories.isNotEmpty) { setState(() { _selectedCategoryId = categoryProvider.categories.first.id; }); } } } // 오피스/협업 툴 확인 if (SubscriptionUrlMatcher.officeTools.keys.any((key) => serviceName.contains(key.toLowerCase()) || key.toLowerCase().contains(serviceName))) { // 오피스 관련 카테고리 찾기 try { final officeCategory = categoryProvider.categories.firstWhere( (cat) => cat.name.contains('오피스') || cat.name.contains('협업') || cat.name.contains('업무'), ); setState(() { _selectedCategoryId = officeCategory.id; }); return; } catch (_) { // 오피스 카테고리가 없으면 첫 번째 카테고리 사용 if (categoryProvider.categories.isNotEmpty) { setState(() { _selectedCategoryId = categoryProvider.categories.first.id; }); } } } // 기타 서비스 확인 if (SubscriptionUrlMatcher.otherServices.keys.any((key) => serviceName.contains(key.toLowerCase()) || key.toLowerCase().contains(serviceName))) { // 기타 관련 카테고리 찾기 try { final otherCategory = categoryProvider.categories.firstWhere( (cat) => cat.name.contains('기타') || cat.name.contains('게임'), ); setState(() { _selectedCategoryId = otherCategory.id; }); } catch (_) { // 기타 카테고리가 없으면 첫 번째 카테고리 사용 if (categoryProvider.categories.isNotEmpty) { setState(() { _selectedCategoryId = categoryProvider.categories.first.id; }); } } } } // URL을 외부 앱에서 여는 함수 Future _openCancellationPage() async { final serviceName = widget.subscription.serviceName; final websiteUrl = widget.subscription.websiteUrl; // 해지 안내 페이지 URL 찾기 final cancellationUrl = SubscriptionUrlMatcher.findCancellationUrl(serviceName); if (cancellationUrl == null) { // 해지 안내 페이지가 없는 경우 사용자에게 안내 if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('공식 해지 안내 페이지가 제공되지 않는 서비스입니다.'), behavior: SnackBarBehavior.floating, backgroundColor: Colors.grey.shade700, duration: const Duration(seconds: 2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ); } return; } try { final Uri url = Uri.parse(cancellationUrl); if (await canLaunchUrl(url)) { await launchUrl(url, mode: LaunchMode.externalApplication); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('해지 안내 페이지를 열 수 없습니다.'), behavior: SnackBarBehavior.floating, backgroundColor: Colors.red.shade700, duration: const Duration(seconds: 2), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), ); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('오류가 발생했습니다: $e'), behavior: SnackBarBehavior.floating, backgroundColor: Colors.red.shade700, duration: const Duration(seconds: 2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ); } } } @override Widget build(BuildContext context) { final daysUntilBilling = widget.subscription.nextBillingDate.difference(DateTime.now()).inDays; final isNearBilling = daysUntilBilling <= 7; final baseColor = _getCardColor(); final double appBarOpacity = math.max(0, math.min(1, _scrollOffset / 150)); return Scaffold( backgroundColor: const Color(0xFFF8FAFC), extendBodyBehindAppBar: true, appBar: PreferredSize( preferredSize: const Size.fromHeight(60), child: Container( decoration: BoxDecoration( color: Colors.white.withOpacity(appBarOpacity), boxShadow: appBarOpacity > 0.6 ? [ BoxShadow( color: Colors.black.withOpacity(0.1 * appBarOpacity), spreadRadius: 1, blurRadius: 8, offset: const Offset(0, 4), ) ] : null, ), child: SafeArea( child: AppBar( title: Text( '구독 상세', style: TextStyle( fontFamily: 'Montserrat', fontSize: 24, fontWeight: FontWeight.w800, letterSpacing: -0.5, color: Color(0xFF1E293B), shadows: appBarOpacity > 0.6 ? [ Shadow( color: Colors.black.withOpacity(0.2), offset: const Offset(0, 1), blurRadius: 2, ) ] : null, ), ), elevation: 0, backgroundColor: Colors.transparent, actions: [ // 해지 안내 버튼 if (SubscriptionUrlMatcher.hasCancellationPage( widget.subscription.serviceName)) MouseRegion( onEnter: (_) => setState(() => _isCancelHovered = true), onExit: (_) => setState(() => _isCancelHovered = false), child: AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.symmetric( horizontal: 8, vertical: 6), decoration: BoxDecoration( color: _isCancelHovered ? const Color(0xFFF1F5F9) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: TextButton.icon( icon: const Icon( Icons.open_in_browser, size: 18, color: Color(0xFF6B7280), ), label: const Text( '해지 안내', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF6B7280), ), ), onPressed: _openCancellationPage, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ), ), MouseRegion( onEnter: (_) => setState(() => _isDeleteHovered = true), onExit: (_) => setState(() => _isDeleteHovered = false), child: AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: _isDeleteHovered ? const Color(0xFFFEF2F2) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: IconButton( icon: const FaIcon(FontAwesomeIcons.trashCan, size: 20, color: Color(0xFFDC2626)), tooltip: '삭제', onPressed: _deleteSubscription, ), ), ), ], ), ), ), ), body: SingleChildScrollView( controller: _scrollController, physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: MediaQuery.of(context).padding.top + 60), FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: Hero( tag: 'subscription_${widget.subscription.id}', child: Card( elevation: 8, shadowColor: baseColor.withOpacity(0.4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), child: Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.3), decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ baseColor.withOpacity(0.8), baseColor, ], ), ), padding: const EdgeInsets.all(24), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( children: [ AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.rotate( angle: _rotateAnimation.value, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black .withOpacity(0.1), blurRadius: 10, spreadRadius: 0, ), ], ), child: WebsiteIcon( key: ValueKey( 'detail_icon_${widget.subscription.id}'), url: widget.subscription.websiteUrl, serviceName: widget.subscription.serviceName, size: 48, ), ), ); }, ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.subscription.serviceName, style: const TextStyle( fontSize: 28, fontWeight: FontWeight.w800, color: Colors.white, letterSpacing: -0.5, shadows: [ Shadow( color: Colors.black26, offset: Offset(0, 2), blurRadius: 4, ), ], ), ), const SizedBox(height: 4), Text( '${widget.subscription.billingCycle} 결제', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white.withOpacity(0.8), ), ), ], ), ), ], ), const SizedBox(height: 20), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '다음 결제일', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white.withOpacity(0.8), ), ), const SizedBox(height: 4), Text( DateFormat('yyyy년 MM월 dd일').format( widget.subscription .nextBillingDate), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white, ), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '월 지출', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white.withOpacity(0.8), ), ), const SizedBox(height: 4), Text( NumberFormat.currency( locale: _currency == 'KRW' ? 'ko_KR' : 'en_US', symbol: _currency == 'KRW' ? '₩' : '\$', decimalDigits: _currency == 'KRW' ? 0 : 2, ).format( widget.subscription.monthlyCost), style: const TextStyle( fontSize: 28, fontWeight: FontWeight.w800, color: Colors.white, ), ), ], ), ], ), ), if (isNearBilling) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), decoration: BoxDecoration( color: const Color(0xFFDC2626) .withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.white.withOpacity(0.3), width: 1, ), ), child: Row( children: [ const Icon( Icons.access_time_rounded, size: 20, color: Colors.white, ), const SizedBox(width: 8), Expanded( child: Text( daysUntilBilling == 0 ? '오늘 결제 예정' : '$daysUntilBilling일 후 결제 예정', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ], ), ), ], ], ), ), ), ), ), ), ), const SizedBox(height: 32), FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: Tween( begin: const Offset(0.0, 0.4), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic), )), child: Text( '구독 정보 수정', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: baseColor, letterSpacing: -0.5, ), ), ), ), const SizedBox(height: 16), FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: Tween( begin: const Offset(0.0, 0.6), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.3, 1.0, curve: Curves.easeOutCubic), )), child: Card( elevation: 1, shadowColor: Colors.black12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), child: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // 서비스명 필드 AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: _currentEditingField == 0 ? baseColor.withOpacity(0.1) : Colors.transparent, ), padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '서비스명', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: baseColor, ), ), const SizedBox(height: 8), TextFormField( controller: _serviceNameController, focusNode: _serviceNameFocus, textInputAction: TextInputAction.next, onTap: () => setState(() => _currentEditingField = 0), onEditingComplete: () { _monthlyCostFocus.requestFocus(); setState(() => _currentEditingField = -1); }, style: const TextStyle( color: Color(0xFF1E293B), fontSize: 16, fontWeight: FontWeight.w500, ), decoration: InputDecoration( filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.all(16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Colors.grey.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: baseColor, width: 2, ), ), prefixIcon: Icon( Icons.business_rounded, color: baseColor, ), ), ), ], ), ), // 월 비용 필드 AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: _currentEditingField == 1 ? baseColor.withOpacity(0.1) : Colors.transparent, ), padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 환율 정보와 비용 입력 제목 표시 (상단) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( '비용 입력', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: baseColor, ), ), if (_currency == 'USD') FutureBuilder( future: ExchangeRateService() .getFormattedExchangeRateInfo(), builder: (context, snapshot) { if (snapshot.hasData) { return Text( snapshot.data!, style: TextStyle( fontSize: 12, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ); } return const SizedBox.shrink(); }, ), ], ), const SizedBox(height: 8), Row( children: [ // 통화 단위 선택 (좌측) Expanded( flex: 3, // 25% 너비 차지 child: DropdownButtonFormField( value: _currency, focusNode: _currencyFocus, isDense: true, onTap: () => setState( () => _currentEditingField = 1), onChanged: (value) { if (value != null) { setState(() { _currency = value; // 통화 단위 변경 시 입력 값 변환 final currentText = _monthlyCostController.text; if (currentText.isNotEmpty) { // 콤마 제거하고 숫자만 추출 final numericValue = double.tryParse(currentText .replaceAll(',', '')); if (numericValue != null) { if (value == 'KRW') { // 달러 → 원화: 소수점 제거 _monthlyCostController .text = NumberFormat .decimalPattern() .format(numericValue .toInt()); } else { // 원화 → 달러: 소수점 2자리 추가 _monthlyCostController .text = NumberFormat( '#,##0.00') .format(numericValue); } } } // 화면 갱신하여 통화 기호도 업데이트 _monthlyCostFocus.requestFocus(); }); } }, decoration: InputDecoration( filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric( vertical: 16, horizontal: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Colors.grey.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: baseColor, width: 2, ), ), ), icon: Icon( Icons.arrow_drop_down, color: baseColor, ), items: ['KRW', 'USD'] .map((currency) => DropdownMenuItem( value: currency, child: Text( currency == 'KRW' ? 'KRW' : 'USD', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black87, ), ), )) .toList(), ), ), const SizedBox(width: 8), // 비용 입력 필드 (우측) Expanded( flex: 7, // 75% 너비 차지 child: Container( height: 50, // 높이를 56에서 50으로 줄임 // 우측에서 40픽셀 줄이기 margin: const EdgeInsets.only(right: 0), // 내부 패딩을 고정값으로 설정하여 포커스 상태와 관계없이 일관되게 유지 padding: const EdgeInsets.all(4), decoration: BoxDecoration( // 포커스 상태에 따른 배경색 변경 color: _currentEditingField == 1 ? const Color( 0xFFF3F4F6) // 포커스 상태일 때 연한 회색 : Colors .transparent, // 포커스 없을 때 투명 borderRadius: BorderRadius.circular(12), // 테두리 설정 (포커스 상태에 따라 색상만 변경) border: Border.all( color: _currentEditingField == 1 ? baseColor : Colors.grey.withOpacity( 0.4), // 포커스 없을 때 더 진한 회색 width: _currentEditingField == 1 ? 2 : 1, ), ), child: Row( children: [ // 통화 기호 - 항상 표시되도록 설정 Container( width: 40, alignment: Alignment.center, decoration: BoxDecoration( // 테두리 추가 (좌측 통화선택란과 동일한 스타일) border: Border( right: BorderSide( color: Colors.grey .withOpacity(0.2), width: 1, ), ), ), child: Text( _currency == 'KRW' ? '₩' : '\$', style: TextStyle( color: baseColor, fontSize: 18, fontWeight: FontWeight.bold, ), ), ), // 실제 입력 필드 Expanded( child: Stack( alignment: Alignment.centerRight, children: [ TextField( controller: _monthlyCostController, focusNode: _monthlyCostFocus, textInputAction: TextInputAction.next, keyboardType: const TextInputType .numberWithOptions( decimal: true), inputFormatters: [ // 통화 단위에 따라 다른 입력 형식 적용 FilteringTextInputFormatter .allow( _currency == 'KRW' ? RegExp( r'[0-9,]') // 원화: 정수만 허용 : RegExp( r'[0-9,.]'), // 달러: 소수점 허용 ), // 커스텀 포맷터 - 3자리마다 콤마 추가 TextInputFormatter .withFunction( (oldValue, newValue) { // 입력값에서 콤마 제거 final text = newValue .text .replaceAll( ',', ''); if (text.isEmpty) { return newValue .copyWith( text: ''); } // 숫자 형식 검증 if (_currency == 'KRW') { // 원화: 정수 형식 if (double.tryParse( text) == null) { return oldValue; } // 3자리마다 콤마 추가 final formattedValue = NumberFormat .decimalPattern() .format( int.parse( text)); return newValue .copyWith( text: formattedValue, selection: TextSelection .collapsed( offset: formattedValue .length), ); } else { // 달러: 소수점 형식 if (double.tryParse( text) == null && text != '.') { return oldValue; } // 소수점 이하 처리를 위해 부분 분리 final parts = text.split('.'); final integerPart = parts[0]; final decimalPart = parts .length > 1 ? '.${parts[1].length > 2 ? parts[1].substring(0, 2) : parts[1]}' : ''; // 3자리마다 콤마 추가 (정수 부분만) String formattedValue; if (integerPart .isEmpty) { formattedValue = '0$decimalPart'; } else { final formatted = NumberFormat .decimalPattern() .format(int.parse( integerPart)); formattedValue = '$formatted$decimalPart'; } return newValue .copyWith( text: formattedValue, selection: TextSelection .collapsed( offset: formattedValue .length), ); } }), ], onTap: () => setState(() => _currentEditingField = 1), onSubmitted: (_) { _billingCycleFocus .requestFocus(); setState(() => _currentEditingField = -1); }, style: const TextStyle( color: Color(0xFF1E293B), fontSize: 16, fontWeight: FontWeight.w500, ), decoration: InputDecoration( border: InputBorder.none, // 포커스 상태와 관계없이 일관된 패딩 유지 contentPadding: const EdgeInsets .symmetric( vertical: 14, horizontal: 8), hintText: _currency == 'KRW' ? '9,000' : '9.99', hintStyle: TextStyle( color: Colors .grey.shade500, fontSize: 16, ), // 모든 테두리 제거 enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, ), ), // 달러일 때 원화 환산 금액 표시 if (_currency == 'USD') ValueListenableBuilder< TextEditingValue>( valueListenable: _monthlyCostController, builder: (context, value, child) { // 입력값이 바뀔 때마다 환산 금액 갱신 return FutureBuilder< String>( future: ExchangeRateService() .getFormattedKrwAmount( double.tryParse(value .text .replaceAll( ',', '')) ?? 0.0), builder: (context, snapshot) { if (snapshot .hasData && snapshot.data! .isNotEmpty) { return Padding( padding: const EdgeInsets .only( right: 12.0), child: Text( snapshot .data!, style: const TextStyle( fontSize: 14, color: Colors .blue, fontWeight: FontWeight .w500, ), ), ); } return const SizedBox .shrink(); }, ); }, ), ], ), ), ], ), ), ), ], ), // 환율 정보 위젯 추가 (달러 선택 시에만 표시) ], ), ), // 결제 주기 필드 AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: _currentEditingField == 2 ? baseColor.withOpacity(0.1) : Colors.transparent, ), padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '결제 주기', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: baseColor, ), ), const SizedBox(height: 8), DropdownButtonFormField( value: _billingCycle, focusNode: _billingCycleFocus, onTap: () => setState(() => _currentEditingField = 2), onChanged: (value) { if (value != null) { setState(() { _billingCycle = value; _currentEditingField = -1; _nextBillingDateFocus.requestFocus(); }); } }, decoration: InputDecoration( filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.all(16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Colors.grey.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: baseColor, width: 2, ), ), prefixIcon: Icon( Icons.calendar_today_rounded, color: baseColor, ), ), dropdownColor: Colors.white, style: const TextStyle( color: Colors.black87, fontSize: 16, fontWeight: FontWeight.w500, ), items: ['월간', '연간', '주간', '일간'] .map((cycle) => DropdownMenuItem( value: cycle, child: Text(cycle), )) .toList(), ), ], ), ), // 다음 결제일 필드 AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: _currentEditingField == 3 ? baseColor.withOpacity(0.1) : Colors.transparent, ), padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '다음 결제일', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: baseColor, ), ), const SizedBox(height: 8), InkWell( focusNode: _nextBillingDateFocus, onTap: () async { setState(() => _currentEditingField = 3); final DateTime? picked = await showDatePicker( context: context, initialDate: _nextBillingDate, firstDate: DateTime.now(), lastDate: DateTime.now().add( const Duration(days: 365 * 2), ), builder: (BuildContext context, Widget? child) { return Theme( data: ThemeData.light().copyWith( colorScheme: ColorScheme.light( primary: baseColor, onPrimary: Colors.white, surface: Colors.white, onSurface: Colors.black, ), ), child: child!, ); }, ); if (picked != null) { setState(() { _nextBillingDate = picked; _currentEditingField = -1; _websiteUrlFocus.requestFocus(); }); } else { setState(() => _currentEditingField = -1); } }, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.all( color: Colors.grey.withOpacity(0.2), ), borderRadius: BorderRadius.circular(12), color: Colors.white, ), child: Row( children: [ Icon( Icons.event_rounded, color: baseColor, ), const SizedBox(width: 12), Text( DateFormat('yyyy년 MM월 dd일') .format(_nextBillingDate), style: const TextStyle( fontSize: 16, color: Color(0xFF1E293B), fontWeight: FontWeight.w500, ), ), ], ), ), ), ], ), ), // 웹사이트 URL 필드 AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: _currentEditingField == 4 ? baseColor.withOpacity(0.1) : Colors.transparent, ), padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '웹사이트 URL (선택)', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: baseColor, ), ), const SizedBox(height: 8), TextFormField( controller: _websiteUrlController, focusNode: _websiteUrlFocus, textInputAction: TextInputAction.done, onTap: () => setState(() => _currentEditingField = 4), onEditingComplete: () { setState(() => _currentEditingField = 5); _categoryFocus.requestFocus(); }, style: const TextStyle( color: Color(0xFF1E293B), fontSize: 16, fontWeight: FontWeight.w500, ), decoration: InputDecoration( filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.all(16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Colors.grey.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: baseColor, width: 2, ), ), hintText: 'https://example.com', hintStyle: TextStyle( color: Colors.grey.shade500, fontSize: 16, ), prefixIcon: Icon( Icons.language_rounded, color: baseColor, ), ), ), ], ), ), // 카테고리 선택 필드 추가 AnimatedContainer( duration: const Duration(milliseconds: 200), margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: _currentEditingField == 5 ? baseColor.withOpacity(0.1) : Colors.transparent, ), padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '카테고리 (선택)', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: baseColor, ), ), const SizedBox(height: 8), Consumer( builder: (context, categoryProvider, child) { // 카테고리가 없을 때 메시지 표시 if (categoryProvider.categories.isEmpty) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 16, ), decoration: BoxDecoration( border: Border.all( color: Colors.grey.withOpacity(0.2), ), borderRadius: BorderRadius.circular(12), color: Colors.white, ), child: Row( children: [ Icon( Icons.category_rounded, color: baseColor, ), const SizedBox(width: 12), const Text( '카테고리가 없습니다', style: TextStyle( color: Colors.grey, fontSize: 16, ), ), ], ), ); } // 카테고리 드롭다운 표시 return DropdownButtonFormField( value: _selectedCategoryId, focusNode: _categoryFocus, onTap: () => setState( () => _currentEditingField = 5), onChanged: (value) { setState(() { _selectedCategoryId = value; _currentEditingField = -1; _categoryFocus.unfocus(); }); }, icon: Icon( Icons.arrow_drop_down_circle_outlined, color: baseColor, ), decoration: InputDecoration( filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.all(16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Colors.grey.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: baseColor, width: 2, ), ), prefixIcon: Icon( Icons.category_rounded, color: baseColor, ), ), style: const TextStyle( color: Colors.black87, fontSize: 16, fontWeight: FontWeight.w500, ), hint: const Text( '카테고리 선택', style: TextStyle( color: Colors.grey, fontSize: 16, ), ), isExpanded: true, items: [ DropdownMenuItem( value: null, child: const Text( '카테고리 없음', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey, ), ), ), ...categoryProvider.categories .map((category) { return DropdownMenuItem( value: category.id, child: Text( category.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black87, ), ), ); }).toList(), ], ); }, ), ], ), ), // 이벤트 설정 섹션 Container( margin: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: _isEventActive ? baseColor : Colors.grey.withOpacity(0.2), width: _isEventActive ? 2 : 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Checkbox( value: _isEventActive, onChanged: (value) { setState(() { _isEventActive = value ?? false; if (!_isEventActive) { // 이벤트 비활성화 시 관련 데이터 초기화 _eventStartDate = null; _eventEndDate = null; _eventPriceController.clear(); } }); }, activeColor: baseColor, ), const Text( '이벤트/할인 설정', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), const SizedBox(width: 8), Icon( Icons.local_offer, size: 20, color: _isEventActive ? baseColor : Colors.grey, ), ], ), // 이벤트 활성화 시 추가 필드 표시 AnimatedContainer( duration: const Duration(milliseconds: 300), height: _isEventActive ? null : 0, child: AnimatedOpacity( duration: const Duration(milliseconds: 300), opacity: _isEventActive ? 1.0 : 0.0, child: Column( children: [ const SizedBox(height: 16), // 이벤트 기간 설정 Row( children: [ // 시작일 Expanded( child: InkWell( onTap: () async { final DateTime? picked = await showDatePicker( context: context, initialDate: _eventStartDate ?? DateTime.now(), firstDate: DateTime.now().subtract(const Duration(days: 365)), lastDate: DateTime.now().add(const Duration(days: 365 * 2)), builder: (BuildContext context, Widget? child) { return Theme( data: ThemeData.light().copyWith( colorScheme: ColorScheme.light( primary: baseColor, onPrimary: Colors.white, surface: Colors.white, onSurface: Colors.black, ), ), child: child!, ); }, ); if (picked != null) { setState(() { _eventStartDate = picked; }); } }, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border.all(color: Colors.grey.withOpacity(0.3)), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '시작일', style: TextStyle( fontSize: 12, color: Colors.grey, ), ), const SizedBox(height: 4), Text( _eventStartDate == null ? '선택' : DateFormat('MM/dd').format(_eventStartDate!), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ), ), ), const SizedBox(width: 8), const Icon(Icons.arrow_forward, color: Colors.grey), const SizedBox(width: 8), // 종료일 Expanded( child: InkWell( onTap: () async { final DateTime? picked = await showDatePicker( context: context, initialDate: _eventEndDate ?? DateTime.now().add(const Duration(days: 30)), firstDate: _eventStartDate ?? DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365 * 2)), builder: (BuildContext context, Widget? child) { return Theme( data: ThemeData.light().copyWith( colorScheme: ColorScheme.light( primary: baseColor, onPrimary: Colors.white, surface: Colors.white, onSurface: Colors.black, ), ), child: child!, ); }, ); if (picked != null) { setState(() { _eventEndDate = picked; }); } }, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border.all(color: Colors.grey.withOpacity(0.3)), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '종료일', style: TextStyle( fontSize: 12, color: Colors.grey, ), ), const SizedBox(height: 4), Text( _eventEndDate == null ? '선택' : DateFormat('MM/dd').format(_eventEndDate!), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ), ), ), ], ), const SizedBox(height: 16), // 이벤트 가격 입력 Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '이벤트 가격', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: baseColor, ), ), const SizedBox(height: 8), TextFormField( controller: _eventPriceController, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[\d,.]')), ], decoration: InputDecoration( hintText: '할인된 가격을 입력하세요', prefixText: _currency == 'KRW' ? '₩ ' : '\$ ', filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric( vertical: 16, horizontal: 16, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Colors.grey.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: baseColor, width: 2, ), ), prefixIcon: Icon( Icons.sell, color: baseColor, ), ), onChanged: (value) { // 콤마 자동 추가 if (value.isNotEmpty && !value.contains('.')) { final number = int.tryParse(value.replaceAll(',', '')); if (number != null) { final formatted = NumberFormat('#,###').format(number); if (formatted != value) { _eventPriceController.value = TextEditingValue( text: formatted, selection: TextSelection.collapsed( offset: formatted.length, ), ); } } } }, ), ], ), ], ), ), ), ], ), ), ], ), ), ), ), ), const SizedBox(height: 32), FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: Tween( begin: const Offset(0.0, 0.8), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.4, 1.0, curve: Curves.easeOutCubic), )), child: MouseRegion( onEnter: (_) => setState(() => _isSaveHovered = true), onExit: (_) => setState(() => _isSaveHovered = false), child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: double.infinity, height: 60, transform: _isSaveHovered ? (Matrix4.identity()..scale(1.02)) : Matrix4.identity(), child: ElevatedButton( onPressed: _updateSubscription, style: ElevatedButton.styleFrom( backgroundColor: baseColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), padding: const EdgeInsets.symmetric(vertical: 16), elevation: _isSaveHovered ? 8 : 4, shadowColor: baseColor.withOpacity(0.5), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.save_rounded, color: Colors.white, size: _isSaveHovered ? 24 : 20, ), const SizedBox(width: 8), const Text( '변경사항 저장', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ), ), ), ), const SizedBox(height: 80), ], ), ), ), ); } }