forked from baron/baron-sso
가족사 이메일 자동 매핑핑
This commit is contained in:
@@ -14,7 +14,7 @@ class SignupScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _SignupScreenState extends State<SignupScreen> {
|
class _SignupScreenState extends State<SignupScreen> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
int _currentStep = 1; // 1: 인증, 2: 정보, 3: 비밀번호
|
int _currentStep = 1;
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
@@ -31,6 +31,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
bool _isPhoneVerified = false;
|
bool _isPhoneVerified = false;
|
||||||
String _affiliationType = 'GENERAL';
|
String _affiliationType = 'GENERAL';
|
||||||
String? _companyCode;
|
String? _companyCode;
|
||||||
|
bool _isAffiliateEmail = false; // 가족사 이메일 여부
|
||||||
bool _termsAccepted = false;
|
bool _termsAccepted = false;
|
||||||
bool _privacyAccepted = false;
|
bool _privacyAccepted = false;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
@@ -47,6 +48,16 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
Timer? _phoneTimer;
|
Timer? _phoneTimer;
|
||||||
int _phoneSeconds = 0;
|
int _phoneSeconds = 0;
|
||||||
|
|
||||||
|
// 가족사 도메인 맵
|
||||||
|
final Map<String, String> _affiliateDomains = {
|
||||||
|
'hanmaceng.co.kr': 'HANMAC',
|
||||||
|
'samaneng.com': 'SAMAN',
|
||||||
|
'jangheon.co.kr': 'JANGHEON',
|
||||||
|
'hallasanup.com': 'HALLA',
|
||||||
|
'pre-cast.co.kr': 'PTC',
|
||||||
|
'baroncs.co.kr': 'BARON',
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_emailTimer?.cancel();
|
_emailTimer?.cancel();
|
||||||
@@ -62,7 +73,36 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Logic Methods ---
|
// 이메일 입력 시 도메인 체크 로직
|
||||||
|
void _checkEmailAffiliation(String email) {
|
||||||
|
if (!email.contains('@')) {
|
||||||
|
if (_isAffiliateEmail) {
|
||||||
|
setState(() {
|
||||||
|
_isAffiliateEmail = false;
|
||||||
|
_affiliationType = 'GENERAL';
|
||||||
|
_companyCode = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final domain = email.split('@').last.toLowerCase();
|
||||||
|
if (_affiliateDomains.containsKey(domain)) {
|
||||||
|
setState(() {
|
||||||
|
_isAffiliateEmail = true;
|
||||||
|
_affiliationType = 'AFFILIATE';
|
||||||
|
_companyCode = _affiliateDomains[domain];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (_isAffiliateEmail) {
|
||||||
|
setState(() {
|
||||||
|
_isAffiliateEmail = false;
|
||||||
|
_affiliationType = 'GENERAL';
|
||||||
|
_companyCode = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _startTimer(String type) {
|
void _startTimer(String type) {
|
||||||
if (type == 'email') {
|
if (type == 'email') {
|
||||||
@@ -101,7 +141,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
setState(() => _emailError = '유효한 이메일 형식이 아닙니다.');
|
setState(() => _emailError = '유효한 이메일 형식이 아닙니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() { _isLoading = true; _emailError = null; });
|
setState(() { _isLoading = true; _emailError = null; });
|
||||||
try {
|
try {
|
||||||
final available = await AuthProxyService.checkEmailAvailability(email);
|
final available = await AuthProxyService.checkEmailAvailability(email);
|
||||||
@@ -178,7 +217,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
if (!_termsAccepted || !_privacyAccepted) return;
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
@@ -231,11 +269,13 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_stepCircle(1, '본인인증'),
|
_stepCircle(1, '약관동의'),
|
||||||
_stepLine(1),
|
_stepLine(1),
|
||||||
_stepCircle(2, '정보입력'),
|
_stepCircle(2, '본인인증'),
|
||||||
_stepLine(2),
|
_stepLine(2),
|
||||||
_stepCircle(3, '비밀번호'),
|
_stepCircle(3, '정보입력'),
|
||||||
|
_stepLine(3),
|
||||||
|
_stepCircle(4, '비밀번호'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -247,12 +287,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 15,
|
radius: 12,
|
||||||
backgroundColor: isDone ? Colors.green : (isCurrent ? Colors.black : Colors.grey[300]),
|
backgroundColor: isDone ? Colors.green : (isCurrent ? Colors.black : Colors.grey[300]),
|
||||||
child: isDone ? const Icon(Icons.check, size: 16, color: Colors.white) : Text('$step', style: TextStyle(color: isCurrent ? Colors.white : Colors.black54, fontSize: 12)),
|
child: isDone ? const Icon(Icons.check, size: 14, color: Colors.white) : Text('$step', style: TextStyle(color: isCurrent ? Colors.white : Colors.black54, fontSize: 10)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(label, style: TextStyle(fontSize: 10, color: isCurrent ? Colors.black : Colors.grey, fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal)),
|
Text(label, style: TextStyle(fontSize: 9, color: isCurrent ? Colors.black : Colors.grey, fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -260,25 +300,92 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
Widget _stepLine(int afterStep) {
|
Widget _stepLine(int afterStep) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16, left: 4, right: 4),
|
margin: const EdgeInsets.only(bottom: 16, left: 2, right: 2),
|
||||||
height: 2,
|
height: 1.5,
|
||||||
color: _currentStep > afterStep ? Colors.green : Colors.grey[300],
|
color: _currentStep > afterStep ? Colors.green : Colors.grey[300],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStep1() {
|
Widget _buildStepAgreement() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('이메일 인증', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600)),
|
Text('서비스 이용을 위해\n약관에 동의해주세요', style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold, height: 1.3)),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 32),
|
||||||
|
_agreementTile(
|
||||||
|
title: '이용약관 동의 (필수)',
|
||||||
|
value: _termsAccepted,
|
||||||
|
onChanged: (val) => setState(() => _termsAccepted = val!),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
_agreementTile(
|
||||||
|
title: '개인정보 수집 및 이용 동의 (필수)',
|
||||||
|
value: _privacyAccepted,
|
||||||
|
onChanged: (val) => setState(() => _privacyAccepted = val!),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: const Text(
|
||||||
|
'Baron SSO는 통합 인증 서비스로, 회원님의 개인정보를 안전하게 보호하며 서비스 제공을 위해 필요한 최소한의 정보만을 수집합니다.',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey, height: 1.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _agreementTile({required String title, required bool value, required ValueChanged<bool?> onChanged}) {
|
||||||
|
return CheckboxListTile(
|
||||||
|
title: Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
activeColor: Colors.black,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStepAuth() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text('본인 확인을 위해\n인증을 진행해주세요', style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 가족사 이메일 안내 문구
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(color: Colors.blue[50], borderRadius: BorderRadius.circular(6)),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, size: 16, color: Colors.blue),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.blue, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text('이메일 인증', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
decoration: InputDecoration(labelText: '이메일 주소', border: const OutlineInputBorder(), errorText: _emailError),
|
onChanged: _checkEmailAffiliation, // 도메인 실시간 체크
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '이메일 주소',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: _emailError,
|
||||||
|
hintText: 'example@hanmaceng.co.kr',
|
||||||
|
),
|
||||||
readOnly: _isEmailVerified,
|
readOnly: _isEmailVerified,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -287,6 +394,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
height: 55,
|
height: 55,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: (_isEmailVerified || _isLoading) ? null : _sendEmailCode,
|
onPressed: (_isEmailVerified || _isLoading) ? null : _sendEmailCode,
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[100], foregroundColor: Colors.black, elevation: 0),
|
||||||
child: Text(_emailSeconds > 0 ? '재발송' : '인증요청'),
|
child: Text(_emailSeconds > 0 ? '재발송' : '인증요청'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -297,7 +405,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailCodeController,
|
controller: _emailCodeController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '이메일 인증코드 6자리',
|
labelText: '인증코드 6자리',
|
||||||
suffixText: _formatTime(_emailSeconds),
|
suffixText: _formatTime(_emailSeconds),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
@@ -308,11 +416,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
],
|
],
|
||||||
if (_isEmailVerified) const Padding(
|
if (_isEmailVerified) const Padding(
|
||||||
padding: EdgeInsets.only(top: 8),
|
padding: EdgeInsets.only(top: 8),
|
||||||
child: Text('✅ 이메일 인증이 완료되었습니다.', style: TextStyle(color: Colors.green, fontSize: 13)),
|
child: Text('✅ 이메일 인증 완료', style: TextStyle(color: Colors.green, fontSize: 13, fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text('휴대폰 인증', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600)),
|
Text('휴대폰 인증', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -328,6 +436,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
height: 55,
|
height: 55,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: (_isPhoneVerified || _isLoading) ? null : _sendPhoneCode,
|
onPressed: (_isPhoneVerified || _isLoading) ? null : _sendPhoneCode,
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[100], foregroundColor: Colors.black, elevation: 0),
|
||||||
child: Text(_phoneSeconds > 0 ? '재발송' : '인증요청'),
|
child: Text(_phoneSeconds > 0 ? '재발송' : '인증요청'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -338,7 +447,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _phoneCodeController,
|
controller: _phoneCodeController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '휴대폰 인증코드 6자리',
|
labelText: '인증코드 6자리',
|
||||||
suffixText: _formatTime(_phoneSeconds),
|
suffixText: _formatTime(_phoneSeconds),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
@@ -349,51 +458,71 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
],
|
],
|
||||||
if (_isPhoneVerified) const Padding(
|
if (_isPhoneVerified) const Padding(
|
||||||
padding: EdgeInsets.only(top: 8),
|
padding: EdgeInsets.only(top: 8),
|
||||||
child: Text('✅ 휴대폰 인증이 완료되었습니다.', style: TextStyle(color: Colors.green, fontSize: 13)),
|
child: Text('✅ 휴대폰 인증 완료', style: TextStyle(color: Colors.green, fontSize: 13, fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStep2() {
|
Widget _buildStepInfo() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('사용자 정보를 입력해주세요', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600)),
|
Text('회원님의\n소속 정보를 알려주세요', style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 24),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
decoration: const InputDecoration(labelText: '이름', border: OutlineInputBorder()),
|
decoration: const InputDecoration(labelText: '이름', border: OutlineInputBorder()),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
DropdownButtonFormField<String>(
|
// 소속 유형 선택 (가족사 메일일 경우 비활성화)
|
||||||
value: _affiliationType,
|
AbsorbPointer(
|
||||||
decoration: const InputDecoration(labelText: '소속 유형', border: OutlineInputBorder()),
|
absorbing: _isAffiliateEmail,
|
||||||
items: const [
|
child: Opacity(
|
||||||
DropdownMenuItem(value: 'GENERAL', child: Text('일반 사용자')),
|
opacity: _isAffiliateEmail ? 0.7 : 1.0,
|
||||||
DropdownMenuItem(value: 'AFFILIATE', child: Text('가족사 임직원')),
|
child: DropdownButtonFormField<String>(
|
||||||
],
|
value: _affiliationType,
|
||||||
onChanged: (val) => setState(() { _affiliationType = val!; }),
|
decoration: InputDecoration(
|
||||||
|
labelText: '소속 유형',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
helperText: _isAffiliateEmail ? '가족사 이메일 사용 시 자동으로 선택됩니다.' : null,
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'GENERAL', child: Text('일반 사용자')),
|
||||||
|
DropdownMenuItem(value: 'AFFILIATE', child: Text('가족사 임직원')),
|
||||||
|
],
|
||||||
|
onChanged: _isAffiliateEmail ? null : (val) => setState(() { _affiliationType = val!; }),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
// 가족사 선택 (가족사 메일일 경우 비활성화)
|
||||||
if (_affiliationType == 'AFFILIATE') ...[
|
if (_affiliationType == 'AFFILIATE') ...[
|
||||||
DropdownButtonFormField<String>(
|
AbsorbPointer(
|
||||||
value: _companyCode,
|
absorbing: _isAffiliateEmail,
|
||||||
decoration: const InputDecoration(labelText: '가족사 선택', border: OutlineInputBorder()),
|
child: Opacity(
|
||||||
items: const [
|
opacity: _isAffiliateEmail ? 0.7 : 1.0,
|
||||||
DropdownMenuItem(value: 'HANMAC', child: Text('한맥')),
|
child: DropdownButtonFormField<String>(
|
||||||
DropdownMenuItem(value: 'SAMAN', child: Text('삼안')),
|
value: _companyCode,
|
||||||
DropdownMenuItem(value: 'PTC', child: Text('PTC')),
|
decoration: const InputDecoration(labelText: '가족사 선택', border: OutlineInputBorder()),
|
||||||
DropdownMenuItem(value: 'JANGHEON', child: Text('장헌')),
|
items: const [
|
||||||
DropdownMenuItem(value: 'BARON', child: Text('바론')),
|
DropdownMenuItem(value: 'HANMAC', child: Text('한맥')),
|
||||||
DropdownMenuItem(value: 'HALLA', child: Text('한라')),
|
DropdownMenuItem(value: 'SAMAN', child: Text('삼안')),
|
||||||
],
|
DropdownMenuItem(value: 'PTC', child: Text('PTC')),
|
||||||
onChanged: (val) => setState(() => _companyCode = val),
|
DropdownMenuItem(value: 'JANGHEON', child: Text('장헌')),
|
||||||
|
DropdownMenuItem(value: 'BARON', child: Text('바론')),
|
||||||
|
DropdownMenuItem(value: 'HALLA', child: Text('한라')),
|
||||||
|
],
|
||||||
|
onChanged: _isAffiliateEmail ? null : (val) => setState(() => _companyCode = val),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _deptController,
|
controller: _deptController,
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: _affiliationType == 'AFFILIATE' ? '부서명' : '소속 정보 (선택)',
|
labelText: _affiliationType == 'AFFILIATE' ? '부서명' : '소속 정보 (선택)',
|
||||||
border: const OutlineInputBorder()
|
border: const OutlineInputBorder()
|
||||||
@@ -403,8 +532,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStep3() {
|
Widget _buildStepPassword() {
|
||||||
// 실시간 비밀번호 체크 로직
|
|
||||||
String p = _passwordController.text;
|
String p = _passwordController.text;
|
||||||
bool hasUpper = p.contains(RegExp(r'[A-Z]'));
|
bool hasUpper = p.contains(RegExp(r'[A-Z]'));
|
||||||
bool hasLower = p.contains(RegExp(r'[a-z]'));
|
bool hasLower = p.contains(RegExp(r'[a-z]'));
|
||||||
@@ -415,8 +543,26 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text('보안 설정', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600)),
|
Text('마지막으로\n비밀번호를 설정해주세요', style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
// 비밀번호 정책 안내 박스
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(color: Colors.blue[50], borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.security, size: 18, color: Colors.blue),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'보안 정책: 12자 이상, 대문자/소문자/숫자/특수문자를 각각 최소 1자 이상 포함해야 합니다.',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.blue[800], fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
@@ -428,7 +574,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 실시간 체크 표시 UI
|
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
children: [
|
children: [
|
||||||
@@ -445,11 +590,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
obscureText: true,
|
obscureText: true,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (val != _passwordController.text) {
|
_confirmPasswordError = (val != _passwordController.text) ? '비밀번호가 일치하지 않습니다.' : null;
|
||||||
_confirmPasswordError = '비밀번호가 일치하지 않습니다.';
|
|
||||||
} else {
|
|
||||||
_confirmPasswordError = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -458,14 +599,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
errorText: _confirmPasswordError,
|
errorText: _confirmPasswordError,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
CheckboxListTile(
|
|
||||||
title: const Text('이용약관 및 개인정보 처리방침 동의', style: TextStyle(fontSize: 13)),
|
|
||||||
value: _termsAccepted && _privacyAccepted,
|
|
||||||
onChanged: (val) => setState(() { _termsAccepted = val!; _privacyAccepted = val; }),
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -484,8 +617,19 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool canGoNext = false;
|
bool canGoNext = false;
|
||||||
if (_currentStep == 1 && _isEmailVerified && _isPhoneVerified) canGoNext = true;
|
if (_currentStep == 1 && _termsAccepted && _privacyAccepted) canGoNext = true;
|
||||||
if (_currentStep == 2 && _nameController.text.isNotEmpty && (_affiliationType == 'GENERAL' || _companyCode != null)) canGoNext = true;
|
if (_currentStep == 2 && _isEmailVerified && _isPhoneVerified) canGoNext = true;
|
||||||
|
if (_currentStep == 3) {
|
||||||
|
final nameOk = _nameController.text.trim().isNotEmpty;
|
||||||
|
if (_affiliationType == 'GENERAL') {
|
||||||
|
canGoNext = nameOk;
|
||||||
|
} else {
|
||||||
|
// AFFILIATE 필수: 이름 + 가족사 선택 + 부서명
|
||||||
|
final companyOk = _companyCode != null;
|
||||||
|
final deptOk = _deptController.text.trim().isNotEmpty;
|
||||||
|
canGoNext = nameOk && companyOk && deptOk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
@@ -499,7 +643,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: _buildStepIndicator(),
|
child: _buildStepIndicator(),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -507,7 +651,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: _currentStep == 1 ? _buildStep1() : (_currentStep == 2 ? _buildStep2() : _buildStep3()),
|
child: _currentStep == 1
|
||||||
|
? _buildStepAgreement()
|
||||||
|
: (_currentStep == 2
|
||||||
|
? _buildStepAuth()
|
||||||
|
: (_currentStep == 3 ? _buildStepInfo() : _buildStepPassword())),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -519,24 +667,24 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () => setState(() => _currentStep--),
|
onPressed: () => setState(() => _currentStep--),
|
||||||
style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(55)),
|
style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(55), side: const BorderSide(color: Colors.black)),
|
||||||
child: const Text('이전'),
|
child: const Text('이전', style: TextStyle(color: Colors.black)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
],
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: _currentStep < 3
|
onPressed: _currentStep < 4
|
||||||
? (canGoNext ? () => setState(() => _currentStep++) : null)
|
? (canGoNext ? () => setState(() => _currentStep++) : null)
|
||||||
: (_isLoading || !_termsAccepted ? null : _handleSignup),
|
: (_isLoading ? null : _handleSignup),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(55),
|
minimumSize: const Size.fromHeight(55),
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
),
|
),
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
: Text(_currentStep < 3 ? '다음 단계' : '가입 완료'),
|
: Text(_currentStep < 4 ? '다음 단계' : '가입 완료'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user