diff --git a/assets/images/info1.png b/assets/images/info1.png deleted file mode 100644 index 5063eba..0000000 Binary files a/assets/images/info1.png and /dev/null differ diff --git a/assets/images/info2.png b/assets/images/info2.png deleted file mode 100644 index 10f4435..0000000 Binary files a/assets/images/info2.png and /dev/null differ diff --git a/assets/images/metaq_v.png b/assets/images/metaq_v.png new file mode 100644 index 0000000..2c5ef8b Binary files /dev/null and b/assets/images/metaq_v.png differ diff --git a/assets/images/open.png b/assets/images/open.png deleted file mode 100644 index 71bdad9..0000000 Binary files a/assets/images/open.png and /dev/null differ diff --git a/assets/images/white_1.png b/assets/images/white_1.png new file mode 100644 index 0000000..705dfb4 Binary files /dev/null and b/assets/images/white_1.png differ diff --git a/assets/images/white_2.png b/assets/images/white_2.png new file mode 100644 index 0000000..0e12db0 Binary files /dev/null and b/assets/images/white_2.png differ diff --git a/lib/home_screen_content.dart b/lib/home_screen_content.dart index 5ffa194..9db0b76 100644 --- a/lib/home_screen_content.dart +++ b/lib/home_screen_content.dart @@ -30,19 +30,18 @@ class _HomeScreenContentState extends State { final LockerApi _api = LockerApi(); bool _isLoading = false; -Future _runLockerAction(String name, Future Function() action) async { + Future _runLockerAction(String name, Future Function() action) async { if (_isLoading) return; setState(() => _isLoading = true); - + // 실제 명령 전송 final success = await action(); - - setState(() => _isLoading = false); if (!mounted) return; - } + setState(() => _isLoading = false); + } // 25.12.03 지은 추가 끝 void test() async { @@ -108,7 +107,7 @@ Future _runLockerAction(String name, Future Function() action) async Widget _buildOverviewSection() { return Container( margin: const EdgeInsets.only(top: 5), - child: Card( + child: DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Column( @@ -170,7 +169,7 @@ Future _runLockerAction(String name, Future Function() action) async _selectedImageIndex == 0 ? 'assets/images/storage.png' : 'assets/images/top.png', - width: 100, + width: 90, ), ), Positioned( @@ -261,7 +260,7 @@ Future _runLockerAction(String name, Future Function() action) async } Widget _buildBatteryStatusCard() { - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( @@ -342,7 +341,7 @@ Future _runLockerAction(String name, Future Function() action) async } Widget _buildControlCard() { - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( @@ -358,24 +357,24 @@ Future _runLockerAction(String name, Future Function() action) async child: Row( children: [ Expanded( - child: _buildStyledToggleSwitch( - 'UV LED', - _controlToggles['UV LED']!, - // 25.12.03 지은 수정 시작 - (val) { - _runLockerAction("UV 제어", () async { - bool success = await _api.setUV(val); + child: _buildStyledToggleSwitch( + 'UV LED', + _controlToggles['UV LED']!, + // 25.12.03 지은 수정 시작 + (val) { + _runLockerAction("UV 제어", () async { + bool success = await _api.setUV(val); - if (success) { - setState(() => _controlToggles['UV LED'] = val); - } - return success; - } - ); - }, - ), - ), - // 25.12.03 지은 수정 끝 + if (success) { + setState(() => _controlToggles['UV LED'] = val); + } + return success; + } + ); + }, + ), + ), + // 25.12.03 지은 수정 끝 VerticalDivider(color: _mainBlueColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch( @@ -390,24 +389,24 @@ Future _runLockerAction(String name, Future Function() action) async (val) => setState(() => _controlToggles['HELMET'] = val))), VerticalDivider(color: _subTextColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( - child: _buildStyledToggleSwitch('FAN', - _controlToggles['FAN']!, - // 25.12.03 지은 수정 시작 - (val) { - print("👉 [디버깅] fan 눌림! 값: $val"); - _runLockerAction("FAN 제어", () async { - print("👉 [디버깅] API 요청 시작..."); - bool success = await _api.setFan(val); - - if (success) { - setState(() => _controlToggles['FAN'] = val); - } - return success; - }); - }, - // 25.12.03 지은 수정 끝 - ), - ), + child: _buildStyledToggleSwitch('FAN', + _controlToggles['FAN']!, + // 25.12.03 지은 수정 시작 + (val) { + print("👉 [디버깅] fan 눌림! 값: $val"); + _runLockerAction("FAN 제어", () async { + print("👉 [디버깅] API 요청 시작..."); + bool success = await _api.setFan(val); + + if (success) { + setState(() => _controlToggles['FAN'] = val); + } + return success; + }); + }, + // 25.12.03 지은 수정 끝 + ), + ), ], ), ) @@ -482,7 +481,7 @@ Future _runLockerAction(String name, Future Function() action) async } Widget _buildEnvironmentSensorsCard() { - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( @@ -552,7 +551,7 @@ Future _runLockerAction(String name, Future Function() action) async Widget _buildMyLocationCard() { const LatLng exampleLocation = LatLng(37.5665, 126.9780); - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, clipBehavior: Clip.antiAlias, @@ -620,7 +619,7 @@ Future _runLockerAction(String name, Future Function() action) async } Widget _buildActivityCard() { - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( @@ -647,9 +646,9 @@ Future _runLockerAction(String name, Future Function() action) async child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _activityText('10:45 AM - 헬멧 잠금 해제'), + _activityText('10:45 AM - 안전모 잠금 해제'), const SizedBox(height: 8), - _activityText('11:00 AM - 헬멧 착용 해제'), + _activityText('11:00 AM - 안전모 착용 해제'), ], ), ), @@ -721,7 +720,7 @@ class _LineChartPainter extends CustomPainter { } } -class Card extends StatelessWidget { +class DashboardCard extends StatelessWidget { final Widget child; final EdgeInsetsGeometry? padding; final Clip clipBehavior; @@ -729,7 +728,7 @@ class Card extends StatelessWidget { final Color? cardColor; final BoxShadow? shadow; - const Card({ + const DashboardCard({ super.key, required this.child, this.padding, diff --git a/lib/login_screen.dart b/lib/login_screen.dart index bcd04d4..013d2df 100644 --- a/lib/login_screen.dart +++ b/lib/login_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'main.dart'; +import 'register_screen.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -15,7 +16,7 @@ class _LoginScreenState extends State { final FocusNode _idFocusNode = FocusNode(); final FocusNode _pwFocusNode = FocusNode(); - final Color mainBlueColor = const Color(0xFF007AFF); + final Color mainBlueColor = const Color(0xFF0033CC); bool _isIdFocused = false; bool _isPwFocused = false; @@ -45,90 +46,101 @@ class _LoginScreenState extends State { return Scaffold( backgroundColor: Colors.white, body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Icon(Icons.polymer, size: 80, color: mainBlueColor), - const SizedBox(height: 20), - Text( - 'METAQLAB', - textAlign: TextAlign.center, - style: TextStyle( - color: mainBlueColor, - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - ), - const SizedBox(height: 60), - - _buildCustomTextField( - label: '아이디', - controller: _idController, - focusNode: _idFocusNode, - isFocused: _isIdFocused, - ), - const SizedBox(height: 16), - - _buildCustomTextField( - label: '비밀번호', - controller: _pwController, - focusNode: _pwFocusNode, - isFocused: _isPwFocused, - isObscure: true, - ), - - const SizedBox(height: 40), - - ElevatedButton( - onPressed: () { - String inputId = _idController.text; - String inputPw = _pwController.text; - - if (inputId == 'user' && inputPw == '1234') { - print('로그인 성공!'); - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - } else { - print('로그인 실패'); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('아이디 또는 비밀번호가 틀렸습니다.'), - backgroundColor: Colors.redAccent, - duration: Duration(seconds: 2), - ), - ); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: mainBlueColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + child: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Image.asset( + 'assets/images/metaq_v.png', + width: 86, + height: 86, + fit: BoxFit.contain, ), - elevation: 0, - ), - child: const Text( - '로그인', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), + const SizedBox(height: 70), + _buildCustomTextField( + label: '아이디', + controller: _idController, + focusNode: _idFocusNode, + isFocused: _isIdFocused, + ), + const SizedBox(height: 16), + _buildCustomTextField( + label: '비밀번호', + controller: _pwController, + focusNode: _pwFocusNode, + isFocused: _isPwFocused, + isObscure: true, + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: () { + String inputId = _idController.text; + String inputPw = _pwController.text; - const SizedBox(height: 20), - TextButton( - onPressed: () {}, - child: Text( - '비밀번호 찾기 / 회원가입', - style: TextStyle(color: mainBlueColor, fontWeight: FontWeight.w500), - ), + if (inputId == 'user' && inputPw == '1234') { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const HomeScreen()), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('아이디 또는 비밀번호가 틀렸습니다.'), + backgroundColor: Colors.redAccent, + duration: Duration(seconds: 2), + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: mainBlueColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Text( + '로그인', + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 12), + + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RegisterScreen()), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: mainBlueColor, + padding: const EdgeInsets.symmetric(vertical: 16), + side: BorderSide(color: mainBlueColor, width: 1.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Text( + '회원가입', + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ], ), - ], + ), ), ), ), @@ -144,7 +156,7 @@ class _LoginScreenState extends State { }) { return AnimatedContainer( duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( color: Colors.white, border: Border.all( @@ -153,36 +165,27 @@ class _LoginScreenState extends State { ), borderRadius: BorderRadius.circular(8), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - color: isFocused ? mainBlueColor : Colors.grey.shade600, - fontSize: 12, - fontWeight: FontWeight.w600, - ), + child: TextField( + controller: controller, + focusNode: focusNode, + obscureText: isObscure, + decoration : InputDecoration( + hintText: label, + hintStyle: TextStyle( + color: Colors.grey.shade400, + fontSize: 16, ), - const SizedBox(height: 4), - TextField( - controller: controller, - focusNode: focusNode, - obscureText: isObscure, - decoration: const InputDecoration( - isDense: true, - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - ), - style: const TextStyle( - fontSize: 16, - color: Colors.black87, - ), - cursorColor: mainBlueColor, - ), - ], + isDense: true, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + style: const TextStyle( + fontSize: 16, + color: Colors.black87, + ), + cursorColor: mainBlueColor, ), ); } diff --git a/lib/main.dart b/lib/main.dart index 22b59f3..57894a6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,7 +39,7 @@ class SmartHelmetApp extends StatelessWidget { bodyMedium: TextStyle(color: _subTextColor, fontWeight: FontWeight.w400), ), ), - home: const HomeScreen(), + home: const LoginScreen(), ); } } diff --git a/lib/register_screen.dart b/lib/register_screen.dart new file mode 100644 index 0000000..d3ea221 --- /dev/null +++ b/lib/register_screen.dart @@ -0,0 +1,520 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +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' + '2. 수집하는 개인정보 항목\n' + '- (필수) 아이디(이메일), 비밀번호, 이름, 휴대전화번호.\n' + '- (자동 수집) 기기 식별 정보(Device ID), PUSH 토큰, 서비스 이용 기록, 접속 로그.\n\n' + '3. 보유 및 이용 기간\n' + '회원 탈퇴 시까지 (단, 관계 법령에 따라 일정 기간 보존이 필요한 경우 해당 기간까지 보관)\n\n' + '4. 동의 거부 권리 및 불이익\n' + '귀하는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다. 단, 필수 항목에 대한 동의를 거부할 경우 회원 가입 및 서비스 이용이 제한됩니다.\n'; + +const String _locationTermContent = """ +1. 위치정보의 수집 및 이용 목적 +회사는 이용자의 현재 위치를 확인하여 긴급 구조 요청, 주행 경로 기록, 주변 시설 검색 등 위치 기반 서비스를 제공하기 위해 위치정보를 수집 및 이용합니다. + +2. 위치정보의 보유 및 이용 기간 +회사는 위치정보의 수집 및 이용 목적이 달성된 후에는 해당 정보를 지체 없이 파기합니다. 단, 관련 법령의 규정에 의하여 보존할 필요가 있는 경우 법령에서 정한 기간 동안 보관합니다. +"""; + +const String _marketingTermContent = + '(주)메타큐랩이 제공하는 이벤트 및 혜택 등 마케팅 정보를 수신하는 것에 동의합니다.\n\n' + '1. 수집 및 이용 목적\n' + '- 신규 서비스(기능) 개발 및 맞춤 서비스 제공.\n' + '- 이벤트, 프로모션, 혜택 등 광고성 정보 제공.\n' + '- 서비스 이용 통계 및 설문조사.\n\n' + '2. 수집하는 개인정보 항목\n' + '- 이름, 휴대전화번호, 이메일, PUSH 토큰.\n\n' + '3. 보유 및 이용 기간\n' + '- 회원 탈퇴 또는 동의 철회 시까지\n\n' + '4. 전송 방법\n' + '- 앱 푸시(App Push) 알림, SMS(문자메시지), 이메일 등.\n\n' + '5. 동의 거부 권리\n' + '귀하는 마케팅 정보 수신에 대한 동의를 거부할 수 있습니다. 동의를 거부하더라도 회원 가입 및 기본 서비스 이용에는 제한이 없습니다. 다만, 이벤트 및 혜택 정보 제공이 제한될 수 있습니다.\n'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final TextEditingController _idController = TextEditingController(); + final TextEditingController _pwController = TextEditingController(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _nicknameController = TextEditingController(); + final TextEditingController _phoneController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + + bool _isServiceAgreed = false; + bool _isPrivacyAgreed = false; + bool _isLocationAgreed = false; + bool _isMarketingAgreed = false; + + final Color mainBlueColor = const Color(0xFF0033CC); + final Color backgroundColor = const Color(0xFFF5F6F8); + final Color iconColor = const Color(0xFF8899A6); + + @override + void dispose() { + _idController.dispose(); + _pwController.dispose(); + _nameController.dispose(); + _nicknameController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + void _tryRegister() { + if (_idController.text.isEmpty) return _showError('아이디를 입력해주세요.'); + if (_pwController.text.isEmpty) return _showError('비밀번호를 입력해주세요.'); + if (_emailController.text.isEmpty) return _showError('이메일을 입력해주세요.'); + if (_nameController.text.isEmpty) return _showError('이름을 입력해주세요.'); + if (_phoneController.text.isEmpty) return _showError('전화번호를 입력해주세요.'); + if (_phoneController.text.length < 12) return _showError('올바른 전화번호를 입력해주세요.'); + if (_nicknameController.text.isEmpty) return _showError('닉네임을 입력해주세요.'); + + if (!_isServiceAgreed || !_isPrivacyAgreed || !_isLocationAgreed) { + return _showError('(필수) 약관에 모두 동의해주세요.'); + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('회원가입이 완료되었습니다! 환영합니다.'), + backgroundColor: mainBlueColor, + behavior: SnackBarBehavior.floating, + ), + ); + + Future.delayed(const Duration(seconds: 1), () { + if (mounted) Navigator.pop(context); + }); + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.redAccent, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 2), + ), + ); + } + + void _showTermDetails(String title, String content) { + String fullContent = content; + + showDialog( + context: context, + builder: (context) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + backgroundColor: Colors.white, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, right: 10, top: 15, bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.grey), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + const Divider(height: 1, thickness: 1), + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Text( + fullContent, + style: const TextStyle(fontSize: 14, height: 1.6, color: Colors.black54), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: mainBlueColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: const Text( + '확인', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + ), + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: backgroundColor, + appBar: AppBar( + backgroundColor: backgroundColor, + elevation: 0, + scrolledUnderElevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + '회원가입', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 18), + ), + centerTitle: true, + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 30.0), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + _buildInputRow( + icon: Icons.account_circle, + hint: '아이디', + controller: _idController, + ), + _buildDivider(), + _buildInputRow( + icon: Icons.lock_outline, + hint: '비밀번호', + controller: _pwController, + isObscure: true, + ), + _buildDivider(), + _buildInputRow( + icon: Icons.email_outlined, + hint: '이메일', + controller: _emailController, + keyboardType: TextInputType.emailAddress, + ), + ], + ), + ), + + const SizedBox(height: 20), + + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + _buildInputRow( + icon: Icons.account_circle, + hint: '이름', + controller: _nameController, + ), + _buildDivider(), + _buildInputRow( + icon: Icons.phone_iphone_outlined, + hint: '전화번호', + controller: _phoneController, + keyboardType: TextInputType.phone, + isPhone: true, + ), + _buildDivider(), + _buildInputRow( + icon: Icons.person_outline, + hint: '닉네임', + controller: _nicknameController, + ), + ], + ), + ), + + const SizedBox(height: 24), + + Container( + padding: const EdgeInsets.all(20.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '약관 동의', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + _buildAgreementRow( + title: '(필수) 서비스 이용약관 동의', + value: _isServiceAgreed, + onChanged: (v) => setState(() => _isServiceAgreed = v!), + details: _serviceTermContent, + ), + const SizedBox(height: 12), + _buildAgreementRow( + title: '(필수) 개인정보 수집 및 이용 동의', + value: _isPrivacyAgreed, + onChanged: (v) => setState(() => _isPrivacyAgreed = v!), + details: _privacyTermContent, + ), + const SizedBox(height: 12), + _buildAgreementRow( + title: '(필수) 위치기반 서비스 이용약관 동의', + value: _isLocationAgreed, + onChanged: (v) => setState(() => _isLocationAgreed = v!), + details: _locationTermContent, + ), + const SizedBox(height: 12), + _buildAgreementRow( + title: '(선택) 이벤트 및 마케팅 수신 동의', + value: _isMarketingAgreed, + onChanged: (v) => setState(() => _isMarketingAgreed = v!), + details: _marketingTermContent, + ), + ], + ), + ), + + const SizedBox(height: 32), + + ElevatedButton( + onPressed: _tryRegister, + style: ElevatedButton.styleFrom( + backgroundColor: mainBlueColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + minimumSize: const Size(double.infinity, 56), + ), + child: const Text( + '가입하기', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ), + ); + } + + Widget _buildDivider() { + return Divider(height: 1, thickness: 1, color: Colors.grey.shade100, indent: 50, endIndent: 20); + } + + Widget _buildInputRow({ + required IconData icon, + required String hint, + required TextEditingController controller, + bool isObscure = false, + TextInputType keyboardType = TextInputType.text, + bool isPhone = false, + }) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Row( + children: [ + Icon(icon, color: iconColor, size: 24), + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: controller, + obscureText: isObscure, + keyboardType: keyboardType, + inputFormatters: isPhone + ? [ + FilteringTextInputFormatter.digitsOnly, + _PhoneNumberFormatter(), + LengthLimitingTextInputFormatter(13), + ] + : [], + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: Colors.grey.shade400, fontSize: 15), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 14), + ), + style: const TextStyle(fontSize: 16, color: Colors.black87), + cursorColor: mainBlueColor, + ), + ), + ], + ), + ); + } + + Widget _buildAgreementRow({ + required String title, + required bool value, + required ValueChanged onChanged, + required String details, + }) { + return Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: Checkbox( + value: value, + activeColor: mainBlueColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + side: BorderSide(color: Colors.grey.shade300, width: 1.5), + onChanged: onChanged, + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => onChanged(!value), + child: Text( + title, + style: const TextStyle(fontSize: 14, color: Colors.black87), + ), + ), + ), + IconButton( + onPressed: () => _showTermDetails(title, details), + icon: const Icon(Icons.arrow_forward_ios_rounded, color: Colors.grey, size: 14), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ); + } +} + +class _PhoneNumberFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + var text = newValue.text; + if (newValue.selection.baseOffset == 0) return newValue; + var buffer = StringBuffer(); + for (int i = 0; i < text.length; i++) { + buffer.write(text[i]); + var nonZeroIndex = i + 1; + if (nonZeroIndex <= 3) { + if (nonZeroIndex % 3 == 0 && nonZeroIndex != text.length) buffer.write('-'); + } else { + if (nonZeroIndex % 7 == 0 && nonZeroIndex != text.length && nonZeroIndex > 4) buffer.write('-'); + } + } + var string = buffer.toString(); + return newValue.copyWith(text: string, selection: TextSelection.collapsed(offset: string.length)); + } +} \ No newline at end of file diff --git a/lib/rent_return_screen.dart b/lib/rent_return_screen.dart index e7f3bfc..54d8218 100644 --- a/lib/rent_return_screen.dart +++ b/lib/rent_return_screen.dart @@ -368,7 +368,7 @@ class RentReturnScreen extends StatelessWidget { padding: const EdgeInsets.all(20), children: [ _buildLogItem("08:58:33", "문 닫힘", Icons.door_front_door, _mainBlueColor), - _buildLogItem("08:58:30", "헬멧 반납 확인", Icons.check_circle_outline, _mainBlueColor), + _buildLogItem("08:58:30", "안전모 반납 확인", Icons.check_circle_outline, _mainBlueColor), _buildLogItem("08:55:12", "사용자 문 잠금 해제", Icons.lock_open, _mainTextColor), _buildLogItem("08:30:00", "UV 살균 완료", Icons.cleaning_services, _mainBlueColor), _buildLogItem("08:00:00", "시스템 가동 중", Icons.power_settings_new, _subTextColor), diff --git a/lib/rental_process_screen.dart b/lib/rental_process_screen.dart index 54fa613..87b2808 100644 --- a/lib/rental_process_screen.dart +++ b/lib/rental_process_screen.dart @@ -53,8 +53,8 @@ Future _runLockerAction(String name, Future Function() action) async Timer? _timer; final List _imageList = [ 'assets/images/top.png', - 'assets/images/info1.png', - 'assets/images/info2.png', + 'assets/images/white_1.png', + 'assets/images/white_2.png', ]; @override @@ -112,7 +112,7 @@ Future _runLockerAction(String name, Future Function() action) async children: [ _buildStatusCard(context), const SizedBox(height: 24), - _buildProcessSectionTitle('헬멧 대여'), + _buildProcessSectionTitle('안전모 대여'), const SizedBox(height: 12), Container( decoration: BoxDecoration( @@ -126,7 +126,7 @@ Future _runLockerAction(String name, Future Function() action) async _buildDivider(), _buildStepRow(2, '문 상태 확인', Icons.sensor_door_outlined), _buildDivider(), - _buildStepRow(3, '헬멧 꺼내기', Icons.outbox), + _buildStepRow(3, '안전모 꺼내기', Icons.outbox), _buildDivider(), _buildStepRow(4, '주행 시작', Icons.sentiment_satisfied_alt, showDivider: false), @@ -134,7 +134,7 @@ Future _runLockerAction(String name, Future Function() action) async ), ), const SizedBox(height: 24), - _buildProcessSectionTitle('헬멧 반납'), + _buildProcessSectionTitle('안전모 반납'), const SizedBox(height: 12), Container( decoration: BoxDecoration( @@ -145,7 +145,7 @@ Future _runLockerAction(String name, Future Function() action) async children: [ _buildStepRow(1, '잠금해제 & 문열림', Icons.lock_open), _buildDivider(), - _buildStepRow(2, '헬멧 넣기', Icons.move_to_inbox), + _buildStepRow(2, '안전모 넣기', Icons.move_to_inbox), _buildDivider(), _buildStepRow(3, '센서 스캔 & 반납 완료', Icons.sync), _buildDivider(), @@ -477,7 +477,7 @@ Future _runLockerAction(String name, Future Function() action) async children: [ _buildLogItem("08:58:33", "문 열림", Icons.door_front_door, _mainBlueColor), - _buildLogItem("08:58:30", "헬멧 반납 확인(센서 A)", + _buildLogItem("08:58:30", "안전모 반납 확인(센서 A)", Icons.check_circle_outline, _mainBlueColor), _buildLogItem("08:55:12", "사용자 문 잠금 해제", Icons.lock_open, _mainTextColor), diff --git a/lib/settings_screen.dart b/lib/settings_screen.dart index aec5e55..f2fd946 100644 --- a/lib/settings_screen.dart +++ b/lib/settings_screen.dart @@ -15,6 +15,172 @@ const BoxShadow _cleanShadow = BoxShadow( 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 = + '(주)메타큐랩이 제공하는 이벤트 및 혜택 등 마케팅 정보를 수신하는 것에 동의합니다.\n\n' + '1. 수집 및 이용 목적\n' + '- 신규 서비스(기능) 개발 및 맞춤 서비스 제공.\n' + '- 이벤트, 프로모션, 혜택 등 광고성 정보 제공.\n' + '- 서비스 이용 통계 및 설문조사.\n\n' + '2. 수집하는 개인정보 항목\n' + '- 이름, 휴대전화번호, 이메일, PUSH 토큰.\n\n' + '3. 보유 및 이용 기간\n' + '- 회원 탈퇴 또는 동의 철회 시까지\n\n' + '4. 전송 방법\n' + '- 앱 푸시(App Push) 알림, SMS(문자메시지), 이메일 등.\n\n' + '5. 동의 거부 권리\n' + '귀하는 마케팅 정보 수신에 대한 동의를 거부할 수 있습니다. 동의를 거부하더라도 회원 가입 및 기본 서비스 이용에는 제한이 없습니다. 다만, 이벤트 및 혜택 정보 제공이 제한될 수 있습니다.\n'; + class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -27,6 +193,7 @@ class _SettingsScreenState extends State { bool _isRentalAlertEnabled = true; bool _isStorageStatusAlert = true; bool _isEnvSensorAlert = true; + bool _isMarketingAlertEnabled = true; bool _isBiometricEnabled = false; bool _isAutoLogoutEnabled = true; @@ -35,6 +202,120 @@ class _SettingsScreenState extends State { 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, @@ -217,15 +498,19 @@ class _SettingsScreenState extends State { ); } - void _showTermsSheet(BuildContext context) { + void _showTermContentSheet(BuildContext context, String title, String content) { _showCommonModal( context, - '이용 약관', + title, 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), + child: SizedBox( + width: double.infinity, + child: Text( + content, + textAlign: TextAlign.start, + style: TextStyle(color: _subTextColor, height: 1.6), + ), ), ), ); @@ -269,11 +554,11 @@ class _SettingsScreenState extends State { ), ), const SizedBox(height: 20), - _buildInfoLink('전화 상담 연결', Icons.phone, value: '1588-0000'), + _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: 'help@smarthelmet.com'), + _buildInfoLink('이메일 문의', Icons.email_outlined, value: 'ljieun9005@gmail.com'), ], ), ); @@ -286,7 +571,7 @@ class _SettingsScreenState extends State { ListView( padding: const EdgeInsets.all(16), children: [ - _buildFAQItem('Q. 헬멧 대여는 어떻게 하나요?', 'A. 메인 화면의 지도에서 가까운 보관함을 찾은 후, QR코드를 스캔하여 대여할 수 있습니다.'), + _buildFAQItem('Q. 안전모 대여는 어떻게 하나요?', 'A. 메인 화면의 지도에서 가까운 보관함을 찾은 후, QR코드를 스캔하여 대여할 수 있습니다.'), _buildFAQItem('Q. 반납이 안 될 때는 어떻게 하나요?', 'A. 보관함의 통신 상태를 확인해 주세요. 지속적으로 실패할 경우 고객센터로 연락 바랍니다.'), _buildFAQItem('Q. 결제 수단 변경은 어디서 하나요?', 'A. [마이페이지] > [결제 관리] 메뉴에서 카드 정보를 변경하실 수 있습니다.'), ], @@ -324,7 +609,7 @@ class _SettingsScreenState extends State { children: [ const Text('알림 권한이 꺼져 있나요?', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), - Text('중요한 헬멧 안전 경고 및 반납 알림을 받으려면 기기 설정에서 알림을 허용해야 합니다.', style: TextStyle(color: _subTextColor, height: 1.5)), + Text('중요한 안전모 안전 경고 및 반납 알림을 받으려면 기기 설정에서 알림을 허용해야 합니다.', style: TextStyle(color: _subTextColor, height: 1.5)), const SizedBox(height: 30), _buildInfoLink('기기 설정으로 이동', Icons.settings, onTap: () => Navigator.pop(context)), ], @@ -356,7 +641,7 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 12), Text( - '헬멧 대여 및 반납 시 QR코드를 스캔하기 위해\n카메라 접근 권한이 반드시 필요합니다.\n권한을 거부하면 서비스를 이용할 수 없습니다.', + '안전모 대여 및 반납 시 QR코드를 스캔하기 위해\n카메라 접근 권한이 반드시 필요합니다.\n권한을 거부하면 서비스를 이용할 수 없습니다.', textAlign: TextAlign.center, style: TextStyle(color: _subTextColor, height: 1.5), ), @@ -609,6 +894,14 @@ class _SettingsScreenState extends State { (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)), ], @@ -655,7 +948,11 @@ class _SettingsScreenState extends State { children: [ _buildInfoLink('버전 정보', null, value: _currentAppVersion, showArrow: false), _buildDivider(16), - _buildInfoLink('이용 약관 및 개인정보 처리방침', Icons.article_outlined, onTap: () => _showTermsSheet(context)), + _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), @@ -671,13 +968,15 @@ class _SettingsScreenState extends State { const SizedBox(height: 40), Center( child: TextButton( - onPressed: () {}, + onPressed: () { + _showDeleteAccountDialog(context); + }, child: Text( '회원 탈퇴 신청', style: TextStyle( - color: _warningColor, - fontWeight: FontWeight.bold, - fontSize: 14, + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 12, ), ), ), @@ -722,7 +1021,7 @@ class _SettingsScreenState extends State { Widget _buildToggleItem( String title, String subtitle, bool value, ValueChanged onChanged, - {required bool showDivider}) { + {required bool showDivider, VoidCallback? onTapLabel}) { return Column( children: [ Padding( @@ -731,13 +1030,25 @@ class _SettingsScreenState extends State { 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)), - ], + 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( diff --git a/lib/widgets/custom_header.dart b/lib/widgets/custom_header.dart index 8c6d78b..09bc12a 100644 --- a/lib/widgets/custom_header.dart +++ b/lib/widgets/custom_header.dart @@ -38,7 +38,7 @@ class CustomHeader extends StatelessWidget { ), const SizedBox(width: 10), Text( - '스마트 헬멧 보관함', + '스마트 안전모 보관함', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500,