From 2f60ef585a2eb4de6ca29213056262c8fe164e0c Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Fri, 11 Jul 2025 18:41:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B8=80=EB=9E=98=EC=8A=A4=EB=AA=A8?= =?UTF-8?q?=ED=94=BC=EC=A6=98=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EB=B0=8F=20=EC=83=89=EC=83=81=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EC=A0=84=EB=A9=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @doc/color.md 가이드라인에 따른 색상 시스템 전면 개편 - 딥 블루(#2563EB), 스카이 블루(#60A5FA) 메인 컬러로 변경 - 모든 화면과 위젯에 글래스모피즘 효과 일관성 있게 적용 - darkNavy, navyGray 등 새로운 텍스트 색상 체계 도입 - 공통 스낵바 및 다이얼로그 컴포넌트 추가 - Claude AI 프로젝트 컨텍스트 파일(CLAUDE.md) 추가 영향받은 컴포넌트: - 10개 스크린 (main, settings, detail, splash 등) - 30개 이상 위젯 (buttons, cards, forms 등) - 테마 시스템 (AppColors, AppTheme) 🤖 Generated with Claude Code Co-Authored-By: Claude --- CLAUDE.md | 14 + .../add_subscription_controller.dart | 63 +--- lib/controllers/detail_screen_controller.dart | 72 ++--- lib/screens/add_subscription_screen.dart | 3 +- lib/screens/app_lock_screen.dart | 22 +- lib/screens/category_management_screen.dart | 63 ++-- lib/screens/detail_screen.dart | 3 +- lib/screens/main_screen.dart | 19 +- lib/screens/settings_screen.dart | 35 ++- lib/screens/sms_scan_screen.dart | 156 +++++----- lib/screens/splash_screen.dart | 39 ++- lib/theme/app_colors.dart | 42 ++- lib/theme/app_theme.dart | 60 ++-- .../add_subscription_app_bar.dart | 6 +- .../add_subscription_event_section.dart | 2 +- .../add_subscription_form.dart | 8 +- .../add_subscription_header.dart | 4 +- lib/widgets/analysis/analysis_badge.dart | 9 +- lib/widgets/analysis/event_analysis_card.dart | 13 +- .../analysis/monthly_expense_chart_card.dart | 11 +- .../analysis/subscription_pie_chart_card.dart | 3 +- .../analysis/total_expense_summary_card.dart | 4 +- lib/widgets/common/buttons/danger_button.dart | 19 +- .../common/buttons/primary_button.dart | 9 +- .../common/buttons/secondary_button.dart | 15 +- lib/widgets/common/cards/section_card.dart | 8 +- .../common/dialogs/confirmation_dialog.dart | 6 +- .../common/dialogs/loading_overlay.dart | 10 +- .../common/form_fields/base_text_field.dart | 14 +- .../common/form_fields/date_picker_field.dart | 12 +- lib/widgets/common/snackbar/app_snackbar.dart | 272 ++++++++++++++++++ lib/widgets/detail/detail_form_section.dart | 8 +- .../dialogs/delete_confirmation_dialog.dart | 182 ++++++++++++ lib/widgets/empty_state_widget.dart | 11 +- lib/widgets/expandable_fab.dart | 6 +- lib/widgets/floating_navigation_bar.dart | 104 ++++--- lib/widgets/glassmorphic_app_bar.dart | 8 +- lib/widgets/glassmorphic_scaffold.dart | 23 +- lib/widgets/glassmorphism_card.dart | 15 +- lib/widgets/main_summary_card.dart | 36 ++- lib/widgets/spring_animation_widget.dart | 3 +- lib/widgets/subscription_card.dart | 39 +-- lib/widgets/subscription_list_widget.dart | 32 ++- lib/widgets/swipeable_subscription_card.dart | 177 +++++++----- lib/widgets/themed_text.dart | 14 +- lib/widgets/website_icon.dart | 2 +- 46 files changed, 1096 insertions(+), 580 deletions(-) create mode 100644 CLAUDE.md create mode 100644 lib/widgets/common/snackbar/app_snackbar.dart create mode 100644 lib/widgets/dialogs/delete_confirmation_dialog.dart diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..18f6eab --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,14 @@ +# Claude 프로젝트 컨텍스트 + +## 언어 설정 +- 모든 답변은 한국어로 제공 +- 기술 용어는 영어와 한국어 병기 가능 + +## 프로젝트 정보 +- Flutter 기반 구독 관리 앱 (SubManager) +- 글래스모피즘 디자인 시스템 적용 중 +- @doc/color.md의 색상 가이드를 전체 UI에 통일성 있게 적용하는 작업 진행 중 + +## 현재 작업 +- 전체 10개 화면과 50개 이상의 위젯에 통일된 글래스모피즘 스타일 적용 +- 색상 시스템 업데이트 및 일관성 있는 UI 구현 \ No newline at end of file diff --git a/lib/controllers/add_subscription_controller.dart b/lib/controllers/add_subscription_controller.dart index 86da619..f0c87a3 100644 --- a/lib/controllers/add_subscription_controller.dart +++ b/lib/controllers/add_subscription_controller.dart @@ -6,6 +6,7 @@ import '../providers/subscription_provider.dart'; import '../providers/category_provider.dart'; import '../services/sms_service.dart'; import '../services/subscription_url_matcher.dart'; +import '../widgets/common/snackbar/app_snackbar.dart'; /// AddSubscriptionScreen의 비즈니스 로직을 관리하는 Controller class AddSubscriptionController { @@ -232,21 +233,9 @@ class AddSubscriptionController { final granted = await SMSService.requestSMSPermission(); if (!granted) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Row( - children: [ - Icon(Icons.error_outline, color: Colors.white), - SizedBox(width: 12), - Expanded(child: Text('SMS 권한이 필요합니다.')), - ], - ), - behavior: SnackBarBehavior.floating, - backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), + AppSnackBar.showError( + context: context, + message: 'SMS 권한이 필요합니다.', ); } return; @@ -256,21 +245,9 @@ class AddSubscriptionController { final subscriptions = await SMSService.scanSubscriptions(); if (subscriptions.isEmpty) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Row( - children: [ - Icon(Icons.info_outline, color: Colors.white), - SizedBox(width: 12), - Expanded(child: Text('구독 관련 SMS를 찾을 수 없습니다.')), - ], - ), - behavior: SnackBarBehavior.floating, - backgroundColor: Colors.orange, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), + AppSnackBar.showWarning( + context: context, + message: '구독 관련 SMS를 찾을 수 없습니다.', ); } return; @@ -331,21 +308,9 @@ class AddSubscriptionController { }); } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.error_outline, color: Colors.white), - const SizedBox(width: 12), - Expanded(child: Text('SMS 스캔 중 오류 발생: $e')), - ], - ), - behavior: SnackBarBehavior.floating, - backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), + AppSnackBar.showError( + context: context, + message: 'SMS 스캔 중 오류 발생: $e', ); } } finally { @@ -399,11 +364,9 @@ class AddSubscriptionController { }); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('저장 중 오류가 발생했습니다: $e'), - backgroundColor: Colors.red, - ), + AppSnackBar.showError( + context: context, + message: '저장 중 오류가 발생했습니다: $e', ); } } diff --git a/lib/controllers/detail_screen_controller.dart b/lib/controllers/detail_screen_controller.dart index 1813514..e262865 100644 --- a/lib/controllers/detail_screen_controller.dart +++ b/lib/controllers/detail_screen_controller.dart @@ -7,6 +7,8 @@ import '../providers/category_provider.dart'; import '../services/subscription_url_matcher.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:intl/intl.dart'; +import '../widgets/dialogs/delete_confirmation_dialog.dart'; +import '../widgets/common/snackbar/app_snackbar.dart'; /// DetailScreen의 비즈니스 로직을 관리하는 Controller class DetailScreenController { @@ -313,20 +315,9 @@ class DetailScreenController { await provider.updateSubscription(subscription); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Row( - children: [ - Icon(Icons.check_circle_rounded, color: Colors.white), - SizedBox(width: 12), - Text('구독 정보가 업데이트되었습니다.'), - ], - ), - behavior: SnackBarBehavior.floating, - backgroundColor: const Color(0xFF10B981), - duration: const Duration(seconds: 2), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), + AppSnackBar.showSuccess( + context: context, + message: '구독 정보가 업데이트되었습니다.', ); // 변경 사항이 반영될 시간을 주기 위해 짧게 지연 후 결과 반환 @@ -340,26 +331,27 @@ class DetailScreenController { /// 구독 삭제 Future deleteSubscription() async { if (context.mounted) { - final provider = Provider.of(context, listen: false); - await provider.deleteSubscription(subscription.id); + // 삭제 확인 다이얼로그 표시 + final shouldDelete = await DeleteConfirmationDialog.show( + context: context, + serviceName: subscription.serviceName, + ); + if (!shouldDelete) return; + + // 사용자가 확인한 경우에만 삭제 진행 if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Row( - children: [ - Icon(Icons.delete_forever_rounded, color: Colors.white), - SizedBox(width: 12), - Text('구독이 삭제되었습니다.'), - ], - ), - behavior: SnackBarBehavior.floating, - backgroundColor: const Color(0xFFDC2626), - duration: const Duration(seconds: 2), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - ); - Navigator.of(context).pop(); + final provider = Provider.of(context, listen: false); + await provider.deleteSubscription(subscription.id); + + if (context.mounted) { + AppSnackBar.showSuccess( + context: context, + message: '구독이 삭제되었습니다.', + icon: Icons.delete_forever_rounded, + ); + Navigator.of(context).pop(); + } } } } @@ -371,21 +363,17 @@ class DetailScreenController { final Uri url = Uri.parse(subscription.websiteUrl!); if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('웹사이트를 열 수 없습니다.'), - backgroundColor: Colors.red, - ), + AppSnackBar.showError( + context: context, + message: '웹사이트를 열 수 없습니다.', ); } } } else { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('웹사이트 정보가 없습니다. 해지는 웹사이트에서 진행해주세요.'), - backgroundColor: Colors.orange, - ), + AppSnackBar.showWarning( + context: context, + message: '웹사이트 정보가 없습니다. 해지는 웹사이트에서 진행해주세요.', ); } } diff --git a/lib/screens/add_subscription_screen.dart b/lib/screens/add_subscription_screen.dart index 1ab6662..c407572 100644 --- a/lib/screens/add_subscription_screen.dart +++ b/lib/screens/add_subscription_screen.dart @@ -5,6 +5,7 @@ import '../widgets/add_subscription/add_subscription_header.dart'; import '../widgets/add_subscription/add_subscription_form.dart'; import '../widgets/add_subscription/add_subscription_event_section.dart'; import '../widgets/add_subscription/add_subscription_save_button.dart'; +import '../theme/app_colors.dart'; /// 새로운 구독을 추가하는 화면 class AddSubscriptionScreen extends StatefulWidget { @@ -44,7 +45,7 @@ class _AddSubscriptionScreenState extends State _controller.scrollController.addListener(_onScroll); return Scaffold( - backgroundColor: const Color(0xFFF8FAFC), + backgroundColor: AppColors.backgroundColor, extendBodyBehindAppBar: true, appBar: AddSubscriptionAppBar( controller: _controller, diff --git a/lib/screens/app_lock_screen.dart b/lib/screens/app_lock_screen.dart index c3a9184..1e139a7 100644 --- a/lib/screens/app_lock_screen.dart +++ b/lib/screens/app_lock_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_lock_provider.dart'; +import '../theme/app_colors.dart'; class AppLockScreen extends StatelessWidget { const AppLockScreen({super.key}); @@ -12,25 +13,26 @@ class AppLockScreen extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.lock_outline, size: 80, - color: Colors.grey, + color: AppColors.navyGray, ), const SizedBox(height: 24), - const Text( + Text( '앱이 잠겨 있습니다', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, + color: AppColors.darkNavy, ), ), const SizedBox(height: 16), - const Text( + Text( '생체 인증으로 잠금을 해제하세요', style: TextStyle( fontSize: 16, - color: Colors.grey, + color: AppColors.navyGray, ), ), const SizedBox(height: 32), @@ -40,8 +42,14 @@ class AppLockScreen extends StatelessWidget { final success = await appLock.authenticate(); if (!success && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('인증에 실패했습니다. 다시 시도해주세요.'), + SnackBar( + content: Text( + '인증에 실패했습니다. 다시 시도해주세요.', + style: TextStyle( + color: AppColors.pureWhite, + ), + ), + backgroundColor: AppColors.dangerColor, ), ); } diff --git a/lib/screens/category_management_screen.dart b/lib/screens/category_management_screen.dart index 1884938..1e00b22 100644 --- a/lib/screens/category_management_screen.dart +++ b/lib/screens/category_management_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/category_provider.dart'; +import '../theme/app_colors.dart'; class CategoryManagementScreen extends StatefulWidget { const CategoryManagementScreen({super.key}); @@ -41,8 +42,13 @@ class _CategoryManagementScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('카테고리 관리'), - backgroundColor: const Color(0xFF1976D2), + title: Text( + '카테고리 관리', + style: TextStyle( + color: AppColors.pureWhite, + ), + ), + backgroundColor: AppColors.primaryColor, ), body: Consumer( builder: (context, provider, child) { @@ -59,8 +65,11 @@ class _CategoryManagementScreenState extends State { children: [ TextFormField( controller: _nameController, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: '카테고리 이름', + labelStyle: TextStyle( + color: AppColors.navyGray, + ), ), validator: (value) { if (value == null || value.isEmpty) { @@ -72,20 +81,23 @@ class _CategoryManagementScreenState extends State { const SizedBox(height: 16), DropdownButtonFormField( value: _selectedColor, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: '색상 선택', + labelStyle: TextStyle( + color: AppColors.navyGray, + ), ), - items: const [ + items: [ DropdownMenuItem( - value: '#1976D2', child: Text('파란색')), + value: '#1976D2', child: Text('파란색', style: TextStyle(color: AppColors.darkNavy))), DropdownMenuItem( - value: '#4CAF50', child: Text('초록색')), + value: '#4CAF50', child: Text('초록색', style: TextStyle(color: AppColors.darkNavy))), DropdownMenuItem( - value: '#FF9800', child: Text('주황색')), + value: '#FF9800', child: Text('주황색', style: TextStyle(color: AppColors.darkNavy))), DropdownMenuItem( - value: '#F44336', child: Text('빨간색')), + value: '#F44336', child: Text('빨간색', style: TextStyle(color: AppColors.darkNavy))), DropdownMenuItem( - value: '#9C27B0', child: Text('보라색')), + value: '#9C27B0', child: Text('보라색', style: TextStyle(color: AppColors.darkNavy))), ], onChanged: (value) { setState(() { @@ -96,19 +108,22 @@ class _CategoryManagementScreenState extends State { const SizedBox(height: 16), DropdownButtonFormField( value: _selectedIcon, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: '아이콘 선택', + labelStyle: TextStyle( + color: AppColors.navyGray, + ), ), - items: const [ + items: [ DropdownMenuItem( - value: 'subscriptions', child: Text('구독')), - DropdownMenuItem(value: 'movie', child: Text('영화')), + value: 'subscriptions', child: Text('구독', style: TextStyle(color: AppColors.darkNavy))), + DropdownMenuItem(value: 'movie', child: Text('영화', style: TextStyle(color: AppColors.darkNavy))), DropdownMenuItem( - value: 'music_note', child: Text('음악')), + value: 'music_note', child: Text('음악', style: TextStyle(color: AppColors.darkNavy))), DropdownMenuItem( - value: 'fitness_center', child: Text('운동')), + value: 'fitness_center', child: Text('운동', style: TextStyle(color: AppColors.darkNavy))), DropdownMenuItem( - value: 'shopping_cart', child: Text('쇼핑')), + value: 'shopping_cart', child: Text('쇼핑', style: TextStyle(color: AppColors.darkNavy))), ], onChanged: (value) { setState(() { @@ -119,7 +134,12 @@ class _CategoryManagementScreenState extends State { const SizedBox(height: 16), ElevatedButton( onPressed: _addCategory, - child: const Text('카테고리 추가'), + child: Text( + '카테고리 추가', + style: TextStyle( + color: AppColors.pureWhite, + ), + ), ), ], ), @@ -141,7 +161,12 @@ class _CategoryManagementScreenState extends State { color: Color( int.parse(category.color.replaceAll('#', '0xFF'))), ), - title: Text(category.name), + title: Text( + category.name, + style: TextStyle( + color: AppColors.darkNavy, + ), + ), trailing: IconButton( icon: const Icon(Icons.delete), onPressed: () async { diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index 3cd35e9..eb7336f 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -6,6 +6,7 @@ import '../widgets/detail/detail_form_section.dart'; import '../widgets/detail/detail_event_section.dart'; import '../widgets/detail/detail_url_section.dart'; import '../widgets/detail/detail_action_buttons.dart'; +import '../theme/app_colors.dart'; /// 구독 상세 정보를 표시하고 편집할 수 있는 화면 class DetailScreen extends StatefulWidget { @@ -46,7 +47,7 @@ class _DetailScreenState extends State final baseColor = _controller.getCardColor(); return Scaffold( - backgroundColor: const Color(0xFFF5F5F7), + backgroundColor: AppColors.backgroundColor, body: CustomScrollView( controller: _controller.scrollController, slivers: [ diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index a061335..9914d3a 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -166,7 +166,7 @@ class _MainScreenState extends State children: [ Icon( Icons.check_circle, - color: Colors.white, + color: AppColors.pureWhite, size: 20, ), SizedBox(width: 12), @@ -175,11 +175,12 @@ class _MainScreenState extends State style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, + color: AppColors.pureWhite, ), ), ], ), - backgroundColor: const Color(0xFF10B981), // 초록색 + backgroundColor: AppColors.successColor, behavior: SnackBarBehavior.floating, margin: EdgeInsets.only( top: MediaQuery.of(context).padding.top + 16, // 상단 여백 @@ -219,19 +220,9 @@ class _MainScreenState extends State @override Widget build(BuildContext context) { final navigationProvider = context.watch(); - final hour = DateTime.now().hour; - List backgroundGradient; - // 시간대별 배경 그라디언트 설정 - if (hour >= 6 && hour < 10) { - backgroundGradient = AppColors.morningGradient; - } else if (hour >= 10 && hour < 17) { - backgroundGradient = AppColors.dayGradient; - } else if (hour >= 17 && hour < 20) { - backgroundGradient = AppColors.eveningGradient; - } else { - backgroundGradient = AppColors.nightGradient; - } + // 메인 그라데이션 사용 + List backgroundGradient = AppColors.mainGradient; // 현재 인덱스가 유효한지 확인 int currentIndex = navigationProvider.currentIndex; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 4d6467b..0190900 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -8,6 +8,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../providers/theme_provider.dart'; import '../theme/adaptive_theme.dart'; import '../widgets/glassmorphism_card.dart'; +import '../theme/app_colors.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @@ -24,13 +25,13 @@ class SettingsScreen extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: isSelected - ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.2) + ? AppColors.primaryColor.withValues(alpha: 0.2) : Colors.transparent, borderRadius: BorderRadius.circular(8), border: Border.all( color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline.withValues(alpha: 0.5), + ? AppColors.primaryColor + : AppColors.textSecondary.withValues(alpha: 0.5), width: isSelected ? 2 : 1, ), ), @@ -43,8 +44,8 @@ class SettingsScreen extends StatelessWidget { ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, + ? AppColors.primaryColor + : AppColors.textSecondary, size: 24, ), const SizedBox(height: 6), @@ -54,8 +55,8 @@ class SettingsScreen extends StatelessWidget { fontSize: 14, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, + ? AppColors.primaryColor + : AppColors.textPrimary, ), ), ], @@ -187,8 +188,14 @@ class SettingsScreen extends StatelessWidget { provider.setEnabled(true); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('알림 권한이 거부되었습니다'), + SnackBar( + content: Text( + '알림 권한이 거부되었습니다', + style: TextStyle( + color: AppColors.pureWhite, + ), + ), + backgroundColor: AppColors.dangerColor, ), ); } @@ -449,7 +456,15 @@ class SettingsScreen extends StatelessWidget { } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('스토어를 열 수 없습니다')), + SnackBar( + content: Text( + '스토어를 열 수 없습니다', + style: TextStyle( + color: AppColors.pureWhite, + ), + ), + backgroundColor: AppColors.dangerColor, + ), ); } } diff --git a/lib/screens/sms_scan_screen.dart b/lib/screens/sms_scan_screen.dart index 62c9c6d..bacf8f3 100644 --- a/lib/screens/sms_scan_screen.dart +++ b/lib/screens/sms_scan_screen.dart @@ -9,6 +9,11 @@ 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'; class SmsScanScreen extends StatefulWidget { const SmsScanScreen({super.key}); @@ -352,41 +357,9 @@ class _SmsScanScreenState extends State { // 성공 메시지 표시 if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon( - Icons.check_circle, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - '${subscription.serviceName} 구독이 추가되었습니다.', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - backgroundColor: const Color(0xFF10B981), // 초록색 - behavior: SnackBarBehavior.floating, - margin: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + 16, // 상단 여백 - left: 16, - right: 16, - bottom: MediaQuery.of(context).size.height - 120, // 상단에 위치하도록 bottom 마진 설정 - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - duration: const Duration(seconds: 3), - dismissDirection: DismissDirection.horizontal, - ), + AppSnackBar.showSuccess( + context: context, + message: '${subscription.serviceName} 구독이 추가되었습니다.', ); } @@ -395,12 +368,9 @@ class _SmsScanScreenState extends State { } catch (e) { print('구독 추가 중 오류 발생: $e'); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('구독 추가 중 오류가 발생했습니다: $e'), - backgroundColor: Colors.red, - duration: const Duration(seconds: 2), - ), + AppSnackBar.showError( + context: context, + message: '구독 추가 중 오류가 발생했습니다: $e', ); // 오류가 있어도 다음 구독으로 이동 @@ -411,6 +381,16 @@ class _SmsScanScreenState extends State { // 현재 구독 건너뛰기 void _skipCurrentSubscription() { + final subscription = _scannedSubscriptions[_currentIndex]; + + if (mounted) { + AppSnackBar.showInfo( + context: context, + message: '${subscription.serviceName} 구독을 건너뛰었습니다.', + icon: Icons.skip_next_rounded, + ); + } + _moveToNextSubscription(); } @@ -434,12 +414,9 @@ class _SmsScanScreenState extends State { navigationProvider.updateCurrentIndex(0); // 완료 메시지 표시 - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('모든 구독이 처리되었습니다.'), - backgroundColor: Colors.green, - duration: Duration(seconds: 2), - ), + AppSnackBar.showSuccess( + context: context, + message: '모든 구독이 처리되었습니다.', ); } @@ -530,15 +507,17 @@ class _SmsScanScreenState extends State { // 로딩 상태 UI Widget _buildLoadingState() { - return const Center( + return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - ThemedText('SMS 메시지를 스캔 중입니다...'), - SizedBox(height: 8), - ThemedText('구독 서비스를 찾고 있습니다', opacity: 0.7), + 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), ], ), ); @@ -564,6 +543,7 @@ class _SmsScanScreenState extends State { '2회 이상 결제된 구독 서비스 찾기', fontSize: 20, fontWeight: FontWeight.bold, + forceDark: true, ), const SizedBox(height: 16), const Padding( @@ -572,16 +552,17 @@ class _SmsScanScreenState extends State { '문자 메시지를 스캔하여 반복적으로 결제된 구독 서비스를 자동으로 찾습니다. 서비스명과 금액을 추출하여 쉽게 구독을 추가할 수 있습니다.', textAlign: TextAlign.center, opacity: 0.7, + forceDark: true, ), ), const SizedBox(height: 32), - ElevatedButton.icon( + PrimaryButton( + text: '스캔 시작하기', + icon: Icons.search_rounded, onPressed: _scanSms, - icon: const Icon(Icons.search), - label: const Text('스캔 시작하기'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), - ), + width: 200, + height: 56, + backgroundColor: AppColors.primaryColor, ), ], ), @@ -591,9 +572,10 @@ class _SmsScanScreenState extends State { // 구독 표시 상태 UI Widget _buildSubscriptionState() { if (_currentIndex >= _scannedSubscriptions.length) { - return const Center( - child: ThemedText('모든 구독 처리 완료'), - ); + // 처리 완료 후 초기 상태로 복귀 + _scannedSubscriptions = []; + _currentIndex = 0; + return _buildInitialState(); // 스캔 버튼이 있는 초기 화면으로 돌아감 } final subscription = _scannedSubscriptions[_currentIndex]; @@ -609,7 +591,7 @@ class _SmsScanScreenState extends State { // 진행 상태 표시 LinearProgressIndicator( value: (_currentIndex + 1) / _scannedSubscriptions.length, - backgroundColor: Colors.grey.withValues(alpha: 0.2), + backgroundColor: AppColors.navyGray.withValues(alpha: 0.2), valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary), ), @@ -618,6 +600,7 @@ class _SmsScanScreenState extends State { '${_currentIndex + 1}/${_scannedSubscriptions.length}', fontWeight: FontWeight.w500, opacity: 0.7, + forceDark: true, ), const SizedBox(height: 24), @@ -632,6 +615,7 @@ class _SmsScanScreenState extends State { '다음 구독을 찾았습니다', fontSize: 18, fontWeight: FontWeight.bold, + forceDark: true, ), const SizedBox(height: 24), // 서비스명 @@ -639,12 +623,14 @@ class _SmsScanScreenState extends State { '서비스명', 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), @@ -659,6 +645,7 @@ class _SmsScanScreenState extends State { '월 비용', fontWeight: FontWeight.w500, opacity: 0.7, + forceDark: true, ), const SizedBox(height: 4), ThemedText( @@ -675,6 +662,7 @@ class _SmsScanScreenState extends State { ).format(subscription.monthlyCost), fontSize: 18, fontWeight: FontWeight.bold, + forceDark: true, ), ], ), @@ -687,6 +675,7 @@ class _SmsScanScreenState extends State { '반복 횟수', fontWeight: FontWeight.w500, opacity: 0.7, + forceDark: true, ), const SizedBox(height: 4), ThemedText( @@ -713,12 +702,14 @@ class _SmsScanScreenState extends State { '결제 주기', fontWeight: FontWeight.w500, opacity: 0.7, + forceDark: true, ), const SizedBox(height: 4), ThemedText( subscription.billingCycle, fontSize: 16, fontWeight: FontWeight.w500, + forceDark: true, ), ], ), @@ -731,12 +722,14 @@ class _SmsScanScreenState extends State { '결제일', fontWeight: FontWeight.w500, opacity: 0.7, + forceDark: true, ), const SizedBox(height: 4), ThemedText( _getNextBillingText(subscription.nextBillingDate), fontSize: 14, fontWeight: FontWeight.w500, + forceDark: true, ), ], ), @@ -746,17 +739,18 @@ class _SmsScanScreenState extends State { const SizedBox(height: 24), // 웹사이트 URL 입력 필드 추가/수정 - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _websiteUrlController, - decoration: const InputDecoration( - labelText: '웹사이트 URL (자동 추출됨)', - hintText: '웹사이트 URL을 수정하거나 비워두세요', - prefixIcon: Icon(Icons.language), - border: OutlineInputBorder(), - ), + 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), @@ -764,22 +758,18 @@ class _SmsScanScreenState extends State { Row( children: [ Expanded( - child: OutlinedButton( + child: SecondaryButton( + text: '건너뛰기', onPressed: _skipCurrentSubscription, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - ), - child: const Text('건너뛰기'), + height: 48, ), ), const SizedBox(width: 16), Expanded( - child: ElevatedButton( + child: PrimaryButton( + text: '추가하기', onPressed: _addCurrentSubscription, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - ), - child: const Text('추가하기'), + height: 48, ), ), ], diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index ac3bceb..caacece 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -135,7 +135,7 @@ class _SplashScreenState extends State // 글래스모피즘 오버레이 Container( decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.05), + color: AppColors.pureWhite.withValues(alpha: 0.05), ), ), Stack( @@ -188,8 +188,8 @@ class _SplashScreenState extends State shape: BoxShape.circle, gradient: RadialGradient( colors: [ - Colors.white.withValues(alpha: 0.1), - Colors.white.withValues(alpha: 0.0), + AppColors.pureWhite.withValues(alpha: 0.1), + AppColors.pureWhite.withValues(alpha: 0.0), ], stops: const [0.2, 1.0], ), @@ -208,8 +208,8 @@ class _SplashScreenState extends State shape: BoxShape.circle, gradient: RadialGradient( colors: [ - Colors.white.withValues(alpha: 0.07), - Colors.white.withValues(alpha: 0.0), + AppColors.pureWhite.withValues(alpha: 0.07), + AppColors.pureWhite.withValues(alpha: 0.0), ], stops: const [0.4, 1.0], ), @@ -250,23 +250,22 @@ class _SplashScreenState extends State begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - Colors.white + AppColors.pureWhite .withValues(alpha: 0.2), - Colors.white + AppColors.pureWhite .withValues(alpha: 0.1), ], ), borderRadius: BorderRadius.circular(30), border: Border.all( - color: Colors.white + color: AppColors.pureWhite .withValues(alpha: 0.3), width: 1.5, ), boxShadow: [ BoxShadow( - color: Colors.black - .withValues(alpha: 0.1), + color: AppColors.shadowBlack, spreadRadius: 0, blurRadius: 30, offset: const Offset(0, 10), @@ -323,12 +322,12 @@ class _SplashScreenState extends State ), ); }, - child: const Text( + child: Text( 'SubManager', style: TextStyle( fontSize: 36, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.pureWhite, letterSpacing: 1.2, ), ), @@ -349,11 +348,11 @@ class _SplashScreenState extends State ), ); }, - child: const Text( + child: Text( '구독 서비스 관리를 더 쉽게', style: TextStyle( fontSize: 16, - color: Colors.white70, + color: AppColors.pureWhite.withValues(alpha: 0.7), letterSpacing: 0.5, ), ), @@ -374,17 +373,17 @@ class _SplashScreenState extends State height: 60, padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.1), + color: AppColors.pureWhite.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(50), border: Border.all( color: - Colors.white.withValues(alpha: 0.2), + AppColors.pureWhite.withValues(alpha: 0.2), width: 1, ), ), - child: const CircularProgressIndicator( + child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation( - Colors.white), + AppColors.pureWhite), strokeWidth: 3, ), ), @@ -401,11 +400,11 @@ class _SplashScreenState extends State padding: const EdgeInsets.only(bottom: 24.0), child: FadeTransition( opacity: _fadeAnimation, - child: const Text( + child: Text( '© 2023 CClabs. All rights reserved.', style: TextStyle( fontSize: 12, - color: Colors.white60, + color: AppColors.pureWhite.withValues(alpha: 0.6), letterSpacing: 0.5, ), ), diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index ee1f155..e738499 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; class AppColors { // 메인 컬러 (Metronic Tailwind 스타일) - static const primaryColor = Color(0xFF3B82F6); // 메트로닉 블루 - static const secondaryColor = Color(0xFF64748B); // 슬레이트 600 - static const successColor = Color(0xFF10B981); // 그린 + static const primaryColor = Color(0xFF2563EB); // 딥 블루 + static const secondaryColor = Color(0xFF60A5FA); // 스카이 블루 + static const successColor = Color(0xFF38BDF8); // 소프트 민트 static const infoColor = Color(0xFF6366F1); // 인디고 static const warningColor = Color(0xFFF59E0B); // 앰버 - static const dangerColor = Color(0xFFEF4444); // 레드 + static const dangerColor = Color(0xFFF472B6); // 핑크 액센트 // 배경색 static const backgroundColor = Color(0xFFF1F5F9); // 슬레이트 100 @@ -17,18 +17,24 @@ class AppColors { // 텍스트 컬러 static const textPrimary = Color(0xFF1E293B); // 슬레이트 800 - static const textSecondary = Color(0xFF64748B); // 슬레이트 600 - static const textMuted = Color(0xFF94A3B8); // 슬레이트 400 + static const darkNavy = Color(0xFF1E293B); // 메인 텍스트 (color.md 가이드) + static const textSecondary = Color(0xFF334155); // 네이비 그레이 + static const navyGray = Color(0xFF334155); // 서브 텍스트 (color.md 가이드) + static const textMuted = Color(0xFF334155); // 네이비 그레이 static const textLight = Color(0xFFFFFFFF); // 화이트 + static const pureWhite = Color(0xFFFFFFFF); // 버튼 텍스트용 (color.md 가이드) // 보더 & 디바이더 static const borderColor = Color(0xFFE2E8F0); // 슬레이트 200 static const dividerColor = Color(0xFFE2E8F0); // 슬레이트 200 + + // 그림자 (color.md 가이드) + static const shadowBlack = Color(0x14000000); // rgba(0,0,0,0.08) - 8% opacity // 그라데이션 컬러 - 다양한 효과를 위한 조합 static const List blueGradient = [ - Color(0xFF3B82F6), - Color(0xFF2563EB) + Color(0xFF2563EB), // 딥 블루 + Color(0xFF60A5FA) // 스카이 블루 ]; static const List tealGradient = [ Color(0xFF14B8A6), @@ -48,10 +54,10 @@ class AppColors { ]; // Glassmorphism 효과를 위한 색상 - static const glassSurface = Color(0x0FFFFFFF); // 매우 연한 흰색 (6% opacity) - static const glassBackground = Color(0x1AFFFFFF); // 연한 흰색 (10% opacity) + static const glassSurface = Color(0x33FFFFFF); // 화이트 글래스 (20% opacity) + static const glassBackground = Color(0x33FFFFFF); // 화이트 글래스 (20% opacity) static const glassCard = Color(0x33FFFFFF); // 반투명 흰색 (20% opacity) - static const glassBorder = Color(0x4DFFFFFF); // 반투명 테두리 (30% opacity) + static const glassBorder = Color(0xFF2563EB); // 딥 블루 테두리 static const glassOverlay = Color(0x0D000000); // 연한 검정 오버레이 (5% opacity) // 다크 모드용 Glassmorphism 색상 @@ -62,8 +68,8 @@ class AppColors { // 백드롭 블러 효과를 위한 그라디언트 static const List glassGradient = [ + Color(0x33FFFFFF), // 20% white Color(0x1AFFFFFF), // 10% white - Color(0x0FFFFFFF), // 6% white ]; static const List glassGradientDark = [ @@ -71,6 +77,18 @@ class AppColors { Color(0x0F000000), // 6% black ]; + // 메인 그라데이션 + static const List mainGradient = [ + Color(0xFF2563EB), // 딥 블루 + Color(0xFF60A5FA), // 스카이 블루 + Color(0xFFE0E7EF), // 라이트 그레이 + ]; + + static const List accentGradient = [ + Color(0xFF38BDF8), // 소프트 민트 + Color(0xFF60A5FA), // 스카이 블루 + ]; + // 시간대별 배경 그라디언트 static const List morningGradient = [ Color(0xFFFED7AA), // 따뜻한 오렌지 diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 5faac04..82f2e65 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -17,22 +17,22 @@ class AppTheme { // 기본 배경색 scaffoldBackgroundColor: AppColors.backgroundColor, - // 카드 스타일 - 부드러운 그림자, 둥근 모서리 + // 카드 스타일 - 글래스모피즘 효과 cardTheme: CardTheme( - color: AppColors.cardColor, - elevation: 1, - shadowColor: Colors.black.withValues(alpha: 0.04), + color: AppColors.glassCard, + elevation: 0, + shadowColor: AppColors.shadowBlack, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: const BorderSide(color: AppColors.borderColor, width: 0.5), + side: const BorderSide(color: AppColors.glassBorder, width: 1), ), clipBehavior: Clip.antiAlias, margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), ), - // 앱바 스타일 - 깔끔하고 투명한 디자인 + // 앱바 스타일 - 글래스모피즘 디자인 appBarTheme: const AppBarTheme( - backgroundColor: AppColors.surfaceColor, + backgroundColor: Colors.transparent, foregroundColor: AppColors.textPrimary, elevation: 0, centerTitle: false, @@ -43,7 +43,7 @@ class AppTheme { letterSpacing: -0.2, ), iconTheme: const IconThemeData( - color: AppColors.secondaryColor, + color: AppColors.primaryColor, size: 24, ), ), @@ -52,21 +52,21 @@ class AppTheme { textTheme: const TextTheme( // 헤드라인 - 페이지 제목 headlineLarge: const TextStyle( - color: AppColors.textPrimary, + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 fontSize: 32, fontWeight: FontWeight.w700, letterSpacing: -0.5, height: 1.2, ), headlineMedium: const TextStyle( - color: AppColors.textPrimary, + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 fontSize: 28, fontWeight: FontWeight.w700, letterSpacing: -0.5, height: 1.2, ), headlineSmall: const TextStyle( - color: AppColors.textPrimary, + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 fontSize: 24, fontWeight: FontWeight.w600, letterSpacing: -0.25, @@ -75,21 +75,21 @@ class AppTheme { // 타이틀 - 카드, 섹션 제목 titleLarge: const TextStyle( - color: AppColors.textPrimary, + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 fontSize: 20, fontWeight: FontWeight.w600, letterSpacing: -0.2, height: 1.4, ), titleMedium: TextStyle( - color: AppColors.textPrimary, + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: -0.1, height: 1.4, ), titleSmall: TextStyle( - color: AppColors.textPrimary, + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0, @@ -98,21 +98,21 @@ class AppTheme { // 본문 텍스트 bodyLarge: TextStyle( - color: AppColors.textPrimary, + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 fontSize: 16, fontWeight: FontWeight.w400, letterSpacing: 0.1, height: 1.5, ), bodyMedium: TextStyle( - color: AppColors.textSecondary, + color: AppColors.navyGray, // color.md 가이드: 서브 텍스트 fontSize: 14, fontWeight: FontWeight.w400, letterSpacing: 0.1, height: 1.5, ), bodySmall: TextStyle( - color: AppColors.textMuted, + color: AppColors.navyGray, // color.md 가이드: 서브 텍스트 fontSize: 12, fontWeight: FontWeight.w400, letterSpacing: 0.2, @@ -121,21 +121,21 @@ class AppTheme { // 라벨 텍스트 labelLarge: TextStyle( - color: AppColors.textPrimary, + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.1, height: 1.4, ), labelMedium: TextStyle( - color: AppColors.textSecondary, + color: AppColors.navyGray, // color.md 가이드: 서브 텍스트 fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.2, height: 1.4, ), labelSmall: TextStyle( - color: AppColors.textMuted, + color: AppColors.navyGray, // color.md 가이드: 서브 텍스트 fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: 0.2, @@ -143,10 +143,10 @@ class AppTheme { ), ), - // 입력 필드 스타일 - 깔끔하고 현대적인 디자인 + // 입력 필드 스타일 - 글래스모피즘 디자인 inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: AppColors.surfaceColorAlt, + fillColor: AppColors.glassBackground, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -154,7 +154,7 @@ class AppTheme { ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: AppColors.borderColor, width: 1), + borderSide: const BorderSide(color: AppColors.textSecondary, width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -224,13 +224,13 @@ class AppTheme { // 아웃라인 버튼 스타일 outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - foregroundColor: AppColors.textPrimary, + foregroundColor: AppColors.primaryColor, minimumSize: const Size(0, 48), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - side: const BorderSide(color: AppColors.borderColor, width: 1), + side: const BorderSide(color: AppColors.secondaryColor, width: 1), textStyle: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, @@ -265,7 +265,7 @@ class AppTheme { }), trackColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.selected)) { - return AppColors.primaryColor.withValues(alpha: 0.5); + return AppColors.secondaryColor.withValues(alpha: 0.5); } return AppColors.borderColor; }), @@ -282,7 +282,7 @@ class AppTheme { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), ), - side: const BorderSide(color: AppColors.borderColor, width: 1.5), + side: const BorderSide(color: AppColors.secondaryColor, width: 1.5), ), // 라디오 버튼 스타일 @@ -291,16 +291,16 @@ class AppTheme { if (states.contains(MaterialState.selected)) { return AppColors.primaryColor; } - return AppColors.borderColor; + return AppColors.textSecondary; }), ), // 슬라이더 스타일 sliderTheme: SliderThemeData( activeTrackColor: AppColors.primaryColor, - inactiveTrackColor: AppColors.borderColor, + inactiveTrackColor: AppColors.textSecondary, thumbColor: AppColors.primaryColor, - overlayColor: AppColors.primaryColor.withValues(alpha: 0.2), + overlayColor: AppColors.primaryColor.withValues(alpha: 0.3), trackHeight: 4, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10), overlayShape: const RoundSliderOverlayShape(overlayRadius: 20), diff --git a/lib/widgets/add_subscription/add_subscription_app_bar.dart b/lib/widgets/add_subscription/add_subscription_app_bar.dart index 65a4a8f..911ca9e 100644 --- a/lib/widgets/add_subscription/add_subscription_app_bar.dart +++ b/lib/widgets/add_subscription/add_subscription_app_bar.dart @@ -26,11 +26,11 @@ class AddSubscriptionAppBar extends StatelessWidget implements PreferredSizeWidg return Container( decoration: BoxDecoration( - color: Colors.white.withOpacity(appBarOpacity), + color: Colors.white.withValues(alpha: appBarOpacity), boxShadow: appBarOpacity > 0.6 ? [ BoxShadow( - color: Colors.black.withOpacity(0.1 * appBarOpacity), + color: Colors.black.withValues(alpha: 0.1 * appBarOpacity), spreadRadius: 1, blurRadius: 8, offset: const Offset(0, 4), @@ -51,7 +51,7 @@ class AddSubscriptionAppBar extends StatelessWidget implements PreferredSizeWidg shadows: appBarOpacity > 0.6 ? [ Shadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), offset: const Offset(0, 1), blurRadius: 2, ) diff --git a/lib/widgets/add_subscription/add_subscription_event_section.dart b/lib/widgets/add_subscription/add_subscription_event_section.dart index d88a343..4132135 100644 --- a/lib/widgets/add_subscription/add_subscription_event_section.dart +++ b/lib/widgets/add_subscription/add_subscription_event_section.dart @@ -44,7 +44,7 @@ class AddSubscriptionEventSection extends StatelessWidget { border: Border.all( color: controller.isEventActive ? const Color(0xFF3B82F6) - : Colors.grey.withOpacity(0.2), + : Colors.grey.withValues(alpha: 0.2), width: controller.isEventActive ? 2 : 1, ), ), diff --git a/lib/widgets/add_subscription/add_subscription_form.dart b/lib/widgets/add_subscription/add_subscription_form.dart index 71f06e8..f9eca9b 100644 --- a/lib/widgets/add_subscription/add_subscription_form.dart +++ b/lib/widgets/add_subscription/add_subscription_form.dart @@ -297,7 +297,7 @@ class _CurrencyOption extends StatelessWidget { decoration: BoxDecoration( color: isSelected ? const Color(0xFF3B82F6) - : Colors.grey.withOpacity(0.1), + : Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Center( @@ -350,7 +350,7 @@ class _BillingCycleSelector extends StatelessWidget { decoration: BoxDecoration( color: isSelected ? gradientColors[0] - : Colors.grey.withOpacity(0.1), + : Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( @@ -402,14 +402,14 @@ class _CategorySelector extends StatelessWidget { decoration: BoxDecoration( color: isSelected ? gradientColors[0] - : Colors.grey.withOpacity(0.1), + : Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - category.emoji, + category.icon, style: const TextStyle(fontSize: 16), ), const SizedBox(width: 6), diff --git a/lib/widgets/add_subscription/add_subscription_header.dart b/lib/widgets/add_subscription/add_subscription_header.dart index 8265184..84eaa6f 100644 --- a/lib/widgets/add_subscription/add_subscription_header.dart +++ b/lib/widgets/add_subscription/add_subscription_header.dart @@ -32,7 +32,7 @@ class AddSubscriptionHeader extends StatelessWidget { ), boxShadow: [ BoxShadow( - color: controller.gradientColors[0].withOpacity(0.3), + color: controller.gradientColors[0].withValues(alpha: 0.3), blurRadius: 20, spreadRadius: 0, offset: const Offset(0, 8), @@ -44,7 +44,7 @@ class AddSubscriptionHeader extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16), ), child: const Icon( diff --git a/lib/widgets/analysis/analysis_badge.dart b/lib/widgets/analysis/analysis_badge.dart index c59561c..88ecef9 100644 --- a/lib/widgets/analysis/analysis_badge.dart +++ b/lib/widgets/analysis/analysis_badge.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import '../../models/subscription_model.dart'; import '../../services/currency_util.dart'; +import '../../theme/app_colors.dart'; /// 파이 차트에서 선택된 섹션에 표시되는 배지 위젯 class AnalysisBadge extends StatelessWidget { @@ -23,7 +24,7 @@ class AnalysisBadge extends StatelessWidget { width: size, height: size, decoration: BoxDecoration( - color: Colors.white, + color: AppColors.pureWhite, shape: BoxShape.circle, border: Border.all( color: borderColor, @@ -31,7 +32,7 @@ class AnalysisBadge extends StatelessWidget { ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.5), + color: AppColors.shadowBlack, blurRadius: 10, spreadRadius: 2, ), @@ -48,7 +49,7 @@ class AnalysisBadge extends StatelessWidget { style: const TextStyle( fontSize: 8, fontWeight: FontWeight.bold, - color: Colors.black87, + color: AppColors.darkNavy, ), ), const SizedBox(height: 2), @@ -68,7 +69,7 @@ class AnalysisBadge extends StatelessWidget { displayText, style: const TextStyle( fontSize: 7, - color: Colors.black54, + color: AppColors.navyGray, ), ); } diff --git a/lib/widgets/analysis/event_analysis_card.dart b/lib/widgets/analysis/event_analysis_card.dart index 233c3e8..3d8ef4a 100644 --- a/lib/widgets/analysis/event_analysis_card.dart +++ b/lib/widgets/analysis/event_analysis_card.dart @@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import '../../providers/subscription_provider.dart'; import '../../services/currency_util.dart'; +import '../../theme/app_colors.dart'; import '../glassmorphism_card.dart'; import '../themed_text.dart'; @@ -73,7 +74,7 @@ class EventAnalysisCard extends StatelessWidget { const FaIcon( FontAwesomeIcons.fire, size: 12, - color: Colors.white, + color: AppColors.pureWhite, ), const SizedBox(width: 4), Text( @@ -81,7 +82,7 @@ class EventAnalysisCard extends StatelessWidget { style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.pureWhite, ), ), ], @@ -159,10 +160,10 @@ class EventAnalysisCard extends StatelessWidget { margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.05), + color: AppColors.darkNavy.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.white.withValues(alpha: 0.1), + color: AppColors.darkNavy.withValues(alpha: 0.1), ), ), child: Row( @@ -194,7 +195,7 @@ class EventAnalysisCard extends StatelessWidget { fontSize: 12, decoration: TextDecoration .lineThrough, - color: Colors.grey, + color: AppColors.navyGray, ), ); } @@ -205,7 +206,7 @@ class EventAnalysisCard extends StatelessWidget { const Icon( Icons.arrow_forward, size: 12, - color: Colors.grey, + color: AppColors.navyGray, ), const SizedBox(width: 8), FutureBuilder( diff --git a/lib/widgets/analysis/monthly_expense_chart_card.dart b/lib/widgets/analysis/monthly_expense_chart_card.dart index 24041af..1e1f20d 100644 --- a/lib/widgets/analysis/monthly_expense_chart_card.dart +++ b/lib/widgets/analysis/monthly_expense_chart_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'dart:math' as math; import '../../services/currency_util.dart'; +import '../../theme/app_colors.dart'; import '../glassmorphism_card.dart'; import '../themed_text.dart'; @@ -44,7 +45,7 @@ class MonthlyExpenseChartCard extends StatelessWidget { backDrawRodData: BackgroundBarChartRodData( show: true, toY: maxAmount + (maxAmount * 0.1), - color: Colors.grey.withValues(alpha: 0.1), + color: AppColors.navyGray.withValues(alpha: 0.1), ), ), ], @@ -124,7 +125,7 @@ class MonthlyExpenseChartCard extends StatelessWidget { ), getDrawingHorizontalLine: (value) { return FlLine( - color: Colors.grey.withValues(alpha: 0.1), + color: AppColors.navyGray.withValues(alpha: 0.1), strokeWidth: 1, ); }, @@ -163,14 +164,14 @@ class MonthlyExpenseChartCard extends StatelessWidget { barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( - tooltipBgColor: Colors.blueGrey.shade800, + tooltipBgColor: AppColors.darkNavy, tooltipRoundedRadius: 8, getTooltipItem: (group, groupIndex, rod, rodIndex) { return BarTooltipItem( '${monthlyData[group.x]['monthName']}\n', const TextStyle( - color: Colors.white, + color: AppColors.pureWhite, fontWeight: FontWeight.bold, ), children: [ @@ -179,7 +180,7 @@ class MonthlyExpenseChartCard extends StatelessWidget { monthlyData[group.x]['totalExpense'] as double), style: const TextStyle( - color: Colors.yellow, + color: Color(0xFFFBBF24), fontSize: 14, fontWeight: FontWeight.w500, ), diff --git a/lib/widgets/analysis/subscription_pie_chart_card.dart b/lib/widgets/analysis/subscription_pie_chart_card.dart index a152ec0..b43aff2 100644 --- a/lib/widgets/analysis/subscription_pie_chart_card.dart +++ b/lib/widgets/analysis/subscription_pie_chart_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import '../../models/subscription_model.dart'; import '../../services/currency_util.dart'; +import '../../theme/app_colors.dart'; import '../glassmorphism_card.dart'; import '../themed_text.dart'; import 'analysis_badge.dart'; @@ -68,7 +69,7 @@ class SubscriptionPieChartCard extends StatelessWidget { titleStyle: TextStyle( fontSize: fontSize, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.pureWhite, shadows: const [ Shadow(color: Colors.black26, blurRadius: 2, offset: Offset(0, 1)) ], diff --git a/lib/widgets/analysis/total_expense_summary_card.dart b/lib/widgets/analysis/total_expense_summary_card.dart index a6231f3..b55923b 100644 --- a/lib/widgets/analysis/total_expense_summary_card.dart +++ b/lib/widgets/analysis/total_expense_summary_card.dart @@ -139,7 +139,7 @@ class TotalExpenseSummaryCard extends StatelessWidget { child: const FaIcon( FontAwesomeIcons.listCheck, size: 16, - color: Colors.blue, + color: AppColors.primaryColor, ), ), const SizedBox(width: 12), @@ -181,7 +181,7 @@ class TotalExpenseSummaryCard extends StatelessWidget { child: const FaIcon( FontAwesomeIcons.chartLine, size: 16, - color: Colors.green, + color: AppColors.successColor, ), ), const SizedBox(width: 12), diff --git a/lib/widgets/common/buttons/danger_button.dart b/lib/widgets/common/buttons/danger_button.dart index 65967d8..53a1387 100644 --- a/lib/widgets/common/buttons/danger_button.dart +++ b/lib/widgets/common/buttons/danger_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../theme/app_colors.dart'; /// 위험한 액션에 사용되는 Danger 버튼 /// 삭제, 취소, 종료 등의 위험한 액션에 사용됩니다. @@ -39,7 +40,7 @@ class DangerButton extends StatefulWidget { class _DangerButtonState extends State { bool _isHovered = false; - static const Color _dangerColor = Color(0xFFDC2626); + static const Color _dangerColor = AppColors.dangerColor; Future _handlePress() async { if (widget.requireConfirmation) { @@ -62,7 +63,7 @@ class _DangerButtonState extends State { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: _dangerColor.withOpacity(0.1), + color: _dangerColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( @@ -98,7 +99,7 @@ class _DangerButtonState extends State { ), child: Text( widget.text, - style: const TextStyle(color: Colors.white), + style: const TextStyle(color: AppColors.pureWhite), ), ), ], @@ -126,14 +127,14 @@ class _DangerButtonState extends State { onPressed: widget.onPressed != null ? _handlePress : null, style: ElevatedButton.styleFrom( backgroundColor: _dangerColor, - foregroundColor: Colors.white, + foregroundColor: AppColors.pureWhite, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(widget.borderRadius), ), padding: widget.padding ?? const EdgeInsets.symmetric(vertical: 16), - elevation: widget.enableHoverEffect && _isHovered ? 8 : 4, - shadowColor: _dangerColor.withOpacity(0.5), - disabledBackgroundColor: _dangerColor.withOpacity(0.6), + elevation: widget.enableHoverEffect && _isHovered ? 2 : 0, + shadowColor: Colors.black.withValues(alpha: 0.08), + disabledBackgroundColor: _dangerColor.withValues(alpha: 0.6), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -142,7 +143,7 @@ class _DangerButtonState extends State { if (widget.icon != null) ...[ Icon( widget.icon, - color: Colors.white, + color: AppColors.pureWhite, size: _isHovered ? 24 : 20, ), const SizedBox(width: 8), @@ -152,7 +153,7 @@ class _DangerButtonState extends State { style: TextStyle( fontSize: widget.fontSize, fontWeight: FontWeight.w600, - color: Colors.white, + color: AppColors.pureWhite, ), ), ], diff --git a/lib/widgets/common/buttons/primary_button.dart b/lib/widgets/common/buttons/primary_button.dart index 49a598c..9dd3434 100644 --- a/lib/widgets/common/buttons/primary_button.dart +++ b/lib/widgets/common/buttons/primary_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../theme/app_colors.dart'; /// 주요 액션에 사용되는 Primary 버튼 /// 저장, 추가, 확인 등의 주요 액션에 사용됩니다. @@ -43,7 +44,7 @@ class _PrimaryButtonState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final effectiveBackgroundColor = widget.backgroundColor ?? theme.primaryColor; - final effectiveForegroundColor = widget.foregroundColor ?? Colors.white; + final effectiveForegroundColor = widget.foregroundColor ?? AppColors.pureWhite; Widget button = AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -61,9 +62,9 @@ class _PrimaryButtonState extends State { borderRadius: BorderRadius.circular(widget.borderRadius), ), padding: widget.padding ?? const EdgeInsets.symmetric(vertical: 16), - elevation: widget.enableHoverEffect && _isHovered ? 8 : 4, - shadowColor: effectiveBackgroundColor.withOpacity(0.5), - disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.6), + elevation: widget.enableHoverEffect && _isHovered ? 2 : 0, + shadowColor: Colors.black.withValues(alpha: 0.08), + disabledBackgroundColor: effectiveBackgroundColor.withValues(alpha: 0.6), ), child: widget.isLoading ? SizedBox( diff --git a/lib/widgets/common/buttons/secondary_button.dart b/lib/widgets/common/buttons/secondary_button.dart index bcbc2a1..30a0258 100644 --- a/lib/widgets/common/buttons/secondary_button.dart +++ b/lib/widgets/common/buttons/secondary_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../theme/app_colors.dart'; /// 부차적인 액션에 사용되는 Secondary 버튼 /// 취소, 되돌아가기, 부가 옵션 등에 사용됩니다. @@ -42,10 +43,8 @@ class _SecondaryButtonState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final effectiveBorderColor = widget.borderColor ?? - theme.colorScheme.onSurface.withOpacity(0.2); - final effectiveTextColor = widget.textColor ?? - theme.colorScheme.onSurface.withOpacity(0.8); + final effectiveBorderColor = widget.borderColor ?? AppColors.secondaryColor; + final effectiveTextColor = widget.textColor ?? AppColors.primaryColor; Widget button = AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -63,7 +62,7 @@ class _SecondaryButtonState extends State { ), side: BorderSide( color: _isHovered - ? effectiveBorderColor.withOpacity(0.4) + ? effectiveBorderColor.withValues(alpha: 0.4) : effectiveBorderColor, width: widget.borderWidth, ), @@ -72,7 +71,7 @@ class _SecondaryButtonState extends State { horizontal: 24, ), backgroundColor: _isHovered - ? theme.colorScheme.onSurface.withOpacity(0.05) + ? AppColors.glassBackground : Colors.transparent, ), child: Row( @@ -142,13 +141,13 @@ class _TextLinkButtonState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final effectiveColor = widget.color ?? theme.colorScheme.primary; + final effectiveColor = widget.color ?? AppColors.primaryColor; Widget button = AnimatedContainer( duration: const Duration(milliseconds: 200), decoration: BoxDecoration( color: _isHovered - ? theme.colorScheme.onSurface.withOpacity(0.05) + ? theme.colorScheme.onSurface.withValues(alpha: 0.05) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), diff --git a/lib/widgets/common/cards/section_card.dart b/lib/widgets/common/cards/section_card.dart index 1680d66..3b83c18 100644 --- a/lib/widgets/common/cards/section_card.dart +++ b/lib/widgets/common/cards/section_card.dart @@ -36,7 +36,7 @@ class SectionCard extends StatelessWidget { final effectiveBackgroundColor = backgroundColor ?? Colors.white; final effectiveShadow = boxShadow ?? [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), @@ -116,7 +116,7 @@ class TransparentSectionCard extends StatelessWidget { Widget card = Container( margin: margin, decoration: BoxDecoration( - color: Colors.white.withOpacity(opacity), + color: Colors.white.withValues(alpha: opacity), borderRadius: BorderRadius.circular(borderRadius), border: borderColor != null ? Border.all(color: borderColor!, width: 1) @@ -134,7 +134,7 @@ class TransparentSectionCard extends StatelessWidget { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: Colors.white.withOpacity(0.9), + color: Colors.white.withValues(alpha: 0.9), ), ), const SizedBox(height: 12), @@ -207,7 +207,7 @@ class InfoCard extends StatelessWidget { label, style: TextStyle( fontSize: 14, - color: theme.colorScheme.onSurface.withOpacity(0.6), + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), ), const SizedBox(height: 4), diff --git a/lib/widgets/common/dialogs/confirmation_dialog.dart b/lib/widgets/common/dialogs/confirmation_dialog.dart index 589cd0a..5495b5a 100644 --- a/lib/widgets/common/dialogs/confirmation_dialog.dart +++ b/lib/widgets/common/dialogs/confirmation_dialog.dart @@ -53,7 +53,7 @@ class ConfirmationDialog extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: (iconColor ?? effectiveConfirmColor).withOpacity(0.1), + color: (iconColor ?? effectiveConfirmColor).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( @@ -163,7 +163,7 @@ class SuccessDialog extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.green.withOpacity(0.1), + color: Colors.green.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: const Icon( @@ -271,7 +271,7 @@ class ErrorDialog extends StatelessWidget { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), + color: Colors.red.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: const Icon( diff --git a/lib/widgets/common/dialogs/loading_overlay.dart b/lib/widgets/common/dialogs/loading_overlay.dart index 2440d4d..cafa096 100644 --- a/lib/widgets/common/dialogs/loading_overlay.dart +++ b/lib/widgets/common/dialogs/loading_overlay.dart @@ -27,7 +27,7 @@ class LoadingOverlay extends StatelessWidget { child, if (isLoading) Container( - color: (backgroundColor ?? Colors.black).withOpacity(opacity), + color: (backgroundColor ?? Colors.black).withValues(alpha: opacity), child: Center( child: Container( padding: const EdgeInsets.all(24), @@ -36,7 +36,7 @@ class LoadingOverlay extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 10, offset: const Offset(0, 4), ), @@ -193,7 +193,7 @@ class _CustomLoadingIndicatorState extends State width: widget.size / 5, height: widget.size / 5, decoration: BoxDecoration( - color: effectiveColor.withOpacity(0.3 + value * 0.7), + color: effectiveColor.withValues(alpha: 0.3 + value * 0.7), shape: BoxShape.circle, ), ); @@ -212,7 +212,7 @@ class _CustomLoadingIndicatorState extends State height: widget.size, decoration: BoxDecoration( shape: BoxShape.circle, - color: effectiveColor.withOpacity(0.3), + color: effectiveColor.withValues(alpha: 0.3), ), child: Center( child: Container( @@ -220,7 +220,7 @@ class _CustomLoadingIndicatorState extends State height: widget.size * (0.3 + _animation.value * 0.5), decoration: BoxDecoration( shape: BoxShape.circle, - color: effectiveColor.withOpacity(1 - _animation.value), + color: effectiveColor.withValues(alpha: 1 - _animation.value), ), ), ), diff --git a/lib/widgets/common/form_fields/base_text_field.dart b/lib/widgets/common/form_fields/base_text_field.dart index 061158d..ada517f 100644 --- a/lib/widgets/common/form_fields/base_text_field.dart +++ b/lib/widgets/common/form_fields/base_text_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../../../theme/app_colors.dart'; /// 공통 텍스트 필드 위젯 /// 프로젝트 전체에서 일관된 스타일의 텍스트 입력 필드를 제공합니다. @@ -68,7 +69,7 @@ class BaseTextField extends StatelessWidget { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, + color: AppColors.textSecondary, ), ), const SizedBox(height: 8), @@ -91,18 +92,18 @@ class BaseTextField extends StatelessWidget { cursorColor: cursorColor ?? theme.primaryColor, style: style ?? TextStyle( fontSize: 16, - color: theme.colorScheme.onSurface, + color: AppColors.textPrimary, ), decoration: InputDecoration( hintText: hintText, hintStyle: TextStyle( - color: theme.colorScheme.onSurface.withOpacity(0.6), + color: AppColors.textMuted, ), prefixIcon: prefixIcon, prefixText: prefixText, suffixIcon: suffixIcon, filled: true, - fillColor: fillColor ?? Colors.white, + fillColor: fillColor ?? AppColors.glassBackground, contentPadding: contentPadding ?? const EdgeInsets.all(16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), @@ -117,7 +118,10 @@ class BaseTextField extends StatelessWidget { ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, + borderSide: BorderSide( + color: AppColors.textSecondary, + width: 1, + ), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), diff --git a/lib/widgets/common/form_fields/date_picker_field.dart b/lib/widgets/common/form_fields/date_picker_field.dart index d9e5d83..753bdde 100644 --- a/lib/widgets/common/form_fields/date_picker_field.dart +++ b/lib/widgets/common/form_fields/date_picker_field.dart @@ -68,7 +68,6 @@ class DatePickerField extends StatelessWidget { surface: Colors.white, onSurface: Colors.black, ), - dialogBackgroundColor: Colors.white, ), child: child!, ); @@ -98,7 +97,7 @@ class DatePickerField extends StatelessWidget { fontSize: 16, color: enabled ? theme.colorScheme.onSurface - : theme.colorScheme.onSurface.withOpacity(0.5), + : theme.colorScheme.onSurface.withValues(alpha: 0.5), ), ), ), @@ -106,8 +105,8 @@ class DatePickerField extends StatelessWidget { Icons.calendar_today, size: 20, color: enabled - ? theme.colorScheme.onSurface.withOpacity(0.6) - : theme.colorScheme.onSurface.withOpacity(0.3), + ? theme.colorScheme.onSurface.withValues(alpha: 0.6) + : theme.colorScheme.onSurface.withValues(alpha: 0.3), ), ], ), @@ -214,7 +213,6 @@ class _DateRangeItem extends StatelessWidget { surface: Colors.white, onSurface: Colors.black, ), - dialogBackgroundColor: Colors.white, ), child: child!, ); @@ -239,7 +237,7 @@ class _DateRangeItem extends StatelessWidget { label, style: TextStyle( fontSize: 12, - color: theme.colorScheme.onSurface.withOpacity(0.6), + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), ), const SizedBox(height: 4), @@ -252,7 +250,7 @@ class _DateRangeItem extends StatelessWidget { fontWeight: FontWeight.w500, color: date != null ? theme.colorScheme.onSurface - : theme.colorScheme.onSurface.withOpacity(0.4), + : theme.colorScheme.onSurface.withValues(alpha: 0.4), ), ), ], diff --git a/lib/widgets/common/snackbar/app_snackbar.dart b/lib/widgets/common/snackbar/app_snackbar.dart new file mode 100644 index 0000000..5f11cf4 --- /dev/null +++ b/lib/widgets/common/snackbar/app_snackbar.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import '../../../theme/app_colors.dart'; + +/// 앱 전체에서 사용되는 통합 스낵바 +/// 성공, 에러, 정보 등 다양한 타입의 메시지를 표시합니다. +class AppSnackBar { + /// 성공 메시지를 표시합니다. + static void showSuccess({ + required BuildContext context, + required String message, + IconData icon = Icons.check_circle_rounded, + Duration duration = const Duration(seconds: 3), + bool showAtTop = true, + }) { + _show( + context: context, + message: message, + icon: icon, + backgroundColor: AppColors.successColor, + iconColor: AppColors.pureWhite, + textColor: AppColors.pureWhite, + duration: duration, + showAtTop: showAtTop, + ); + } + + /// 에러 메시지를 표시합니다. + static void showError({ + required BuildContext context, + required String message, + IconData icon = Icons.error_rounded, + Duration duration = const Duration(seconds: 4), + bool showAtTop = true, + }) { + _show( + context: context, + message: message, + icon: icon, + backgroundColor: AppColors.dangerColor, + iconColor: AppColors.pureWhite, + textColor: AppColors.pureWhite, + duration: duration, + showAtTop: showAtTop, + ); + } + + /// 정보 메시지를 표시합니다. + static void showInfo({ + required BuildContext context, + required String message, + IconData icon = Icons.info_rounded, + Duration duration = const Duration(seconds: 3), + bool showAtTop = true, + }) { + _show( + context: context, + message: message, + icon: icon, + backgroundColor: AppColors.primaryColor, + iconColor: AppColors.pureWhite, + textColor: AppColors.pureWhite, + duration: duration, + showAtTop: showAtTop, + ); + } + + /// 경고 메시지를 표시합니다. + static void showWarning({ + required BuildContext context, + required String message, + IconData icon = Icons.warning_amber_rounded, + Duration duration = const Duration(seconds: 3), + bool showAtTop = true, + }) { + _show( + context: context, + message: message, + icon: icon, + backgroundColor: AppColors.warningColor, + iconColor: AppColors.pureWhite, + textColor: AppColors.pureWhite, + duration: duration, + showAtTop: showAtTop, + ); + } + + /// 커스텀 스낵바를 표시합니다. + static void showCustom({ + required BuildContext context, + required String message, + required IconData icon, + required Color backgroundColor, + Color iconColor = AppColors.pureWhite, + Color textColor = AppColors.pureWhite, + Duration duration = const Duration(seconds: 3), + bool showAtTop = true, + SnackBarAction? action, + }) { + _show( + context: context, + message: message, + icon: icon, + backgroundColor: backgroundColor, + iconColor: iconColor, + textColor: textColor, + duration: duration, + showAtTop: showAtTop, + action: action, + ); + } + + /// 내부적으로 스낵바를 표시하는 메서드 + static void _show({ + required BuildContext context, + required String message, + required IconData icon, + required Color backgroundColor, + required Color iconColor, + required Color textColor, + required Duration duration, + required bool showAtTop, + SnackBarAction? action, + }) { + // 기존 스낵바 제거 + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + // 새 스낵바 표시 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + // 아이콘 + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: iconColor, + size: 20, + ), + ), + const SizedBox(width: 12), + // 메시지 + Expanded( + child: Text( + message, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: textColor, + height: 1.3, + ), + ), + ), + ], + ), + backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + margin: showAtTop + ? EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 16, + left: 16, + right: 16, + bottom: MediaQuery.of(context).size.height - 120, + ) + : const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + duration: duration, + dismissDirection: DismissDirection.horizontal, + action: action, + ), + ); + } + + /// 로딩 스낵바를 표시합니다. (자동으로 사라지지 않음) + static ScaffoldFeatureController showLoading({ + required BuildContext context, + required String message, + bool showAtTop = true, + }) { + // 기존 스낵바 제거 + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + return ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + // 로딩 인디케이터 + Container( + width: 24, + height: 24, + margin: const EdgeInsets.only(right: 12), + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: AppColors.pureWhite, + ), + ), + // 메시지 + Expanded( + child: Text( + message, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColors.pureWhite, + ), + ), + ), + ], + ), + backgroundColor: AppColors.primaryColor, + behavior: SnackBarBehavior.floating, + margin: showAtTop + ? EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 16, + left: 16, + right: 16, + bottom: MediaQuery.of(context).size.height - 120, + ) + : const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + duration: const Duration(days: 365), // 자동으로 사라지지 않음 + dismissDirection: DismissDirection.none, // 스와이프로 닫을 수 없음 + ), + ); + } + + /// 액션 버튼이 있는 스낵바를 표시합니다. + static void showWithAction({ + required BuildContext context, + required String message, + required String actionLabel, + required VoidCallback onActionPressed, + IconData icon = Icons.info_rounded, + Color backgroundColor = AppColors.primaryColor, + Duration duration = const Duration(seconds: 4), + bool showAtTop = true, + }) { + _show( + context: context, + message: message, + icon: icon, + backgroundColor: backgroundColor, + iconColor: AppColors.pureWhite, + textColor: AppColors.pureWhite, + duration: duration, + showAtTop: showAtTop, + action: SnackBarAction( + label: actionLabel, + textColor: AppColors.pureWhite, + onPressed: onActionPressed, + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/detail/detail_form_section.dart b/lib/widgets/detail/detail_form_section.dart index 8cab419..bb87db9 100644 --- a/lib/widgets/detail/detail_form_section.dart +++ b/lib/widgets/detail/detail_form_section.dart @@ -240,7 +240,7 @@ class _CurrencyOption extends StatelessWidget { decoration: BoxDecoration( color: isSelected ? Theme.of(context).primaryColor - : Colors.grey.withOpacity(0.1), + : Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Center( @@ -291,7 +291,7 @@ class _BillingCycleSelector extends StatelessWidget { vertical: 12, ), decoration: BoxDecoration( - color: isSelected ? baseColor : Colors.grey.withOpacity(0.1), + color: isSelected ? baseColor : Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( @@ -341,14 +341,14 @@ class _CategorySelector extends StatelessWidget { vertical: 10, ), decoration: BoxDecoration( - color: isSelected ? baseColor : Colors.grey.withOpacity(0.1), + color: isSelected ? baseColor : Colors.grey.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - category.emoji, + category.icon, style: const TextStyle(fontSize: 16), ), const SizedBox(width: 6), diff --git a/lib/widgets/dialogs/delete_confirmation_dialog.dart b/lib/widgets/dialogs/delete_confirmation_dialog.dart new file mode 100644 index 0000000..2cf2d83 --- /dev/null +++ b/lib/widgets/dialogs/delete_confirmation_dialog.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'dart:ui'; +import '../../theme/app_colors.dart'; +import '../common/buttons/primary_button.dart'; +import '../common/buttons/secondary_button.dart'; + +/// 삭제 확인 다이얼로그 +/// 글래스모피즘 스타일의 삭제 확인 다이얼로그입니다. +class DeleteConfirmationDialog extends StatelessWidget { + final String serviceName; + + const DeleteConfirmationDialog({ + super.key, + required this.serviceName, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + child: Stack( + children: [ + // 글래스모피즘 배경 + ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration( + color: AppColors.glassCard.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: AppColors.glassBorder, + width: 1, + ), + ), + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 아이콘 + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.delete_forever_rounded, + color: Colors.red, + size: 40, + ), + ), + const SizedBox(height: 24), + + // 타이틀 + const Text( + '구독 삭제', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 12), + + // 설명 + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle( + fontSize: 16, + color: AppColors.textSecondary, + height: 1.5, + ), + children: [ + const TextSpan(text: '정말로 '), + TextSpan( + text: serviceName, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + ), + const TextSpan(text: ' 구독을\n삭제하시겠습니까?'), + ], + ), + ), + const SizedBox(height: 8), + + // 경고 메시지 + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.red.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.warning_amber_rounded, + color: Colors.red.withValues(alpha: 0.8), + size: 20, + ), + const SizedBox(width: 8), + const Text( + '이 작업은 되돌릴 수 없습니다', + style: TextStyle( + fontSize: 14, + color: Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const SizedBox(height: 32), + + // 버튼들 + Row( + children: [ + Expanded( + child: SecondaryButton( + text: '취소', + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: PrimaryButton( + text: '삭제', + icon: Icons.delete_rounded, + onPressed: () { + Navigator.of(context).pop(true); + }, + backgroundColor: Colors.red, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } + + /// 삭제 확인 다이얼로그를 표시합니다. + static Future show({ + required BuildContext context, + required String serviceName, + }) async { + final result = await showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withValues(alpha: 0.5), + builder: (context) => DeleteConfirmationDialog( + serviceName: serviceName, + ), + ); + + return result ?? false; + } +} \ No newline at end of file diff --git a/lib/widgets/empty_state_widget.dart b/lib/widgets/empty_state_widget.dart index 86bba91..de53f8d 100644 --- a/lib/widgets/empty_state_widget.dart +++ b/lib/widgets/empty_state_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'dart:math' as math; import 'glassmorphism_card.dart'; import 'themed_text.dart'; +import '../theme/app_colors.dart'; /// 구독이 없을 때 표시되는 빈 화면 위젯 /// @@ -49,14 +50,14 @@ class EmptyStateWidget extends StatelessWidget { padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: const LinearGradient( - colors: [Color(0xFF3B82F6), Color(0xFF2563EB)], + colors: AppColors.blueGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: const Color(0xFF3B82F6).withValues(alpha: 0.3), + color: AppColors.primaryColor.withValues(alpha: 0.3), spreadRadius: 0, blurRadius: 16, offset: const Offset(0, 8), @@ -66,7 +67,7 @@ class EmptyStateWidget extends StatelessWidget { child: const Icon( Icons.subscriptions_outlined, size: 48, - color: Colors.white, + color: AppColors.pureWhite, ), ), ); @@ -100,7 +101,7 @@ class EmptyStateWidget extends StatelessWidget { borderRadius: BorderRadius.circular(16), ), elevation: 4, - backgroundColor: const Color(0xFF3B82F6), + backgroundColor: AppColors.primaryColor, ), onPressed: () { HapticFeedback.mediumImpact(); @@ -112,7 +113,7 @@ class EmptyStateWidget extends StatelessWidget { fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.5, - color: Colors.white, + color: AppColors.pureWhite, ), ), ), diff --git a/lib/widgets/expandable_fab.dart b/lib/widgets/expandable_fab.dart index 3bbd7bf..ece5110 100644 --- a/lib/widgets/expandable_fab.dart +++ b/lib/widgets/expandable_fab.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'dart:math' as math; import '../theme/app_colors.dart'; import '../utils/haptic_feedback_helper.dart'; @@ -82,7 +81,7 @@ class _ExpandableFabState extends State animation: _expandAnimation, builder: (context, child) { return Container( - color: Colors.black.withValues(alpha: 0.3 * _expandAnimation.value), + color: AppColors.shadowBlack.withValues(alpha: 3.75 * _expandAnimation.value), ); }, ), @@ -118,7 +117,7 @@ class _ExpandableFabState extends State child: Icon( action.icon, size: 20, - color: Colors.white, + color: AppColors.pureWhite, ), ), ), @@ -176,6 +175,7 @@ class _ExpandableFabState extends State style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, + color: AppColors.darkNavy, ), ), ), diff --git a/lib/widgets/floating_navigation_bar.dart b/lib/widgets/floating_navigation_bar.dart index 9771cde..e14e560 100644 --- a/lib/widgets/floating_navigation_bar.dart +++ b/lib/widgets/floating_navigation_bar.dart @@ -72,42 +72,58 @@ class _FloatingNavigationBarState extends State offset: Offset(0, 100 * (1 - _animation.value)), child: Opacity( opacity: _animation.value, - child: GlassmorphismCard( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - borderRadius: 24, - blur: 10.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _NavigationItem( - icon: Icons.home_rounded, - label: '홈', - isSelected: widget.selectedIndex == 0, - onTap: () => _onItemTapped(0), + child: Stack( + children: [ + // 차단 레이어 - 크기 명시 + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(24), + ), ), - _NavigationItem( - icon: Icons.analytics_rounded, - label: '분석', - isSelected: widget.selectedIndex == 1, - onTap: () => _onItemTapped(1), + ), + // 글래스모피즘 레이어 + GlassmorphismCard( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + borderRadius: 24, + blur: 10.0, + backgroundColor: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _NavigationItem( + icon: Icons.home_rounded, + label: '홈', + isSelected: widget.selectedIndex == 0, + onTap: () => _onItemTapped(0), + ), + _NavigationItem( + icon: Icons.analytics_rounded, + label: '분석', + isSelected: widget.selectedIndex == 1, + onTap: () => _onItemTapped(1), + ), + _AddButton( + onTap: () => _onItemTapped(2), + ), + _NavigationItem( + icon: Icons.qr_code_scanner_rounded, + label: 'SMS', + isSelected: widget.selectedIndex == 3, + onTap: () => _onItemTapped(3), + ), + _NavigationItem( + icon: Icons.settings_rounded, + label: '설정', + isSelected: widget.selectedIndex == 4, + onTap: () => _onItemTapped(4), + ), + ], ), - _AddButton( - onTap: () => _onItemTapped(2), - ), - _NavigationItem( - icon: Icons.qr_code_scanner_rounded, - label: 'SMS', - isSelected: widget.selectedIndex == 3, - onTap: () => _onItemTapped(3), - ), - _NavigationItem( - icon: Icons.settings_rounded, - label: '설정', - isSelected: widget.selectedIndex == 4, - onTap: () => _onItemTapped(4), - ), - ], - ), + ), + ], ), ), ), @@ -137,8 +153,6 @@ class _NavigationItem extends StatelessWidget { @override Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), @@ -147,7 +161,7 @@ class _NavigationItem extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: isSelected - ? const Color(0xFF14B8A6).withValues(alpha: 0.1) + ? AppColors.primaryColor.withValues(alpha: 0.1) : Colors.transparent, borderRadius: BorderRadius.circular(12), ), @@ -158,9 +172,7 @@ class _NavigationItem extends StatelessWidget { duration: const Duration(milliseconds: 200), child: Icon( icon, - color: isSelected - ? const Color(0xFF14B8A6) - : (isDarkMode ? Colors.white70 : AppColors.textSecondary), + color: isSelected ? AppColors.primaryColor : AppColors.navyGray, size: isSelected ? 26 : 24, ), ), @@ -170,9 +182,7 @@ class _NavigationItem extends StatelessWidget { style: TextStyle( fontSize: 11, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected - ? const Color(0xFF14B8A6) - : (isDarkMode ? Colors.white70 : AppColors.textSecondary), + color: isSelected ? AppColors.primaryColor : AppColors.navyGray, ), child: Text(label), ), @@ -243,17 +253,17 @@ class _AddButtonState extends State<_AddButton> colors: AppColors.blueGradient, ), borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: const [ BoxShadow( - color: AppColors.primaryColor.withValues(alpha: 0.3), + color: AppColors.shadowBlack, blurRadius: 12, - offset: const Offset(0, 4), + offset: Offset(0, 4), ), ], ), child: const Icon( Icons.add_rounded, - color: Colors.white, + color: AppColors.pureWhite, size: 28, ), ), diff --git a/lib/widgets/glassmorphic_app_bar.dart b/lib/widgets/glassmorphic_app_bar.dart index 1ca9496..cb980ea 100644 --- a/lib/widgets/glassmorphic_app_bar.dart +++ b/lib/widgets/glassmorphic_app_bar.dart @@ -64,8 +64,8 @@ class GlassmorphicAppBar extends StatelessWidget implements PreferredSizeWidget border: Border( bottom: BorderSide( color: isDarkMode - ? AppColors.glassBorderDark.withValues(alpha: 0.3) - : AppColors.glassBorder.withValues(alpha: 0.3), + ? AppColors.primaryColor.withValues(alpha: 0.3) + : AppColors.glassBorder.withValues(alpha: 0.5), width: 0.5, ), ), @@ -268,8 +268,8 @@ class GlassmorphicSliverAppBar extends StatelessWidget { border: Border( bottom: BorderSide( color: isDarkMode - ? AppColors.glassBorderDark.withValues(alpha: 0.3) - : AppColors.glassBorder.withValues(alpha: 0.3), + ? AppColors.primaryColor.withValues(alpha: 0.3) + : AppColors.glassBorder.withValues(alpha: 0.5), width: 0.5, ), ), diff --git a/lib/widgets/glassmorphic_scaffold.dart b/lib/widgets/glassmorphic_scaffold.dart index 4f88297..1b828d5 100644 --- a/lib/widgets/glassmorphic_scaffold.dart +++ b/lib/widgets/glassmorphic_scaffold.dart @@ -105,17 +105,8 @@ class _GlassmorphicScaffoldState extends State return widget.backgroundGradient!; } - // 시간대별 기본 그라디언트 - final hour = DateTime.now().hour; - if (hour >= 6 && hour < 10) { - return AppColors.morningGradient; - } else if (hour >= 10 && hour < 17) { - return AppColors.dayGradient; - } else if (hour >= 17 && hour < 20) { - return AppColors.eveningGradient; - } else { - return AppColors.nightGradient; - } + // 디폴트 그라디언트 + return AppColors.mainGradient; } @override @@ -166,7 +157,11 @@ class _GlassmorphicScaffoldState extends State gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: gradientColors.map((color) => color.withValues(alpha: 0.1)).toList(), + colors: [ + AppColors.backgroundColor, + ...gradientColors.map((color) => color.withValues(alpha: 0.05)).toList(), + AppColors.backgroundColor, + ], ), ), ), @@ -201,7 +196,7 @@ class _GlassmorphicScaffoldState extends State return CustomPaint( painter: WavePainter( animation: _waveController, - waveColor: AppColors.primaryColor.withValues(alpha: 0.1), + waveColor: AppColors.secondaryColor.withValues(alpha: 0.1), ), ); }, @@ -244,7 +239,7 @@ class ParticlePainter extends CustomPainter { final progress = animation.value; final y = (particle.y + progress * particle.speed) % 1.0; - paint.color = Colors.white.withValues(alpha: particle.opacity); + paint.color = AppColors.pureWhite.withValues(alpha: particle.opacity); canvas.drawCircle( Offset(particle.x * size.width, y * size.height), particle.size, diff --git a/lib/widgets/glassmorphism_card.dart b/lib/widgets/glassmorphism_card.dart index 21658f8..4ff57ca 100644 --- a/lib/widgets/glassmorphism_card.dart +++ b/lib/widgets/glassmorphism_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'dart:ui'; import '../theme/app_colors.dart'; +import 'themed_text.dart'; class GlassmorphismCard extends StatelessWidget { final Widget child; @@ -54,9 +55,7 @@ class GlassmorphismCard extends StatelessWidget { child: Container( padding: padding, decoration: BoxDecoration( - color: backgroundColor ?? (isDarkMode - ? AppColors.glassCardDark - : AppColors.glassCard), + color: backgroundColor ?? AppColors.glassCard, gradient: gradient ?? LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -67,20 +66,22 @@ class GlassmorphismCard extends StatelessWidget { borderRadius: BorderRadius.circular(borderRadius), border: border ?? Border.all( color: isDarkMode - ? AppColors.glassBorderDark + ? AppColors.primaryColor.withValues(alpha: 0.3) : AppColors.glassBorder, - width: 1.5, + width: 1, ), boxShadow: boxShadow ?? [ BoxShadow( - color: Colors.black.withValues(alpha: 0.1), + color: AppColors.shadowBlack, // color.md 가이드: rgba(0,0,0,0.08) blurRadius: 20, spreadRadius: -5, offset: const Offset(0, 10), ), ], ), - child: child, + child: GlassmorphicIndicator( + child: child, + ), ), ), ), diff --git a/lib/widgets/main_summary_card.dart b/lib/widgets/main_summary_card.dart index 3690cba..dc6febe 100644 --- a/lib/widgets/main_summary_card.dart +++ b/lib/widgets/main_summary_card.dart @@ -39,17 +39,15 @@ class MainScreenSummaryCard extends StatelessWidget { child: GlassmorphismCard( borderRadius: 24, blur: 15, - backgroundColor: AppColors.primaryColor.withValues(alpha: 0.2), + backgroundColor: AppColors.glassCard, gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ - AppColors.primaryColor.withValues(alpha: 0.3), - AppColors.primaryColor.withBlue( - (AppColors.primaryColor.blue * 1.3) - .clamp(0, 255) - .toInt()).withValues(alpha: 0.2), - ], + colors: AppColors.mainGradient.map((color) => color.withValues(alpha: 0.2)).toList(), + ), + border: Border.all( + color: AppColors.glassBorder, + width: 1, ), child: Container( width: double.infinity, @@ -81,7 +79,7 @@ class MainScreenSummaryCard extends StatelessWidget { Text( '이번 달 총 구독 비용', style: TextStyle( - color: Colors.white.withValues(alpha: 0.9), + color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트 fontSize: 15, fontWeight: FontWeight.w500, ), @@ -98,7 +96,7 @@ class MainScreenSummaryCard extends StatelessWidget { decimalDigits: 0, ).format(monthlyCost), style: const TextStyle( - color: Colors.white, + color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트 fontSize: 32, fontWeight: FontWeight.bold, letterSpacing: -1, @@ -108,7 +106,7 @@ class MainScreenSummaryCard extends StatelessWidget { Text( '원', style: TextStyle( - color: Colors.white.withValues(alpha: 0.9), + color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트 fontSize: 16, fontWeight: FontWeight.w500, ), @@ -149,7 +147,7 @@ class MainScreenSummaryCard extends StatelessWidget { ), borderRadius: BorderRadius.circular(12), border: Border.all( - color: Colors.white.withValues(alpha: 0.3), + color: AppColors.primaryColor.withValues(alpha: 0.3), width: 1, ), ), @@ -165,7 +163,7 @@ class MainScreenSummaryCard extends StatelessWidget { child: const Icon( Icons.local_offer_rounded, size: 14, - color: Colors.white, + color: AppColors.primaryColor, // color.md 가이드: 밝은 배경 위 딥 블루 아이콘 ), ), const SizedBox(width: 10), @@ -175,7 +173,7 @@ class MainScreenSummaryCard extends StatelessWidget { Text( '이벤트 할인 중', style: TextStyle( - color: Colors.white.withValues(alpha: 0.9), + color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트 fontSize: 11, fontWeight: FontWeight.w500, ), @@ -190,7 +188,7 @@ class MainScreenSummaryCard extends StatelessWidget { decimalDigits: 0, ).format(eventSavings), style: const TextStyle( - color: Colors.white, + color: AppColors.primaryColor, // color.md 가이드: 밝은 배경 위 딥 블루 강조 fontSize: 14, fontWeight: FontWeight.bold, ), @@ -198,7 +196,7 @@ class MainScreenSummaryCard extends StatelessWidget { Text( ' 절약 ($activeEvents개)', style: TextStyle( - color: Colors.white.withValues(alpha: 0.85), + color: AppColors.navyGray, // color.md 가이드: 밝은 배경 위 서브 텍스트 fontSize: 12, fontWeight: FontWeight.w500, ), @@ -229,7 +227,7 @@ class MainScreenSummaryCard extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.15), + color: AppColors.glassBackground, borderRadius: BorderRadius.circular(12), ), child: Column( @@ -238,7 +236,7 @@ class MainScreenSummaryCard extends StatelessWidget { Text( title, style: TextStyle( - color: Colors.white.withValues(alpha: 0.85), + color: AppColors.navyGray, // color.md 가이드: 밝은 배경 위 서브 텍스트 fontSize: 12, fontWeight: FontWeight.w500, ), @@ -247,7 +245,7 @@ class MainScreenSummaryCard extends StatelessWidget { Text( value, style: const TextStyle( - color: Colors.white, + color: AppColors.darkNavy, // color.md 가이드: 밝은 배경 위 어두운 텍스트 fontSize: 14, fontWeight: FontWeight.bold, ), diff --git a/lib/widgets/spring_animation_widget.dart b/lib/widgets/spring_animation_widget.dart index f3a13b6..9e46fb2 100644 --- a/lib/widgets/spring_animation_widget.dart +++ b/lib/widgets/spring_animation_widget.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/physics.dart'; /// 물리 기반 스프링 애니메이션을 적용하는 위젯 class SpringAnimationWidget extends StatefulWidget { @@ -212,7 +211,7 @@ class _GravityAnimationState extends State late AnimationController _controller; double _position = 0; double _velocity = 0; - double _floor = 300; + final double _floor = 300; @override void initState() { diff --git a/lib/widgets/subscription_card.dart b/lib/widgets/subscription_card.dart index 4ab204b..545b5bf 100644 --- a/lib/widgets/subscription_card.dart +++ b/lib/widgets/subscription_card.dart @@ -190,14 +190,10 @@ class _SubscriptionCardState extends State return false; } - Color _getCardColor() { - return Colors.white; - } @override Widget build(BuildContext context) { final isNearBilling = _isNearBilling(); - final Color cardColor = _getCardColor(); return Hero( tag: 'subscription_${widget.subscription.id}', @@ -225,27 +221,7 @@ class _SubscriptionCardState extends State padding: EdgeInsets.zero, borderRadius: 16, blur: _isHovering ? 15 : 10, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: cardColor, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: _isHovering - ? AppColors.primaryColor.withValues(alpha: 0.3) - : AppColors.borderColor, - width: _isHovering ? 1.5 : 0.5, - ), - boxShadow: [ - BoxShadow( - color: AppColors.primaryColor.withValues(alpha: - 0.03 + (0.05 * _hoverController.value)), - blurRadius: 8 + (8 * _hoverController.value), - spreadRadius: 0, - offset: Offset(0, 4 + (2 * _hoverController.value)), - ), - ], - ), + width: double.infinity, // 전체 너비를 차지하도록 설정 child: Column( children: [ // 그라데이션 상단 바 효과 @@ -300,7 +276,7 @@ class _SubscriptionCardState extends State style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 18, - color: Color(0xFF1E293B), + color: AppColors.darkNavy, // color.md 가이드: 메인 텍스트 ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -334,7 +310,7 @@ class _SubscriptionCardState extends State Icon( Icons.local_offer_rounded, size: 11, - color: Colors.white, + color: AppColors.pureWhite, ), SizedBox(width: 3), Text( @@ -342,7 +318,7 @@ class _SubscriptionCardState extends State style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, - color: Colors.white, + color: AppColors.pureWhite, ), ), ], @@ -371,7 +347,7 @@ class _SubscriptionCardState extends State style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, - color: AppColors.textSecondary, + color: AppColors.navyGray, // color.md 가이드: 서브 텍스트 ), ), ), @@ -409,7 +385,7 @@ class _SubscriptionCardState extends State style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.textSecondary, + color: AppColors.navyGray, // color.md 가이드: 서브 텍스트 decoration: TextDecoration.lineThrough, ), ), @@ -539,7 +515,7 @@ class _SubscriptionCardState extends State '${widget.subscription.eventEndDate!.difference(DateTime.now()).inDays}일 남음', style: const TextStyle( fontSize: 11, - color: AppColors.textSecondary, + color: AppColors.navyGray, // color.md 가이드: 서브 텍스트 ), ), ], @@ -555,7 +531,6 @@ class _SubscriptionCardState extends State ], ), ), - ), ), ), ); diff --git a/lib/widgets/subscription_list_widget.dart b/lib/widgets/subscription_list_widget.dart index 46fe98f..0c81291 100644 --- a/lib/widgets/subscription_list_widget.dart +++ b/lib/widgets/subscription_list_widget.dart @@ -6,6 +6,8 @@ import '../widgets/staggered_list_animation.dart'; import '../widgets/app_navigator.dart'; import 'package:provider/provider.dart'; import '../providers/subscription_provider.dart'; +import './dialogs/delete_confirmation_dialog.dart'; +import './common/snackbar/app_snackbar.dart'; /// 카테고리별로 구독 목록을 표시하는 위젯 class SubscriptionListWidget extends StatelessWidget { @@ -92,14 +94,30 @@ class SubscriptionListWidget extends StatelessWidget { AppNavigator.toDetail(context, subscriptions[subIndex]); }, onDelete: () async { - // 삭제 확인 다이얼로그 - final provider = Provider.of( - context, - listen: false, - ); - await provider.deleteSubscription( - subscriptions[subIndex].id, + // 삭제 확인 다이얼로그 표시 + final shouldDelete = await DeleteConfirmationDialog.show( + context: context, + serviceName: subscriptions[subIndex].serviceName, ); + + if (shouldDelete && context.mounted) { + // 사용자가 확인한 경우에만 삭제 진행 + final provider = Provider.of( + context, + listen: false, + ); + await provider.deleteSubscription( + subscriptions[subIndex].id, + ); + + if (context.mounted) { + AppSnackBar.showSuccess( + context: context, + message: '${subscriptions[subIndex].serviceName} 구독이 삭제되었습니다.', + icon: Icons.delete_forever_rounded, + ); + } + } }, ), ), diff --git a/lib/widgets/swipeable_subscription_card.dart b/lib/widgets/swipeable_subscription_card.dart index dd32d25..9bbf7c1 100644 --- a/lib/widgets/swipeable_subscription_card.dart +++ b/lib/widgets/swipeable_subscription_card.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import '../models/subscription_model.dart'; import '../utils/haptic_feedback_helper.dart'; import 'subscription_card.dart'; -import '../theme/app_colors.dart'; class SwipeableSubscriptionCard extends StatefulWidget { final SubscriptionModel subscription; final VoidCallback? onEdit; - final VoidCallback? onDelete; + final Future Function()? onDelete; final VoidCallback? onTap; const SwipeableSubscriptionCard({ @@ -27,12 +26,15 @@ class _SwipeableSubscriptionCardState extends State late AnimationController _controller; late Animation _animation; double _dragStartX = 0; - double _dragExtent = 0; + double _currentOffset = 0; // 현재 카드의 실제 위치 + bool _isDragging = false; // 드래그 중인지 여부 bool _isSwipingLeft = false; bool _hapticTriggered = false; + double _screenWidth = 0; + double _cardWidth = 0; // 카드의 실제 너비 (margin 제외) - static const double _swipeThreshold = 80.0; - static const double _deleteThreshold = 150.0; + static const double _actionThresholdPercent = 0.15; // 15%에서 액션 버튼 표시 + static const double _deleteThresholdPercent = 0.40; // 40%에서 삭제/편집 실행 @override void initState() { @@ -48,81 +50,137 @@ class _SwipeableSubscriptionCardState extends State parent: _controller, curve: Curves.easeOutExpo, )); + + // 애니메이션 상태 리스너 추가 + _controller.addStatusListener(_onAnimationStatusChanged); + + // 애니메이션 리스너 추가 + _controller.addListener(_onAnimationUpdate); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _screenWidth = MediaQuery.of(context).size.width; + _cardWidth = _screenWidth - 32; // 좌우 margin 16px씩 제외 + } + + @override + void didUpdateWidget(SwipeableSubscriptionCard oldWidget) { + super.didUpdateWidget(oldWidget); + // 위젯이 업데이트될 때 카드를 원위치로 복귀 + if (oldWidget.subscription.id != widget.subscription.id) { + _controller.stop(); + setState(() { + _currentOffset = 0; + _isDragging = false; + }); + } } @override void dispose() { + _controller.removeListener(_onAnimationUpdate); + _controller.removeStatusListener(_onAnimationStatusChanged); + _controller.stop(); _controller.dispose(); super.dispose(); } + + void _onAnimationUpdate() { + if (!_isDragging) { + setState(() { + _currentOffset = _animation.value; + }); + } + } + + void _onAnimationStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.completed && !_isDragging) { + setState(() { + _currentOffset = _animation.value; + }); + } + } void _handleDragStart(DragStartDetails details) { _dragStartX = details.localPosition.dx; _hapticTriggered = false; + _isDragging = true; + _controller.stop(); // 진행 중인 애니메이션 중지 } void _handleDragUpdate(DragUpdateDetails details) { final delta = details.localPosition.dx - _dragStartX; setState(() { - _dragExtent = delta; + _currentOffset = delta; _isSwipingLeft = delta < 0; }); - // 햅틱 피드백 트리거 - if (!_hapticTriggered && _dragExtent.abs() > _swipeThreshold) { + // 햅틱 피드백 트리거 (카드 너비의 15%) + final actionThreshold = _cardWidth * _actionThresholdPercent; + if (!_hapticTriggered && _currentOffset.abs() > actionThreshold) { _hapticTriggered = true; HapticFeedbackHelper.mediumImpact(); } - // 삭제 임계값에 도달했을 때 강한 햅틱 - if (_dragExtent.abs() > _deleteThreshold && _hapticTriggered) { + // 삭제 임계값에 도달했을 때 강한 햅틱 (카드 너비의 40%) + final deleteThreshold = _cardWidth * _deleteThresholdPercent; + if (_currentOffset.abs() > deleteThreshold && _hapticTriggered) { HapticFeedbackHelper.heavyImpact(); _hapticTriggered = false; // 반복 방지 } } - void _handleDragEnd(DragEndDetails details) { + void _handleDragEnd(DragEndDetails details) async { + _isDragging = false; final velocity = details.velocity.pixelsPerSecond.dx; - final extent = _dragExtent.abs(); + final extent = _currentOffset.abs(); - if (extent > _deleteThreshold || velocity.abs() > 800) { - // 삭제 액션 + // 카드 너비의 40% 계산 + final deleteThreshold = _cardWidth * _deleteThresholdPercent; + + if (extent > deleteThreshold || velocity.abs() > 800) { + // 40% 이상 스와이프 시 삭제/편집 액션 if (_isSwipingLeft && widget.onDelete != null) { HapticFeedbackHelper.success(); - _animateToOffset(-MediaQuery.of(context).size.width); - Future.delayed(const Duration(milliseconds: 300), () { - widget.onDelete!(); - }); + // 삭제 확인 다이얼로그 표시 + await widget.onDelete!(); + // 다이얼로그가 닫힌 후 원위치로 복귀 + if (mounted) { + _animateToOffset(0); + } } else if (!_isSwipingLeft && widget.onEdit != null) { HapticFeedbackHelper.success(); - _animateToOffset(MediaQuery.of(context).size.width); + // 편집 화면으로 이동 전 원위치로 복귀 + _animateToOffset(0); Future.delayed(const Duration(milliseconds: 300), () { widget.onEdit!(); }); + } else { + // 액션이 없는 경우 원위치로 복귀 + _animateToOffset(0); } - } else if (extent > _swipeThreshold) { - // 액션 버튼 표시 - HapticFeedbackHelper.lightImpact(); - _animateToOffset(_isSwipingLeft ? -_swipeThreshold : _swipeThreshold); } else { - // 원위치로 복귀 + // 40% 미만: 모두 원위치로 복귀 _animateToOffset(0); } } void _animateToOffset(double offset) { + // 애니메이션 컨트롤러 리셋 + _controller.stop(); + _controller.value = 0; + _animation = Tween( - begin: _dragExtent, + begin: _currentOffset, end: offset, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeOutExpo, )); - _controller.forward(from: 0).then((_) { - setState(() { - _dragExtent = offset; - }); - }); + + _controller.forward(); } @override @@ -135,9 +193,7 @@ class _SwipeableSubscriptionCardState extends State margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: _isSwipingLeft - ? AppColors.dangerColor - : AppColors.primaryColor, + color: Colors.transparent, // 투명하게 변경 ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -148,10 +204,10 @@ class _SwipeableSubscriptionCardState extends State padding: const EdgeInsets.only(left: 24), child: AnimatedOpacity( duration: const Duration(milliseconds: 200), - opacity: _dragExtent > 40 ? 1.0 : 0.0, + opacity: _currentOffset > (_cardWidth * 0.10) ? 1.0 : 0.0, child: AnimatedScale( duration: const Duration(milliseconds: 200), - scale: _dragExtent > 40 ? 1.0 : 0.5, + scale: _currentOffset > (_cardWidth * 0.10) ? 1.0 : 0.5, child: const Icon( Icons.edit_rounded, color: Colors.white, @@ -166,12 +222,12 @@ class _SwipeableSubscriptionCardState extends State padding: const EdgeInsets.only(right: 24), child: AnimatedOpacity( duration: const Duration(milliseconds: 200), - opacity: _dragExtent.abs() > 40 ? 1.0 : 0.0, + opacity: _currentOffset.abs() > (_cardWidth * 0.10) ? 1.0 : 0.0, child: AnimatedScale( duration: const Duration(milliseconds: 200), - scale: _dragExtent.abs() > 40 ? 1.0 : 0.5, + scale: _currentOffset.abs() > (_cardWidth * 0.10) ? 1.0 : 0.5, child: Icon( - _dragExtent.abs() > _deleteThreshold + _currentOffset.abs() > (_cardWidth * _deleteThresholdPercent) ? Icons.delete_forever_rounded : Icons.delete_rounded, color: Colors.white, @@ -186,33 +242,24 @@ class _SwipeableSubscriptionCardState extends State ), // 스와이프 가능한 카드 - AnimatedBuilder( - animation: _animation, - builder: (context, child) { - return Transform.translate( - offset: Offset(_animation.value, 0), - child: child, - ); - }, - child: GestureDetector( - onHorizontalDragStart: _handleDragStart, - onHorizontalDragUpdate: _handleDragUpdate, - onHorizontalDragEnd: _handleDragEnd, - child: Transform.translate( - offset: Offset(_dragExtent, 0), - child: Transform.scale( - scale: 1.0 - (_dragExtent.abs() / 2000), - child: Transform.rotate( - angle: _dragExtent / 2000, - child: GestureDetector( - onTap: () { - if (_dragExtent.abs() < 10) { - widget.onTap?.call(); - } - }, - child: SubscriptionCard( - subscription: widget.subscription, - ), + GestureDetector( + onHorizontalDragStart: _handleDragStart, + onHorizontalDragUpdate: _handleDragUpdate, + onHorizontalDragEnd: _handleDragEnd, + child: Transform.translate( + offset: Offset(_currentOffset, 0), + child: Transform.scale( + scale: 1.0 - (_currentOffset.abs() / 2000), + child: Transform.rotate( + angle: _currentOffset / 2000, + child: GestureDetector( + onTap: () { + if (_currentOffset.abs() < 10) { + widget.onTap?.call(); + } + }, + child: SubscriptionCard( + subscription: widget.subscription, ), ), ), diff --git a/lib/widgets/themed_text.dart b/lib/widgets/themed_text.dart index 33822f4..e46abc7 100644 --- a/lib/widgets/themed_text.dart +++ b/lib/widgets/themed_text.dart @@ -39,22 +39,20 @@ class ThemedText extends StatelessWidget { bool forceLight = false, bool forceDark = false, }) { - if (forceLight) return Colors.white; - if (forceDark) return AppColors.textPrimary; + if (forceLight) return AppColors.pureWhite; + if (forceDark) return AppColors.darkNavy; final brightness = Theme.of(context).brightness; - // 글래스모피즘 환경에서는 보통 어두운 배경 위에 밝은 텍스트 + // 글래스모피즘 환경에서는 배경이 밝으므로 어두운 텍스트 사용 if (_isGlassmorphicContext(context)) { - return brightness == Brightness.dark - ? Colors.white.withValues(alpha: 0.95) - : AppColors.textPrimary; + return AppColors.darkNavy; // color.md 가이드: 밝은 배경 위 어두운 텍스트 } // 일반 환경 return brightness == Brightness.dark - ? Colors.white - : AppColors.textPrimary; + ? AppColors.pureWhite + : AppColors.darkNavy; } /// 글래스모피즘 컨텍스트인지 확인 diff --git a/lib/widgets/website_icon.dart b/lib/widgets/website_icon.dart index 0dc2bcf..788920f 100644 --- a/lib/widgets/website_icon.dart +++ b/lib/widgets/website_icon.dart @@ -660,7 +660,7 @@ class _WebsiteIconState extends State } return ClipRRect( - key: ValueKey('local_logo_${_localLogoPath}'), + key: ValueKey('local_logo_$_localLogoPath'), borderRadius: BorderRadius.circular(widget.size * 0.2), child: Image.file( File(_localLogoPath!),