1102 lines
53 KiB
Dart
1102 lines
53 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,
|
|
);
|
|
|
|
const String _serviceTermContent =
|
|
'제1조 (목적)\n'
|
|
'본 약관은 (주)메타큐랩(이하 "회사")이 제공하는 공유 전동 킥보드용 스마트 안전모 보관함 및 관련 애플리케이션 서비스(이하 "서비스")의 이용과 관련하여 회사와 회원의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.\n\n'
|
|
'제2조 (용어의 정의)\n'
|
|
'1. "서비스"란 회사가 제공하는 스마트 안전모 보관함 앱을 통해 안전모를 대여, 반납, 관리하는 모든 제반 서비스를 의미합니다.\n'
|
|
'2. "보관함"이란 안전모의 보관, 살균, 건조, 잠금 기능을 수행하는 물리적 하드웨어 장치를 말합니다.\n'
|
|
'3. "회원"이란 앱을 설치하고 본 약관에 동의하여 회사와 이용계약을 체결한 자를 말합니다.\n'
|
|
'4. "대여"란 앱을 통해 보관함의 잠금을 해제하고 안전모를 수령하는 행위를 말합니다.\n'
|
|
'5. "반납"이란 사용 후 안전모를 보관함에 넣고 문을 닫아 센서가 안전모를 인식하고 잠금이 완료된 상태를 말합니다.\n\n'
|
|
'제3조 (이용계약의 체결)\n'
|
|
'1. 이용계약은 회원이 되고자 하는 자(이하 "가입신청자")가 약관의 내용에 대하여 동의를 한 다음 회원가입 신청을 하고 회사가 이러한 신청을 승낙함으로써 체결됩니다.\n'
|
|
'2. 가입신청자는 앱 내에서 아이디(이메일), 비밀번호 등 필수 정보를 입력해야 하며, 회사는 필요시 본인 인증을 요구할 수 있습니다.\n\n'
|
|
'제4조 (서비스의 제공 및 기능)\n'
|
|
'회사는 회원에게 다음과 같은 서비스를 제공합니다.\n'
|
|
'1. 안전모 보관함 위치 찾기 (지도 및 리스트 제공)\n'
|
|
'2. 보관함 잠금장치 원격 제어 (대여/반납)\n'
|
|
'3. 안전모 살균 및 건조 상태 모니터링\n'
|
|
'4. 이용 내역 조회 및 알림 서비스\n\n'
|
|
'제5조 (대여 및 반납)\n'
|
|
'1. 회원은 앱을 통해 이용 가능한 보관함을 확인하고 잠금을 해제하여 안전모를 대여할 수 있습니다.\n'
|
|
'2. 회원은 이용 종료 후 안전모를 지정된 보관함에 넣고 문을 닫아야 합니다.\n'
|
|
'3. (반납의 완료) 반납 처리는 보관함 내부 센서가 안전모를 정상적으로 인식하고 문이 잠길 때 완료됩니다. 안전모가 감지되지 않거나 도어가 닫히지 않을 경우 반납으로 처리되지 않으며, 이에 따른 불이익은 회원이 부담합니다.\n'
|
|
'4. 센서 오류 등으로 반납 처리가 되지 않을 경우, 회원은 앱 내 "비상 신고 버튼" 또는 사진 업로드 기능을 통해 반납 사실을 증명해야 합니다.\n\n'
|
|
'제6조 (이용 요금 및 결제)\n'
|
|
'1. 서비스 이용 요금은 회사의 정책에 따르며 앱 내에 공지합니다.\n'
|
|
'2. 회원이 안전모를 분실하거나 파손한 경우, 회사는 별도의 실비 변상을 청구할 수 있습니다.\n\n'
|
|
'제7조 (살균 및 건조)\n'
|
|
'보관함은 반납 완료 후 자동으로 UV 살균 및 건조 기능을 수행합니다. 단, 회원은 착용 전 안전모의 상태를 육안으로 확인해야 하며, 오염 등이 심한 경우 이용을 중단하고 신고해야 합니다.\n\n'
|
|
'제8조 (회원의 의무)\n'
|
|
'1. 회원은 도로교통법 등 관련 법령에 따라 전동 킥보드 탑승 시 반드시 안전모를 착용해야 합니다.\n'
|
|
'2. 회원은 대여한 안전모를 제3자에게 양도하거나 대여해서는 안 됩니다.\n'
|
|
'3. 회원은 보관함 및 안전모를 파손하거나 기능을 임의로 조작해서는 안 됩니다.\n\n'
|
|
'제9조 (위치기반서비스의 내용)\n'
|
|
'회사는 회원의 위치 정보를 이용하여 주변 보관함 검색 기능을 제공하며, 회원의 위치 정보는 서비스 제공 목적 외에는 사용되지 않습니다.\n\n'
|
|
'제10조 (책임 제한)\n'
|
|
'1. 회사는 천재지변 또는 이에 준하는 불가항력으로 인하여 서비스를 제공할 수 없는 경우에는 서비스 제공에 관한 책임이 면제됩니다.\n'
|
|
'2. 회사는 회원의 귀책사유로 인한 서비스 이용의 장애에 대하여는 책임을 지지 않습니다.\n'
|
|
'3. 회원은 안전모 미착용 또는 올바르지 않은 착용으로 인해 발생한 사고에 대해 전적으로 책임을 지며, 회사는 이에 대해 책임을 지지 않습니다.\n\n'
|
|
'제11조 (서비스 알림)\n'
|
|
'회사는 회원의 안전한 이용을 위해 대여/반납 현황, 미반납 알림, 이상 감지 알림 등을 PUSH 알림으로 전송할 수 있습니다.\n\n'
|
|
'제12조 (준거법 및 재판관할)\n'
|
|
'본 약관에 명시되지 않은 사항은 대한민국의 관계 법령에 따르며, 서비스 이용으로 발생한 분쟁에 대해 소송이 제기되는 경우 회사의 본점 소재지를 관할하는 법원을 전속 관할법원으로 합니다.\n\n'
|
|
'부 칙\n'
|
|
'본 약관은 2025년 11월 28일부터 시행합니다.\n';
|
|
|
|
const String _privacyTermContent =
|
|
'(주)메타큐랩(이하 "회사")은 정보통신망 이용촉진 및 정보보호 등에 관한 법률, 개인정보보호법 등 관련 법령을 준수하며, 이용자의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.\n\n'
|
|
'1. 개인정보의 수집 및 이용 목적\n'
|
|
'회사는 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\n'
|
|
'가. 회원 가입 및 관리\n'
|
|
'- 회원제 서비스 제공에 따른 본인 식별·인증\n'
|
|
'- 회원 자격 유지·관리, 서비스 부정이용 방지\n'
|
|
'- 만 14세 미만 아동의 개인정보 처리 시 법정대리인의 동의 여부 확인\n'
|
|
'- 각종 고지·통지, 고충 처리\n'
|
|
'나. 재화 또는 서비스 제공\n'
|
|
'- 안전모 보관함 위치 찾기: 지도 기반의 보관함 위치 정보 제공\n'
|
|
'- 보관함 대여 및 반납: 잠금장치 제어, 대여/반납 이력 관리\n'
|
|
'- 안전 관리: 안전모 착용 여부 확인, 살균/건조 상태 안내\n'
|
|
'- 장애 대응: 보관함 고장 신고 시 위치 및 현장 사진 접수\n'
|
|
'다. 신규 서비스 개발 및 마케팅·광고에의 활용\n'
|
|
'- 신규 서비스 개발 및 맞춤 서비스 제공\n'
|
|
'- 이벤트 및 광고성 정보 제공 및 참여기회 제공 (마케팅 동의 시)\n'
|
|
'- 서비스 유효성 확인, 접속 빈도 파악 또는 회원의 서비스 이용에 대한 통계\n\n'
|
|
'2. 수집하는 개인정보의 항목 및 수집 방법\n'
|
|
'회사는 서비스 제공을 위해 아래와 같은 개인정보를 수집하고 있습니다.\n'
|
|
'가. 필수적 수집 항목\n'
|
|
'- 로그인 정보: 아이디(이메일), 비밀번호, 이름, 휴대전화번호\n'
|
|
'- 기기 정보: 기기 식별 고유번호(Device ID), PUSH 토큰(알림 수신용)\n'
|
|
'나. 서비스 이용 과정에서 생성/수집되는 항목\n'
|
|
'- 서비스 이용 기록(대여/반납 일시, 이용 보관함 정보), 접속 로그, 쿠키, 접속 IP 정보, 불량 이용 기록\n'
|
|
'- 위치 정보: 이용자의 현재 위치 (주변 보관함 검색 목적, 서버에 저장되지 않음)\n'
|
|
'다. 앱 접근 권한을 통한 수집 항목\n'
|
|
'- 카메라: 반납 인증 사진 촬영, 고장/오류 신고 시 사진 첨부\n'
|
|
'- 블루투스: 보관함 잠금장치 무선 연결 및 제어\n'
|
|
'- 위치: 지도상 현재 위치 표시 및 주변 기기 검색\n\n'
|
|
'3. 개인정보의 처리 및 보유 기간\n'
|
|
'회사는 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.\n'
|
|
'- 회원 가입 및 관리 정보: 회원 탈퇴 시까지 (단, 관계 법령 위반에 따른 수사·조사 등이 진행 중인 경우에는 해당 수사·조사 종료 시까지)\n'
|
|
'- 재화 또는 서비스 제공: 재화·서비스 공급완료 및 요금결제·정산 완료 시까지\n'
|
|
'- 관련 법령에 의한 정보 보유:\n'
|
|
' 1) 계약 또는 청약철회 등에 관한 기록: 5년 (전자상거래 등에서의 소비자보호에 관한 법률)\n'
|
|
' 2) 대금결제 및 재화 등의 공급에 관한 기록: 5년\n'
|
|
' 3) 소비자의 불만 또는 분쟁처리에 관한 기록: 3년\n'
|
|
' 4) 웹사이트 방문 기록: 3개월 (통신비밀보호법)\n\n'
|
|
'4. 개인정보의 파기 절차 및 방법\n'
|
|
'회사는 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체 없이 해당 개인정보를 파기합니다.\n'
|
|
'- 파기절차: 파기 사유가 발생한 개인정보를 선정하고, 회사의 개인정보 보호책임자의 승인을 받아 파기합니다.\n'
|
|
'- 파기방법: 전자적 파일 형태로 기록·저장된 개인정보는 기록을 재생할 수 없도록 파기하며, 종이 문서에 기록·저장된 개인정보는 분쇄기로 분쇄하거나 소각하여 파기합니다.\n\n'
|
|
'5. 이용자와 법정대리인의 권리·의무 및 행사방법\n'
|
|
'- 이용자는 회사에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다. (앱 내 "내 정보 수정" 또는 "회원 탈퇴" 메뉴 이용)\n'
|
|
'- 권리 행사는 회사에 대해 서면, 전화, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 회사는 이에 대해 지체 없이 조치하겠습니다.\n\n'
|
|
'6. 개인정보의 안전성 확보조치\n'
|
|
'회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.\n'
|
|
'- 관리적 조치: 내부관리계획 수립·시행, 정기적 직원 교육\n'
|
|
'- 기술적 조치: 개인정보처리시스템 등의 접근권한 관리, 비밀번호 등 중요 정보의 암호화 저장, 보안프로그램 설치 및 주기적 점검\n'
|
|
'- 물리적 조치: 전산실, 자료보관실 등의 접근통제\n\n'
|
|
'7. 개인정보 보호책임자\n'
|
|
'회사는 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.\n'
|
|
'- 성명: 임지은\n'
|
|
'- 직책: 대표이사\n'
|
|
'- 연락처: 070-4272-9322, ljieun9005@gmail.com\n\n'
|
|
'8. 개인정보 처리방침의 변경 및 고지 의무\n'
|
|
'회사는 개인정보 처리방침의 법령, 정책 또는 보안기술의 변경에 따라 내용의 추가, 삭제 및 수정이 있을 시에는 개정 최소 7일 전부터 애플리케이션 내 "공지사항", 팝업, 또는 앱 PUSH 알림 등을 통하여 변경 사유 및 내용을 공지하도록 하겠습니다.\n'
|
|
'다만, 수집하는 개인정보의 항목, 이용목적의 변경 등과 같이 이용자의 권리에 중대한 변경이 발생할 때에는 최소 30일 전에 미리 공지하며, 필요 시 이용자의 동의를 다시 받을 수 있습니다.\n\n'
|
|
'부 칙\n'
|
|
'개인정보 처리방침은 2025년 11월 28일부터 시행합니다.\n';
|
|
|
|
const String _locationTermContent =
|
|
'[위치기반서비스 이용약관]\n\n'
|
|
'제 1 조 (목적)\n'
|
|
'본 약관은 (주)메타큐랩(이하 "회사")이 제공하는 스마트 안전모 보관함 애플리케이션 서비스(이하 "서비스")와 관련하여 회사와 개인위치정보주체(이하 "회원") 간의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.\n\n'
|
|
'제 2 조 (약관의 효력 및 변경)\n'
|
|
'1. 본 약관은 서비스를 신청한 고객 또는 개인위치정보주체가 본 약관에 동의하고 회사가 소정의 절차에 따라 서비스의 이용자로 등록함으로써 효력이 발생합니다.\n'
|
|
'2. 회사는 법률이나 위치기반서비스의 변경사항을 반영하기 위해 본 약관을 수정할 수 있으며, 약관을 개정할 경우에는 적용일자 7일 전부터 웹사이트 또는 애플리케이션 공지사항을 통해 공지합니다. 다만, 회원에게 불리한 내용으로 변경할 경우에는 최소 30일 전에 공지합니다.\n\n'
|
|
'제 3 조 (서비스의 내용)\n'
|
|
'회사는 위치정보사업자로부터 위치정보를 전달받아 아래와 같은 위치기반서비스를 제공합니다.\n'
|
|
'- 내 주변 보관함 찾기: 회원의 현재 위치를 기반으로 주변에 있는 스마트 안전모 보관함의 위치를 지도에 표시하고, 거리순으로 정렬하여 제공합니다.\n'
|
|
'- 이용 내역 및 경로 관리: 안전모 대여 시점과 반납 시점의 위치 정보를 기록하여 이용 내역을 관리하고, 비정상적인 반납(지정 구역 외 반납 등)을 확인합니다.\n'
|
|
'- 긴급 구조 및 사고/장애 신고: 보관함 이용 중 사고 또는 장애 발생 시, 신고 접수 단계에서 회원의 현재 위치를 확인하여 신속한 유지보수 및 대응을 지원합니다.\n\n'
|
|
'제 4 조 (서비스 이용요금)\n'
|
|
'회사가 제공하는 위치기반서비스는 무료입니다. 단, 무선 서비스 이용 시 발생하는 데이터 통신료는 별도이며, 이는 회원이 가입한 이동통신사의 정책에 따릅니다.\n\n'
|
|
'제 5 조 (개인위치정보주체의 권리)\n'
|
|
'1. 회원은 언제든지 개인위치정보의 수집, 이용 또는 제공에 대한 동의의 전부 또는 일부를 철회할 수 있습니다. 이 경우 회사는 수집한 개인위치정보 및 위치정보 이용·제공사실 확인자료를 파기합니다.\n'
|
|
'2. 회원은 언제든지 개인위치정보의 수집, 이용 또는 제공의 일시적인 중지를 요구할 수 있으며, 회사는 이를 거절할 수 없습니다.\n'
|
|
'3. 회원은 회사에 대하여 다음 각 호의 자료에 대한 열람 또는 고지를 요구할 수 있고, 당해 자료에 오류가 있는 경우에는 그 정정을 요구할 수 있습니다.\n'
|
|
'- 본인에 대한 위치정보 수집, 이용, 제공사실 확인자료\n'
|
|
'- 본인의 개인위치정보가 위치정보의 보호 및 이용 등에 관한 법률 또는 다른 법률 규정에 의하여 제3자에게 제공된 이유 및 내용\n\n'
|
|
'제 6 조 (위치정보 이용·제공사실 확인자료의 보유근거 및 보유기간)\n'
|
|
'회사는 위치정보의 보호 및 이용 등에 관한 법률 제16조 제2항에 근거하여 회원의 위치정보 수집, 이용, 제공사실 확인자료를 위치정보시스템에 자동으로 기록하며, 해당 자료는 6개월 이상 보관합니다.\n\n'
|
|
'제 7 조 (서비스의 변경 및 중지)\n'
|
|
'1. 회사는 위치정보사업자의 정책 변경 또는 기술적 사정으로 인하여 서비스의 전부 또는 일부를 제한, 변경하거나 중지할 수 있습니다.\n'
|
|
'2. 위와 같은 경우 회사는 그 사유를 사전에 공지하거나 통지합니다.\n\n'
|
|
'제 8 조 (개인위치정보의 제3자 제공)\n'
|
|
'회사는 회원의 동의 없이 개인위치정보를 제3자에게 제공하지 않으며, 제3자에게 제공하는 경우에는 제공받는 자, 제공일시 및 제공목적을 즉시 회원에게 통보합니다.\n'
|
|
'단, 법령의 규정에 의거하거나, 수사 목적으로 법령에 정해진 절차와 방법에 따라 수사기관의 요구가 있는 경우는 예외로 합니다.\n\n'
|
|
'제 9 조 (손해배상)\n'
|
|
'회사가 위치정보의 보호 및 이용 등에 관한 법률 제15조 내지 제26조의 규정을 위반하여 회원에게 손해가 발생한 경우, 회원이 고의 또는 과실 없음을 입증하지 아니하면 회원은 그 손해에 대해 배상을 청구할 수 있습니다.\n\n'
|
|
'제 10 조 (분쟁의 조정)\n'
|
|
'1. 회사는 위치정보와 관련된 분쟁에 대해 당사자 간 협의가 이루어지지 아니하거나 협의를 할 수 없는 경우에는 방송통신위원회에 재정을 신청할 수 있습니다.\n'
|
|
'2. 회사 또는 고객은 위치정보와 관련된 분쟁에 대해 당사자 간 협의가 이루어지지 아니하거나 협의를 할 수 없는 경우에는 개인정보분쟁조정위원회에 조정을 신청할 수 있습니다.\n\n'
|
|
'제 11 조 (사업자 정보 및 위치정보 관리책임자)\n'
|
|
'1. 회사의 상호 및 주소 등은 다음과 같습니다.\n'
|
|
'- 상호: 주식회사 메타큐랩\n'
|
|
'- 주소: 전라남도 나주시 빛가람로 760, 103호\n'
|
|
'- 대표전화: 070-4272-9322\n'
|
|
'2. 회사는 개인위치정보를 적절히 관리·보호하고 개인위치정보주체의 불만을 원활히 처리할 수 있도록 실질적인 책임을 질 수 있는 지위에 있는 자를 위치정보 관리책임자로 지정해 운영합니다.\n'
|
|
'- 위치정보 관리책임자: 임지은\n'
|
|
'- 연락처: ljieun9005@gmail.com\n\n'
|
|
'부칙 제1조 (시행일)\n'
|
|
'본 약관은 2025년 11월 28일부터 시행합니다.\n';
|
|
|
|
const String _marketingTermContent = """
|
|
1. 수집 및 이용 목적
|
|
이벤트 정보 및 참여 기회 제공, 광고성 정보 제공 등 마케팅 활동을 위해 사용됩니다.
|
|
|
|
2. 수신 동의 철회
|
|
회원은 언제든지 설정 메뉴 또는 고객센터를 통해 마케팅 정보 수신 동의를 철회할 수 있습니다. 수신 동의를 철회하더라도 기본 서비스 이용에는 제한이 없습니다.
|
|
""";
|
|
|
|
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 _isMarketingAlertEnabled = 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 _showTermContentSheet(BuildContext context, String title, String content) {
|
|
_showCommonModal(
|
|
context,
|
|
title,
|
|
SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: Text(
|
|
content,
|
|
textAlign: TextAlign.start,
|
|
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: '070-4272-9322'),
|
|
const SizedBox(height: 10),
|
|
_buildInfoLink('1:1 채팅 상담', Icons.chat_bubble_outline),
|
|
const SizedBox(height: 10),
|
|
_buildInfoLink('이메일 문의', Icons.email_outlined, value: 'ljieun9005@gmail.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,
|
|
),
|
|
_buildToggleItem(
|
|
'이벤트 및 마케팅 알림',
|
|
'혜택 및 소식 받기',
|
|
_isMarketingAlertEnabled,
|
|
(val) => setState(() => _isMarketingAlertEnabled = val),
|
|
showDivider: true,
|
|
onTapLabel: () => _showTermContentSheet(context, '마케팅 수신 동의', _marketingTermContent),
|
|
),
|
|
_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.description_outlined, onTap: () => _showTermContentSheet(context, '서비스 이용약관', _serviceTermContent)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('개인정보 처리방침', Icons.privacy_tip_outlined, onTap: () => _showTermContentSheet(context, '개인정보 처리방침', _privacyTermContent)),
|
|
_buildDivider(16),
|
|
_buildInfoLink('위치기반 서비스 이용약관', Icons.location_on_outlined, onTap: () => _showTermContentSheet(context, '위치기반 서비스 이용약관', _locationTermContent)),
|
|
_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, VoidCallback? onTapLabel}) {
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: onTapLabel,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(title, style: TextStyle(color: _mainTextColor, fontSize: 14, fontWeight: FontWeight.w500)),
|
|
if (onTapLabel != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 4.0),
|
|
child: Icon(Icons.info_outline, size: 14, color: _subTextColor),
|
|
),
|
|
],
|
|
),
|
|
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,
|
|
);
|
|
}
|
|
} |