Files
submanager/lib/screens/settings_screen.dart
JiWoong Sul 0f0b02bf08 feat: 다국어 지원 및 다중 통화 환율 변환 기능 확대
- ExchangeRateService에 JPY, CNY 환율 지원 추가
- 구독 서비스별 다국어 표시 이름 지원
- 분석 화면 차트 및 UI/UX 개선
- 설정 화면 전면 리팩토링
- SMS 스캔 기능 사용성 개선
- 전체 앱 다국어 번역 확대

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 17:34:32 +09:00

571 lines
28 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 '../theme/adaptive_theme.dart';
import '../widgets/glassmorphism_card.dart';
import '../theme/app_colors.dart';
import '../widgets/native_ad_widget.dart';
import '../widgets/common/snackbar/app_snackbar.dart';
import '../l10n/app_localizations.dart';
import '../providers/locale_provider.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: [
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
// toolbar 높이 추가
SizedBox(
height: kToolbarHeight + MediaQuery.of(context).padding.top,
),
// 광고 위젯 추가
const NativeAdWidget(key: ValueKey('settings_ad')),
const SizedBox(height: 16),
// 언어 설정
GlassmorphismCard(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(8),
child: Consumer<LocaleProvider>(
builder: (context, localeProvider, child) {
final loc = AppLocalizations.of(context);
return ListTile(
title: Text(
loc.language,
style: const TextStyle(color: AppColors.textPrimary),
),
leading: const Icon(
Icons.language,
color: AppColors.textSecondary,
),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color:
AppColors.textSecondary.withValues(alpha: 0.5),
),
),
child: DropdownButton<String>(
value: localeProvider.locale.languageCode,
underline: const SizedBox(),
borderRadius: BorderRadius.circular(12),
dropdownColor: const Color(0xFF2A2A2A), // 어두운 배경색 설정
icon: const Icon(
Icons.arrow_drop_down,
color: AppColors.textPrimary,
),
iconEnabledColor: AppColors.textPrimary,
selectedItemBuilder: (BuildContext context) {
return [
Text(loc.korean,
style: const TextStyle(
color: AppColors.textPrimary)),
Text(loc.english,
style: const TextStyle(
color: AppColors.textPrimary)),
Text(loc.japanese,
style: const TextStyle(
color: AppColors.textPrimary)),
Text(loc.chinese,
style: const TextStyle(
color: AppColors.textPrimary)),
];
},
items: [
DropdownMenuItem(
value: 'ko',
child: Text(
loc.korean,
style: const TextStyle(color: Colors.white),
),
),
DropdownMenuItem(
value: 'en',
child: Text(
loc.english,
style: const TextStyle(color: Colors.white),
),
),
DropdownMenuItem(
value: 'ja',
child: Text(
loc.japanese,
style: const TextStyle(color: Colors.white),
),
),
DropdownMenuItem(
value: 'zh',
child: Text(
loc.chinese,
style: const TextStyle(color: Colors.white),
),
),
],
onChanged: (String? value) {
if (value != null) {
localeProvider.setLocale(value);
}
},
),
),
);
},
),
),
// 앱 잠금 설정 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: Text(
AppLocalizations.of(context).notificationPermission,
style:
const TextStyle(color: AppColors.textPrimary),
),
subtitle: Text(
AppLocalizations.of(context)
.notificationPermissionDesc,
style:
const TextStyle(color: AppColors.textSecondary),
),
trailing: ElevatedButton(
onPressed: () async {
final granted =
await NotificationService.requestPermission();
if (granted) {
await provider.setEnabled(true);
} else {
AppSnackBar.showError(
context: context,
message: AppLocalizations.of(context)
.notificationPermissionDenied,
);
}
},
child: Text(
AppLocalizations.of(context).requestPermission),
),
),
const Divider(),
// 결제 예정 알림 기본 스위치
SwitchListTile(
title: Text(
AppLocalizations.of(context).paymentNotification,
style:
const TextStyle(color: AppColors.textPrimary),
),
subtitle: Text(
AppLocalizations.of(context)
.paymentNotificationDesc,
style:
const TextStyle(color: AppColors.textSecondary),
),
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일전)
Text(
AppLocalizations.of(context)
.notificationTiming,
style: const 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,
AppLocalizations.of(context)
.oneDayBefore),
_buildReminderDayRadio(
context,
provider,
2,
AppLocalizations.of(context)
.twoDaysBefore),
_buildReminderDayRadio(
context,
provider,
3,
AppLocalizations.of(context)
.threeDaysBefore),
],
),
),
const SizedBox(height: 16),
// 알림 시간 선택
Text(
AppLocalizations.of(context)
.notificationTime,
style: const 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: Text(
AppLocalizations.of(
context)
.dailyReminder),
subtitle: Text(
provider.isDailyReminderEnabled
? AppLocalizations.of(
context)
.dailyReminderEnabled
: AppLocalizations.of(
context)
.dailyReminderDisabledWithDays(
provider
.reminderDays),
style: const TextStyle(
color: AppColors
.textLight),
),
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: Text(
AppLocalizations.of(context).appInfo,
style: const TextStyle(color: AppColors.textPrimary),
),
subtitle: Text(
'${AppLocalizations.of(context).version} 1.0.0',
style: const TextStyle(color: AppColors.textSecondary),
),
leading: const Icon(
Icons.info,
color: AppColors.textSecondary,
),
onTap: () async {
// 웹 환경에서는 기본 다이얼로그 표시
if (kIsWeb) {
showAboutDialog(
context: context,
applicationName: AppLocalizations.of(context).appTitle,
applicationVersion: '1.0.0',
applicationIcon: const FlutterLogo(size: 50),
children: [
Text(AppLocalizations.of(context).appDescription),
const SizedBox(height: 8),
Text(
'${AppLocalizations.of(context).developer}: Julian Sul'),
],
);
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) {
AppSnackBar.showError(
context: context,
message:
AppLocalizations.of(context).cannotOpenStore,
);
}
}
} else {
// 스토어 링크를 열 수 없는 경우 기존 정보 다이얼로그 표시
showAboutDialog(
context: context,
applicationName: AppLocalizations.of(context).appTitle,
applicationVersion: '1.0.0',
applicationIcon: const FlutterLogo(size: 50),
children: [
Text(AppLocalizations.of(context).appDescription),
const SizedBox(height: 8),
Text(
'${AppLocalizations.of(context).developer}: Julian Sul'),
],
);
}
},
),
),
// FloatingNavigationBar를 위한 충분한 하단 여백
SizedBox(
height: 120 + MediaQuery.of(context).padding.bottom,
),
],
),
),
],
);
}
}