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)); } }