25.12.04
회원 가입 페이지(Register Screen) UI 구현 , 탈퇴 팝업 UI 개선 및 이미지 수정
This commit is contained in:
437
lib/register_screen.dart
Normal file
437
lib/register_screen.dart
Normal file
@@ -0,0 +1,437 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user