914 lines
34 KiB
Dart
914 lines
34 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
final Color _mainBlueColor = const Color(0xFF002FA7);
|
|
final Color _mainTextColor = const Color(0xFF1C1C1E);
|
|
final Color _subTextColor = const Color(0xFF6A717B);
|
|
final Color _pageBackgroundColor = const Color(0xFFF5F7F9);
|
|
final Color _cardBackgroundColor = Colors.white;
|
|
final Color _accentContainerColor = const Color(0xFFF0F2F5);
|
|
final Color _warningColor = const Color(0xFFFF3B30);
|
|
|
|
const BoxShadow _cleanShadow = BoxShadow(
|
|
color: Color.fromRGBO(0, 0, 0, 0.07),
|
|
blurRadius: 8,
|
|
offset: Offset(0, 4),
|
|
spreadRadius: 0,
|
|
);
|
|
|
|
class SettingsScreen extends StatefulWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
}
|
|
|
|
class _SettingsScreenState extends State<SettingsScreen> {
|
|
bool _isPushEnabled = true;
|
|
bool _isRentalAlertEnabled = true;
|
|
bool _isStorageStatusAlert = true;
|
|
bool _isEnvSensorAlert = true;
|
|
|
|
bool _isBiometricEnabled = false;
|
|
bool _isAutoLogoutEnabled = true;
|
|
bool _isLoginNotificationEnabled = true;
|
|
bool _isLocationEnabled = true;
|
|
|
|
final String _currentAppVersion = "v1.0.2";
|
|
|
|
void _showDeleteAccountDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return Dialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20.0),
|
|
),
|
|
elevation: 0,
|
|
backgroundColor: Colors.transparent,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(24.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20.0),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.warning_amber_rounded,
|
|
color: _warningColor,
|
|
size: 64.0,
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
'회원 탈퇴',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 22.0,
|
|
fontWeight: FontWeight.bold,
|
|
color: _mainTextColor,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'탈퇴 시 모든 이용 기록이 영구 삭제되며,\n 식제된 데이터는 복구할 수 없습니다.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 14.0,
|
|
color: _subTextColor,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
side: BorderSide(color: Colors.grey.shade300),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
child: Text(
|
|
'유지하기',
|
|
style: TextStyle(
|
|
color: Colors.grey.shade700,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: FilledButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("탈퇴 요청이 처리되었습니다.")),
|
|
);
|
|
},
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: _warningColor,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
child: const Text(
|
|
'탈퇴하기',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showCommonModal(BuildContext context, String title, Widget content, {String rightButtonLabel = '닫기'}) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (context) {
|
|
return Container(
|
|
height: MediaQuery.of(context).size.height * 0.85,
|
|
decoration: BoxDecoration(
|
|
color: _pageBackgroundColor,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
margin: const EdgeInsets.only(top: 12, bottom: 20),
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: _mainTextColor,
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(rightButtonLabel,
|
|
style: TextStyle(color: _mainBlueColor, fontWeight: FontWeight.bold)),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
const Divider(),
|
|
Expanded(
|
|
child: content,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showEditProfileSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'내 정보 수정',
|
|
ListView(
|
|
padding: const EdgeInsets.all(20),
|
|
children: [
|
|
_buildInputGroup('닉네임', '현재 닉네임'),
|
|
const SizedBox(height: 20),
|
|
_buildInputGroup('이메일', 'user@example.com', isReadOnly: true),
|
|
const SizedBox(height: 20),
|
|
_buildInputGroup('전화번호', '010-1234-5678'),
|
|
],
|
|
),
|
|
rightButtonLabel: '완료',
|
|
);
|
|
}
|
|
|
|
void _showPasswordChangeSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'비밀번호 변경',
|
|
ListView(
|
|
padding: const EdgeInsets.all(20),
|
|
children: [
|
|
_buildInputGroup('현재 비밀번호', '사용 중인 비밀번호 입력', isObscure: true),
|
|
const SizedBox(height: 20),
|
|
_buildInputGroup('새 비밀번호', '영문, 숫자, 특수문자 포함 8자 이상', isObscure: true),
|
|
const SizedBox(height: 20),
|
|
_buildInputGroup('새 비밀번호 확인', '비밀번호 재입력', isObscure: true),
|
|
const SizedBox(height: 30),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _mainBlueColor,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: const Text('변경하기', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showDeviceListSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'로그인 관리',
|
|
ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
_buildSectionTitle('현재 접속 중인 기기'),
|
|
const SizedBox(height: 10),
|
|
_buildDeviceItem('Galaxy S24 Ultra', '서울, 대한민국 • 지금 활동 중', true),
|
|
const SizedBox(height: 30),
|
|
_buildSectionTitle('다른 접속 기기'),
|
|
const SizedBox(height: 10),
|
|
_buildDeviceItem('iPhone 15 Pro', '부산, 대한민국 • 3시간 전', false),
|
|
_buildDeviceItem('Chrome (Windows)', '경기도 성남시 • 1일 전', false),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showNotificationStyleSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'알림 방식 설정',
|
|
ListView(
|
|
padding: const EdgeInsets.all(20),
|
|
children: [
|
|
_buildRadioItem('배너 + 진동', true),
|
|
_buildRadioItem('배너만 표시', false),
|
|
_buildRadioItem('진동만 울림', false),
|
|
_buildRadioItem('무음', false),
|
|
],
|
|
),
|
|
rightButtonLabel: '저장',
|
|
);
|
|
}
|
|
|
|
void _showClearCacheSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'캐시 데이터 관리',
|
|
Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.cleaning_services_outlined, size: 60, color: _subTextColor),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
'저장된 캐시 데이터: 12.5 MB',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _mainTextColor),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'캐시 데이터를 삭제하면 앱의 로딩 속도가 느려질 수 있지만,\n저장 공간을 확보할 수 있습니다.\n로그인 정보나 중요한 설정은 삭제되지 않습니다.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: _subTextColor, height: 1.5),
|
|
),
|
|
const SizedBox(height: 40),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
style: TextButton.styleFrom(
|
|
backgroundColor: _warningColor.withOpacity(0.1),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: Text('모두 삭제하기', style: TextStyle(color: _warningColor, fontWeight: FontWeight.bold)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showTermsSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'이용 약관',
|
|
SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Text(
|
|
'제 1 조 (목적)\n이 약관은 스마트 헬멧 서비스(이하 "서비스")의 이용 조건 및 절차, 이용자와 회사의 권리, 의무, 책임 사항을 규정함을 목적으로 합니다.\n\n제 2 조 (용어의 정의)\n1. "이용자"란 앱에 접속하여 본 약관에 따라 서비스를 이용하는 회원을 말합니다.\n2. "헬멧"이란 회사가 대여하는 스마트 IoT 안전모를 말합니다.\n\n(이하 생략... 더미 데이터입니다.)\n\n제 3 조 (약관의 효력)\n본 약관은 서비스를 신청한 때부터 효력이 발생합니다.',
|
|
style: TextStyle(color: _subTextColor, height: 1.6),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showLicenseSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'오픈소스 라이선스',
|
|
ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
_buildLicenseItem('Flutter', 'Google', 'BSD-style'),
|
|
_buildLicenseItem('Cupertino Icons', 'Google', 'MIT'),
|
|
_buildLicenseItem('Kakao Maps SDK', 'Kakao Corp.', 'Apache 2.0'),
|
|
_buildLicenseItem('Firebase Core', 'Google', 'Apache 2.0'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showSupportSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'고객센터',
|
|
ListView(
|
|
padding: const EdgeInsets.all(20),
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: _accentContainerColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const Text('운영 시간: 평일 09:00 ~ 18:00', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 4),
|
|
Text('(점심시간 12:00 ~ 13:00)', style: TextStyle(color: _subTextColor, fontSize: 12)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildInfoLink('전화 상담 연결', Icons.phone, value: '1588-0000'),
|
|
const SizedBox(height: 10),
|
|
_buildInfoLink('1:1 채팅 상담', Icons.chat_bubble_outline),
|
|
const SizedBox(height: 10),
|
|
_buildInfoLink('이메일 문의', Icons.email_outlined, value: 'help@smarthelmet.com'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showFAQSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'자주 묻는 질문 (FAQ)',
|
|
ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
_buildFAQItem('Q. 헬멧 대여는 어떻게 하나요?', 'A. 메인 화면의 지도에서 가까운 보관함을 찾은 후, QR코드를 스캔하여 대여할 수 있습니다.'),
|
|
_buildFAQItem('Q. 반납이 안 될 때는 어떻게 하나요?', 'A. 보관함의 통신 상태를 확인해 주세요. 지속적으로 실패할 경우 고객센터로 연락 바랍니다.'),
|
|
_buildFAQItem('Q. 결제 수단 변경은 어디서 하나요?', 'A. [마이페이지] > [결제 관리] 메뉴에서 카드 정보를 변경하실 수 있습니다.'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showMapInfoSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'서비스 지역 안내',
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.map_outlined, size: 80, color: Colors.grey.shade300),
|
|
const SizedBox(height: 20),
|
|
Text('서비스 지역 지도 표시 영역', style: TextStyle(fontSize: 16, color: _subTextColor, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
Text('현재 서울, 경기 일부 지역에서\n서비스를 이용하실 수 있습니다.', textAlign: TextAlign.center, style: TextStyle(color: _subTextColor)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showPushPermissionSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'알림 설정 안내',
|
|
Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('알림 권한이 꺼져 있나요?', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 10),
|
|
Text('중요한 헬멧 안전 경고 및 반납 알림을 받으려면 기기 설정에서 알림을 허용해야 합니다.', style: TextStyle(color: _subTextColor, height: 1.5)),
|
|
const SizedBox(height: 30),
|
|
_buildInfoLink('기기 설정으로 이동', Icons.settings, onTap: () => Navigator.pop(context)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showCameraPermissionSheet(BuildContext context) {
|
|
_showCommonModal(
|
|
context,
|
|
'카메라 권한 설정',
|
|
Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: _accentContainerColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(Icons.camera_alt, size: 40, color: _mainBlueColor),
|
|
),
|
|
const SizedBox(height: 24),
|
|
const Text(
|
|
'카메라 권한이 필요합니다',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'헬멧 대여 및 반납 시 QR코드를 스캔하기 위해\n카메라 접근 권한이 반드시 필요합니다.\n권한을 거부하면 서비스를 이용할 수 없습니다.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: _subTextColor, height: 1.5),
|
|
),
|
|
const SizedBox(height: 32),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey.shade200),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text('현재 상태', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
Text('허용됨', style: TextStyle(color: _mainBlueColor, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
),
|
|
const Spacer(),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _mainTextColor,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: const Text('기기 설정에서 변경하기', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDeviceItem(String name, String info, bool isCurrent) {
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: isCurrent ? _mainBlueColor : Colors.transparent, width: 1.5),
|
|
boxShadow: [_cleanShadow],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(isCurrent ? Icons.phone_android : Icons.devices_other, color: isCurrent ? _mainBlueColor : _subTextColor),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(name, style: TextStyle(fontWeight: FontWeight.bold, color: _mainTextColor)),
|
|
const SizedBox(height: 4),
|
|
Text(info, style: TextStyle(fontSize: 12, color: _subTextColor)),
|
|
],
|
|
),
|
|
),
|
|
if (!isCurrent)
|
|
TextButton(
|
|
onPressed: () {},
|
|
child: Text('로그아웃', style: TextStyle(color: _warningColor, fontSize: 12)),
|
|
),
|
|
if (isCurrent)
|
|
Text('현재 기기', style: TextStyle(color: _mainBlueColor, fontSize: 12, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRadioItem(String title, bool isSelected) {
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: ListTile(
|
|
title: Text(title, style: TextStyle(color: _mainTextColor, fontSize: 15)),
|
|
trailing: isSelected ? Icon(Icons.check_circle, color: _mainBlueColor) : Icon(Icons.circle_outlined, color: Colors.grey.shade300),
|
|
onTap: () {},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLicenseItem(String libName, String author, String licenseType) {
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: ExpansionTile(
|
|
title: Text(libName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
|
|
subtitle: Text('$author • $licenseType'),
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Text(
|
|
'Permission is hereby granted, free of charge, to any person obtaining a copy of this software...',
|
|
style: TextStyle(color: _subTextColor, fontSize: 12),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFAQItem(String question, String answer) {
|
|
return Card(
|
|
elevation: 0,
|
|
color: Colors.white,
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), side: BorderSide(color: _accentContainerColor)),
|
|
child: ExpansionTile(
|
|
title: Text(question, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: _mainTextColor)),
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
color: _accentContainerColor.withOpacity(0.5),
|
|
child: Text(answer, style: TextStyle(color: _subTextColor, fontSize: 13, height: 1.5)),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInputGroup(String label, String placeholder, {bool isReadOnly = false, bool isObscure = false}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, style: TextStyle(color: _subTextColor, fontSize: 13, fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 8),
|
|
TextField(
|
|
readOnly: isReadOnly,
|
|
obscureText: isObscure,
|
|
decoration: InputDecoration(
|
|
hintText: placeholder,
|
|
hintStyle: TextStyle(color: _subTextColor.withOpacity(0.5)),
|
|
filled: true,
|
|
fillColor: isReadOnly ? _accentContainerColor : Colors.white,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: _pageBackgroundColor,
|
|
appBar: AppBar(
|
|
scrolledUnderElevation: 0,
|
|
title: Text(
|
|
'설정',
|
|
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16, color: _mainTextColor),
|
|
),
|
|
backgroundColor: _pageBackgroundColor,
|
|
elevation: 0,
|
|
centerTitle: false,
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(Icons.close, color: _mainTextColor),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionTitle('계정 및 보안'),
|
|
const SizedBox(height: 8),
|
|
_buildCardWrapper(
|
|
child: Column(
|
|
children: [
|
|
_buildInfoLink('내 정보 수정', Icons.person_outline, onTap: () => _showEditProfileSheet(context)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('비밀번호 변경', Icons.lock_outline, onTap: () => _showPasswordChangeSheet(context)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('로그인 관리 / 기기 목록', Icons.devices, onTap: () => _showDeviceListSheet(context)),
|
|
_buildDivider(0),
|
|
_buildToggleItem(
|
|
'생체 인증 사용',
|
|
'앱 잠금 해제 및 인증',
|
|
_isBiometricEnabled,
|
|
(val) => setState(() => _isBiometricEnabled = val),
|
|
showDivider: true,
|
|
),
|
|
_buildToggleItem(
|
|
'이상 감지 시 자동 로그아웃',
|
|
'보안 위험 감지 시 즉시 로그아웃',
|
|
_isAutoLogoutEnabled,
|
|
(val) => setState(() => _isAutoLogoutEnabled = val),
|
|
showDivider: true,
|
|
),
|
|
_buildToggleItem(
|
|
'새 기기 로그인 알림',
|
|
'이메일로 알림 발송',
|
|
_isLoginNotificationEnabled,
|
|
(val) => setState(() => _isLoginNotificationEnabled = val),
|
|
showDivider: false,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
_buildSectionTitle('알림 설정'),
|
|
const SizedBox(height: 8),
|
|
_buildCardWrapper(
|
|
child: Column(
|
|
children: [
|
|
_buildToggleItem(
|
|
'알림 전체 수신',
|
|
'모든 푸시 알림을 켜고 끕니다.',
|
|
_isPushEnabled,
|
|
(val) => setState(() => _isPushEnabled = val),
|
|
showDivider: true,
|
|
),
|
|
if (_isPushEnabled) ...[
|
|
_buildToggleItem(
|
|
'대여/반납 알림',
|
|
'시작, 종료, 반납 실패',
|
|
_isRentalAlertEnabled,
|
|
(val) => setState(() => _isRentalAlertEnabled = val),
|
|
showDivider: true,
|
|
),
|
|
_buildToggleItem(
|
|
'보관함 상태 알림',
|
|
'살균/건조 완료, 시스템 오류',
|
|
_isStorageStatusAlert,
|
|
(val) => setState(() => _isStorageStatusAlert = val),
|
|
showDivider: true,
|
|
),
|
|
_buildToggleItem(
|
|
'환경 센서 경고',
|
|
'비정상 온도, 배터리 부족, 통신 장애',
|
|
_isEnvSensorAlert,
|
|
(val) => setState(() => _isEnvSensorAlert = val),
|
|
showDivider: true,
|
|
),
|
|
_buildDivider(16),
|
|
_buildInfoLink('알림 방식', Icons.notifications_active_outlined, value: '배너 + 진동', onTap: () => _showNotificationStyleSheet(context)),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
_buildSectionTitle('데이터 및 캐시'),
|
|
const SizedBox(height: 8),
|
|
_buildCardWrapper(
|
|
child: Column(
|
|
children: [
|
|
_buildInfoLink('캐시 데이터 삭제', Icons.cleaning_services_outlined, value: '12.5 MB', onTap: () => _showClearCacheSheet(context)),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
_buildSectionTitle('위치 및 권한'),
|
|
const SizedBox(height: 8),
|
|
_buildCardWrapper(
|
|
child: Column(
|
|
children: [
|
|
_buildToggleItem(
|
|
'위치 서비스 사용',
|
|
'내 주변 보관함 찾기',
|
|
_isLocationEnabled,
|
|
(val) => setState(() => _isLocationEnabled = val),
|
|
showDivider: true,
|
|
),
|
|
_buildInfoLink('카메라 권한', Icons.camera_alt_outlined, value: '허용됨', onTap: () => _showCameraPermissionSheet(context)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('푸시 권한 설정 안내', Icons.settings_applications_outlined, onTap: () => _showPushPermissionSheet(context)),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
_buildSectionTitle('앱 정보'),
|
|
const SizedBox(height: 8),
|
|
_buildCardWrapper(
|
|
child: Column(
|
|
children: [
|
|
_buildInfoLink('버전 정보', null, value: _currentAppVersion, showArrow: false),
|
|
_buildDivider(16),
|
|
_buildInfoLink('이용 약관 및 개인정보 처리방침', Icons.article_outlined, onTap: () => _showTermsSheet(context)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('오픈소스 라이선스', Icons.code, onTap: () => _showLicenseSheet(context)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('고객센터 / 1:1 문의', Icons.headset_mic_outlined, onTap: () => _showSupportSheet(context)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('FAQ', Icons.help_outline, onTap: () => _showFAQSheet(context)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('서비스 지역 지도 보기', Icons.map_outlined, onTap: () => _showMapInfoSheet(context)),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 40),
|
|
Center(
|
|
child: TextButton(
|
|
onPressed: () {
|
|
_showDeleteAccountDialog(context);
|
|
},
|
|
child: Text(
|
|
'회원 탈퇴 신청',
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Center(
|
|
child: Text(
|
|
'Smart Helmet App © 2025',
|
|
style: TextStyle(color: _subTextColor.withOpacity(0.5), fontSize: 12),
|
|
),
|
|
),
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 4.0),
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(color: _subTextColor, fontSize: 13, fontWeight: FontWeight.w600),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCardWrapper({required Widget child}) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: _cardBackgroundColor,
|
|
borderRadius: BorderRadius.circular(10),
|
|
boxShadow: [_cleanShadow],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildToggleItem(
|
|
String title, String subtitle, bool value, ValueChanged<bool> onChanged,
|
|
{required bool showDivider}) {
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: TextStyle(color: _mainTextColor, fontSize: 14, fontWeight: FontWeight.w500)),
|
|
const SizedBox(height: 2),
|
|
Text(subtitle, style: TextStyle(color: _subTextColor, fontSize: 11)),
|
|
],
|
|
),
|
|
),
|
|
Transform.scale(
|
|
scale: 0.8,
|
|
child: Switch(
|
|
value: value,
|
|
onChanged: onChanged,
|
|
activeColor: _cardBackgroundColor,
|
|
activeTrackColor: _mainBlueColor,
|
|
inactiveThumbColor: Colors.white,
|
|
inactiveTrackColor: _accentContainerColor,
|
|
trackOutlineColor: MaterialStateProperty.resolveWith((states) => Colors.transparent),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (showDivider) _buildDivider(16),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoLink(String title, IconData? icon, {String? value, VoidCallback? onTap, bool showArrow = true}) {
|
|
return InkWell(
|
|
onTap: onTap ?? () {},
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 14.0),
|
|
child: Row(
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(icon, color: _mainTextColor, size: 20),
|
|
const SizedBox(width: 12),
|
|
],
|
|
Expanded(
|
|
child: Text(title, style: TextStyle(color: _mainTextColor, fontSize: 14, fontWeight: FontWeight.w500)),
|
|
),
|
|
if (value != null)
|
|
Text(value, style: TextStyle(color: _subTextColor, fontSize: 13)),
|
|
if (showArrow) ...[
|
|
const SizedBox(width: 8),
|
|
Icon(Icons.arrow_forward_ios, color: _subTextColor.withOpacity(0.5), size: 14),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDivider(double indent) {
|
|
return Divider(
|
|
color: _accentContainerColor,
|
|
height: 1,
|
|
thickness: 1,
|
|
indent: indent,
|
|
endIndent: 0,
|
|
);
|
|
}
|
|
} |