- billing_cycle_selector, category_selector, currency_selector 컴포넌트 분리 - 구독 카드 클릭 이슈 해결을 위한 리팩토링 - SMS 스캔 화면 UI/UX 개선 및 기능 강화 - 상세 화면 컨트롤러 로직 개선 - 알림 서비스 및 구독 URL 매칭 기능 추가 - CLAUDE.md 프로젝트 가이드라인 대폭 확장 - 전반적인 코드 구조 개선 및 타입 안정성 강화 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
445 lines
20 KiB
Dart
445 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'package:provider/provider.dart';
|
|
import '../providers/notification_provider.dart';
|
|
import 'dart:io';
|
|
import '../services/notification_service.dart';
|
|
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';
|
|
import '../widgets/native_ad_widget.dart';
|
|
|
|
class SettingsScreen extends StatelessWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
// 알림 시점 라디오 버튼 생성 헬퍼 메서드
|
|
Widget _buildReminderDayRadio(BuildContext context,
|
|
NotificationProvider provider, int value, String label) {
|
|
final isSelected = provider.reminderDays == value;
|
|
return Expanded(
|
|
child: InkWell(
|
|
onTap: () => provider.setReminderDays(value),
|
|
child: Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? AppColors.primaryColor.withValues(alpha: 0.2)
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? AppColors.primaryColor
|
|
: AppColors.textSecondary.withValues(alpha: 0.5),
|
|
width: isSelected ? 2 : 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
isSelected
|
|
? Icons.radio_button_checked
|
|
: Icons.radio_button_unchecked,
|
|
color: isSelected
|
|
? AppColors.primaryColor
|
|
: AppColors.textSecondary,
|
|
size: 24,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
color: isSelected
|
|
? AppColors.primaryColor
|
|
: AppColors.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
SizedBox(
|
|
height: kToolbarHeight + MediaQuery.of(context).padding.top,
|
|
),
|
|
Expanded(
|
|
child: ListView(
|
|
padding: const EdgeInsets.only(top: 20),
|
|
children: [
|
|
// 광고 위젯 추가
|
|
const NativeAdWidget(key: ValueKey('settings_ad')),
|
|
const SizedBox(height: 16),
|
|
// 앱 잠금 설정 UI 숨김
|
|
// Card(
|
|
// margin: const EdgeInsets.all(16),
|
|
// child: Consumer<AppLockProvider>(
|
|
// builder: (context, provider, child) {
|
|
// return SwitchListTile(
|
|
// title: const Text('앱 잠금'),
|
|
// subtitle: const Text('생체 인증으로 앱 잠금'),
|
|
// value: provider.isEnabled,
|
|
// onChanged: (value) async {
|
|
// if (value) {
|
|
// final isAuthenticated = await provider.authenticate();
|
|
// if (isAuthenticated) {
|
|
// provider.enable();
|
|
// }
|
|
// } else {
|
|
// provider.disable();
|
|
// }
|
|
// },
|
|
// );
|
|
// },
|
|
// ),
|
|
// ),
|
|
|
|
// 알림 설정
|
|
GlassmorphismCard(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
padding: const EdgeInsets.all(8),
|
|
child: Consumer<NotificationProvider>(
|
|
builder: (context, provider, child) {
|
|
return Column(
|
|
children: [
|
|
ListTile(
|
|
title: const Text('알림 권한'),
|
|
subtitle: const Text('알림을 받으려면 권한이 필요합니다'),
|
|
trailing: ElevatedButton(
|
|
onPressed: () async {
|
|
final granted =
|
|
await NotificationService.requestPermission();
|
|
if (granted) {
|
|
provider.setEnabled(true);
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'알림 권한이 거부되었습니다',
|
|
style: TextStyle(
|
|
color: AppColors.pureWhite,
|
|
),
|
|
),
|
|
backgroundColor: AppColors.dangerColor,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
child: const Text('권한 요청'),
|
|
),
|
|
),
|
|
const Divider(),
|
|
// 결제 예정 알림 기본 스위치
|
|
SwitchListTile(
|
|
title: const Text('결제 예정 알림'),
|
|
subtitle: const Text('결제 예정일 알림 받기'),
|
|
value: provider.isPaymentEnabled,
|
|
onChanged: (value) {
|
|
provider.setPaymentEnabled(value);
|
|
},
|
|
),
|
|
|
|
// 알림 세부 설정 (알림 활성화된 경우에만 표시)
|
|
AnimatedSize(
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeInOut,
|
|
child: provider.isPaymentEnabled
|
|
? Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 16.0, right: 16.0, bottom: 8.0),
|
|
child: Card(
|
|
elevation: 0,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.surfaceVariant
|
|
.withValues(alpha: 0.3),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
// 알림 시점 선택 (1일전, 2일전, 3일전)
|
|
const Text('알림 시점',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8.0),
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
_buildReminderDayRadio(
|
|
context, provider, 1, '1일 전'),
|
|
_buildReminderDayRadio(
|
|
context, provider, 2, '2일 전'),
|
|
_buildReminderDayRadio(
|
|
context, provider, 3, '3일 전'),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// 알림 시간 선택
|
|
const Text('알림 시간',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 12),
|
|
InkWell(
|
|
onTap: () async {
|
|
final TimeOfDay? picked =
|
|
await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay(
|
|
hour: provider.reminderHour,
|
|
minute:
|
|
provider.reminderMinute),
|
|
);
|
|
if (picked != null) {
|
|
provider.setReminderTime(
|
|
picked.hour, picked.minute);
|
|
}
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 12, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.outline
|
|
.withValues(alpha: 0.5),
|
|
),
|
|
borderRadius:
|
|
BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.access_time,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.primary,
|
|
size: 22,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
'${provider.reminderHour.toString().padLeft(2, '0')}:${provider.reminderMinute.toString().padLeft(2, '0')}',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight:
|
|
FontWeight.bold,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSurface,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
Icons.arrow_forward_ios,
|
|
size: 16,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.outline,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// 반복 알림 스위치 (2일전, 3일전 선택 시에만 활성화)
|
|
if (provider.reminderDays >= 2)
|
|
Padding(
|
|
padding:
|
|
const EdgeInsets.only(top: 16.0),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 4, horizontal: 4),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.surfaceVariant
|
|
.withValues(alpha: 0.3),
|
|
borderRadius:
|
|
BorderRadius.circular(8),
|
|
),
|
|
child: SwitchListTile(
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(
|
|
horizontal: 12),
|
|
title: const Text('1일마다 반복 알림'),
|
|
subtitle: const Text(
|
|
'결제일까지 매일 알림을 받습니다'),
|
|
value: provider
|
|
.isDailyReminderEnabled,
|
|
activeColor: Theme.of(context)
|
|
.colorScheme
|
|
.primary,
|
|
onChanged: (value) {
|
|
provider
|
|
.setDailyReminderEnabled(
|
|
value);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: const SizedBox.shrink(),
|
|
),
|
|
// 미사용 서비스 알림 기능 비활성화
|
|
// const Divider(),
|
|
// SwitchListTile(
|
|
// title: const Text('미사용 서비스 알림'),
|
|
// subtitle: const Text('2개월 이상 미사용 시 알림'),
|
|
// value: provider.isUnusedServiceNotificationEnabled,
|
|
// onChanged: (value) {
|
|
// provider.setUnusedServiceNotificationEnabled(value);
|
|
// },
|
|
// ),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
// 앱 정보
|
|
GlassmorphismCard(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
padding: const EdgeInsets.all(8),
|
|
child: ListTile(
|
|
title: const Text('앱 정보'),
|
|
subtitle: const Text('버전 1.0.0'),
|
|
leading: const Icon(Icons.info),
|
|
onTap: () async {
|
|
// 웹 환경에서는 기본 다이얼로그 표시
|
|
if (kIsWeb) {
|
|
showAboutDialog(
|
|
context: context,
|
|
applicationName: 'SubManager',
|
|
applicationVersion: '1.0.0',
|
|
applicationIcon: const FlutterLogo(size: 50),
|
|
children: [
|
|
const Text('구독 관리 앱'),
|
|
const SizedBox(height: 8),
|
|
const Text('개발자: SubManager Team'),
|
|
],
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 앱 스토어 링크
|
|
String storeUrl = '';
|
|
|
|
// 플랫폼에 따라 스토어 링크 설정
|
|
if (Platform.isAndroid) {
|
|
// Android - Google Play 스토어 링크
|
|
storeUrl =
|
|
'https://play.google.com/store/apps/details?id=com.submanager.app';
|
|
} else if (Platform.isIOS) {
|
|
// iOS - App Store 링크
|
|
storeUrl =
|
|
'https://apps.apple.com/app/submanager/id123456789';
|
|
}
|
|
|
|
if (storeUrl.isNotEmpty) {
|
|
try {
|
|
final Uri url = Uri.parse(storeUrl);
|
|
await launchUrl(url, mode: LaunchMode.externalApplication);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'스토어를 열 수 없습니다',
|
|
style: TextStyle(
|
|
color: AppColors.pureWhite,
|
|
),
|
|
),
|
|
backgroundColor: AppColors.dangerColor,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// 스토어 링크를 열 수 없는 경우 기존 정보 다이얼로그 표시
|
|
showAboutDialog(
|
|
context: context,
|
|
applicationName: 'SubManager',
|
|
applicationVersion: '1.0.0',
|
|
applicationIcon: const FlutterLogo(size: 50),
|
|
children: [
|
|
const Text('구독 관리 앱'),
|
|
const SizedBox(height: 8),
|
|
const Text('개발자: SubManager Team'),
|
|
],
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
// FloatingNavigationBar를 위한 충분한 하단 여백
|
|
SizedBox(
|
|
height: 120 + MediaQuery.of(context).padding.bottom,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
String _getThemeModeText(AppThemeMode mode) {
|
|
switch (mode) {
|
|
case AppThemeMode.light:
|
|
return '라이트';
|
|
case AppThemeMode.dark:
|
|
return '다크';
|
|
case AppThemeMode.oled:
|
|
return 'OLED 블랙';
|
|
case AppThemeMode.system:
|
|
return '시스템 설정';
|
|
}
|
|
}
|
|
|
|
IconData _getThemeModeIcon(AppThemeMode mode) {
|
|
switch (mode) {
|
|
case AppThemeMode.light:
|
|
return Icons.light_mode;
|
|
case AppThemeMode.dark:
|
|
return Icons.dark_mode;
|
|
case AppThemeMode.oled:
|
|
return Icons.phonelink_lock;
|
|
case AppThemeMode.system:
|
|
return Icons.settings_brightness;
|
|
}
|
|
}
|
|
}
|