476 lines
18 KiB
Dart
476 lines
18 KiB
Dart
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<RegisterScreen> createState() => _RegisterScreenState();
|
|
}
|
|
|
|
class _RegisterScreenState extends State<RegisterScreen> {
|
|
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<bool?> 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));
|
|
}
|
|
} |