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 createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { 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 _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: () {}, child: Text( '회원 탈퇴 신청', style: TextStyle( color: _warningColor, fontWeight: FontWeight.bold, fontSize: 14, ), ), ), ), 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 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, ); } }