import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; const String _serviceTermContent = """ 제1조 (목적) 본 약관은 메타큐랩 서비스 이용과 관련하여 회사와 회원의 권리, 의무 및 책임 사항, 기타 필요한 사항을 규정함을 목적으로 합니다. 제2조 (약관의 효력 및 변경) 1. 본 약관은 서비스를 이용하고자 하는 모든 회원에 대하여 그 효력을 발생합니다. 2. 회사는 관련 법령을 위배하지 않는 범위에서 본 약관을 개정할 수 있습니다. 3. 개정된 약관은 적용일자 및 개정 사유를 명시하여 현행 약관과 함께 서비스 화면에 게시합니다. """; const String _privacyTermContent = """ 1. 수집하는 개인정보의 항목 회사는 회원가입, 상담, 서비스 신청 등을 위해 아래와 같은 개인정보를 수집하고 있습니다. - 필수 항목: 아이디, 비밀번호, 이름, 전화번호, 이메일 주소 - 선택 항목: 닉네임, 마케팅 정보 수신 동의 여부 2. 개인정보의 수집 및 이용 목적 회사는 다음의 목적을 위해 개인정보를 수집 및 이용합니다. - 서비스 제공에 관한 계약 이행 및 요금 정산 - 회원 관리 및 본인 확인 """; const String _locationTermContent = """ 1. 위치정보의 수집 및 이용 목적 회사는 이용자의 현재 위치를 확인하여 긴급 구조 요청, 주행 경로 기록, 주변 시설 검색 등 위치 기반 서비스를 제공하기 위해 위치정보를 수집 및 이용합니다. 2. 위치정보의 보유 및 이용 기간 회사는 위치정보의 수집 및 이용 목적이 달성된 후에는 해당 정보를 지체 없이 파기합니다. 단, 관련 법령의 규정에 의하여 보존할 필요가 있는 경우 법령에서 정한 기간 동안 보관합니다. """; const String _marketingTermContent = """ 1. 수집 및 이용 목적 이벤트 정보 및 참여 기회 제공, 광고성 정보 제공 등 마케팅 활동을 위해 사용됩니다. 2. 수신 동의 철회 회원은 언제든지 이메일 또는 고객센터를 통해 마케팅 정보 수신 동의를 철회할 수 있습니다. 수신 동의를 철회하더라도 기본 서비스 이용에는 제한이 없습니다. """; 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)); } }