Files
smarthelmet_app/lib/register_screen.dart
KIMGYEONGRAN 260e09b3a5 25.12.04
회원 가입 페이지(Register Screen) UI 구현 , 탈퇴 팝업 UI 개선  및 이미지 수정
2025-12-04 17:53:52 +09:00

437 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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 _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) {
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.length < 50
? '$content\n\n'
'제1조 (목적)\n본 약관은 메타큐랩(이하 "회사")이 제공하는 모든 서비스의 이용조건 및 절차, 이용자와 회사의 권리, 의무, 책임사항과 기타 필요한 사항을 규정함을 목적으로 합니다.\n\n'
'제2조 (용어의 정의)\n1. "서비스"라 함은 회원이 이용할 수 있는 관련 제반 서비스를 의미합니다.\n'
'2. "회원"이라 함은 회사의 "서비스"에 접속하여 본 약관에 따라 회사와 이용계약을 체결하고 회사가 제공하는 "서비스"를 이용하는 고객을 말합니다.\n\n'
'제3조 (약관의 게시와 개정)\n1. 회사는 이 약관의 내용을 회원이 쉽게 알 수 있도록 서비스 초기 화면에 게시합니다.\n'
'2. 회사는 "약관의 규제에 관한 법률" 등 관련 법령을 위배하지 않는 범위에서 이 약관을 개정할 수 있습니다.\n\n'
'(이하 생략)'
: 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: '서비스 이용약관 내용입니다...',
),
const SizedBox(height: 12),
_buildAgreementRow(
title: '(필수) 개인정보 수집 및 이용 동의',
value: _isPrivacyAgreed,
onChanged: (v) => setState(() => _isPrivacyAgreed = v!),
details: '개인정보 처리방침 내용입니다...',
),
const SizedBox(height: 12),
_buildAgreementRow(
title: '(선택) 이벤트 및 마케팅 수신 동의',
value: _isMarketingAgreed,
onChanged: (v) => setState(() => _isMarketingAgreed = v!),
details: '마케팅 정보 수신 동의 내용입니다...',
),
],
),
),
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));
}
}