forked from baron/baron-sso
feat: i18n 개선 및 userfront 로그인/로케일 보완
This commit is contained in:
@@ -164,30 +164,36 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
final email = _emailController.text.trim();
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(email)) {
|
||||
setState(() => _emailError = tr(
|
||||
'msg.userfront.signup.email.invalid',
|
||||
fallback: '유효한 이메일 형식이 아닙니다.',
|
||||
));
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.invalid',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() { _isLoading = true; _emailError = null; });
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_emailError = null;
|
||||
});
|
||||
try {
|
||||
final available = await AuthProxyService.checkEmailAvailability(email);
|
||||
if (!available) {
|
||||
setState(() => _emailError = tr(
|
||||
'msg.userfront.signup.email.duplicate',
|
||||
fallback: '이미 가입된 이메일입니다.',
|
||||
));
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.duplicate',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await AuthProxyService.sendSignupCode(email, 'email');
|
||||
_startTimer('email');
|
||||
} catch (e) {
|
||||
setState(() => _emailError = tr(
|
||||
'msg.userfront.signup.email.send_failed',
|
||||
fallback: '발송 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
));
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.send_failed',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
@@ -197,7 +203,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
final code = _emailCodeController.text.trim();
|
||||
if (code.length != 6) return;
|
||||
try {
|
||||
final success = await AuthProxyService.verifySignupCode(_emailController.text.trim(), 'email', code);
|
||||
final success = await AuthProxyService.verifySignupCode(
|
||||
_emailController.text.trim(),
|
||||
'email',
|
||||
code,
|
||||
);
|
||||
if (success) {
|
||||
setState(() {
|
||||
_isEmailVerified = true;
|
||||
@@ -206,33 +216,39 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_emailError = null;
|
||||
});
|
||||
} else {
|
||||
setState(() => _emailError = tr(
|
||||
'msg.userfront.signup.email.code_mismatch',
|
||||
fallback: '인증코드가 일치하지 않습니다.',
|
||||
));
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.code_mismatch',
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _emailError = tr(
|
||||
'msg.userfront.signup.email.verify_failed',
|
||||
fallback: '인증 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
));
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.verify_failed',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendPhoneCode() async {
|
||||
final phone = _phoneController.text.trim();
|
||||
if (phone.isEmpty) return;
|
||||
setState(() { _isLoading = true; _phoneError = null; });
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_phoneError = null;
|
||||
});
|
||||
try {
|
||||
await AuthProxyService.sendSignupCode(phone, 'phone');
|
||||
_startTimer('phone');
|
||||
} catch (e) {
|
||||
setState(() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.send_failed',
|
||||
fallback: '발송 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
));
|
||||
setState(
|
||||
() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.send_failed',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
@@ -242,7 +258,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
final code = _phoneCodeController.text.trim();
|
||||
if (code.length != 6) return;
|
||||
try {
|
||||
final success = await AuthProxyService.verifySignupCode(_phoneController.text.trim(), 'phone', code);
|
||||
final success = await AuthProxyService.verifySignupCode(
|
||||
_phoneController.text.trim(),
|
||||
'phone',
|
||||
code,
|
||||
);
|
||||
if (success) {
|
||||
setState(() {
|
||||
_isPhoneVerified = true;
|
||||
@@ -251,26 +271,29 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_phoneError = null;
|
||||
});
|
||||
} else {
|
||||
setState(() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.code_mismatch',
|
||||
fallback: '인증코드가 일치하지 않습니다.',
|
||||
));
|
||||
setState(
|
||||
() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.code_mismatch',
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.verify_failed',
|
||||
fallback: '인증 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
));
|
||||
setState(
|
||||
() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.verify_failed',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSignup() async {
|
||||
if (_passwordController.text != _confirmPasswordController.text) {
|
||||
setState(() => _confirmPasswordError = tr(
|
||||
'msg.userfront.signup.password.mismatch',
|
||||
fallback: '비밀번호가 일치하지 않습니다.',
|
||||
));
|
||||
setState(
|
||||
() => _confirmPasswordError = tr(
|
||||
'msg.userfront.signup.password.mismatch',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
@@ -288,7 +311,9 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
phone: _phoneController.text.trim(),
|
||||
affiliationType: _affiliationType,
|
||||
companyCode: _affiliationType == 'AFFILIATE' ? _companyCode : null,
|
||||
department: _deptController.text.trim().isEmpty ? (_affiliationType == 'GENERAL' ? 'External' : '') : _deptController.text.trim(),
|
||||
department: _deptController.text.trim().isEmpty
|
||||
? (_affiliationType == 'GENERAL' ? 'External' : '')
|
||||
: _deptController.text.trim(),
|
||||
termsAccepted: true,
|
||||
);
|
||||
if (mounted) _showSuccessDialog();
|
||||
@@ -298,32 +323,26 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
if (eStr.contains('uppercase')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.uppercase_required',
|
||||
fallback: '대문자가 최소 1개 이상 포함되어야 합니다.',
|
||||
);
|
||||
} else if (eStr.contains('lowercase')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.lowercase_required',
|
||||
fallback: '소문자가 최소 1개 이상 포함되어야 합니다.',
|
||||
);
|
||||
} else if (eStr.contains('digit') || eStr.contains('number')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.number_required',
|
||||
fallback: '숫자가 최소 1개 이상 포함되어야 합니다.',
|
||||
);
|
||||
} else if (eStr.contains('symbol') || eStr.contains('special')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.symbol_required',
|
||||
fallback: '특수문자가 최소 1개 이상 포함되어야 합니다.',
|
||||
);
|
||||
} else if (eStr.contains('length') || eStr.contains('12 characters')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.length_required',
|
||||
fallback: '비밀번호는 최소 12자 이상이어야 합니다.',
|
||||
);
|
||||
} else {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.failed',
|
||||
fallback: '가입 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
);
|
||||
}
|
||||
@@ -339,16 +358,16 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
tr('msg.userfront.signup.success.title', fallback: '회원가입 완료'),
|
||||
tr('msg.userfront.signup.success.title'),
|
||||
),
|
||||
content: Text(
|
||||
tr('msg.userfront.signup.success.body', fallback: '성공적으로 가입되었습니다.'),
|
||||
tr('msg.userfront.signup.success.body'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.go('/signin'),
|
||||
child: Text(
|
||||
tr('ui.userfront.signup.success.action', fallback: '로그인하기'),
|
||||
tr('ui.userfront.signup.success.action'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -365,22 +384,22 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
children: [
|
||||
_stepCircle(
|
||||
1,
|
||||
tr('ui.userfront.signup.steps.agreement', fallback: '약관동의'),
|
||||
tr('ui.userfront.signup.steps.agreement'),
|
||||
),
|
||||
_stepLine(1),
|
||||
_stepCircle(
|
||||
2,
|
||||
tr('ui.userfront.signup.steps.verify', fallback: '본인인증'),
|
||||
tr('ui.userfront.signup.steps.verify'),
|
||||
),
|
||||
_stepLine(2),
|
||||
_stepCircle(
|
||||
3,
|
||||
tr('ui.userfront.signup.steps.profile', fallback: '정보입력'),
|
||||
tr('ui.userfront.signup.steps.profile'),
|
||||
),
|
||||
_stepLine(3),
|
||||
_stepCircle(
|
||||
4,
|
||||
tr('ui.userfront.signup.steps.password', fallback: '비밀번호'),
|
||||
tr('ui.userfront.signup.steps.password'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -394,11 +413,28 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor: isDone ? Colors.green : (isCurrent ? Colors.black : Colors.grey[300]),
|
||||
child: isDone ? const Icon(Icons.check, size: 14, color: Colors.white) : Text('$step', style: TextStyle(color: isCurrent ? Colors.white : Colors.black54, fontSize: 10)),
|
||||
backgroundColor: isDone
|
||||
? Colors.green
|
||||
: (isCurrent ? Colors.black : Colors.grey[300]),
|
||||
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),
|
||||
Text(label, style: TextStyle(fontSize: 9, 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -420,7 +456,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.agreement.title',
|
||||
fallback: '서비스 이용을 위해\n약관에 동의해주세요',
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
@@ -438,10 +473,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
child: CheckboxListTile(
|
||||
title: Text(
|
||||
tr(
|
||||
'ui.userfront.signup.agreement.all',
|
||||
fallback: '모두 동의합니다',
|
||||
),
|
||||
tr('ui.userfront.signup.agreement.all'),
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||
),
|
||||
value: _termsAccepted && _privacyAccepted,
|
||||
@@ -459,7 +491,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_agreementSection(
|
||||
title: tr(
|
||||
'ui.userfront.signup.agreement.tos_title',
|
||||
fallback: '바론 소프트웨어 이용약관 (필수)',
|
||||
),
|
||||
content: _tosText,
|
||||
value: _termsAccepted,
|
||||
@@ -469,7 +500,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_agreementSection(
|
||||
title: tr(
|
||||
'ui.userfront.signup.agreement.privacy_title',
|
||||
fallback: '개인정보 수집 및 이용 동의 (필수)',
|
||||
),
|
||||
content: _privacyText,
|
||||
value: _privacyAccepted,
|
||||
@@ -488,8 +518,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
return Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
title: Text(title,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -508,7 +540,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
content,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey, height: 1.5),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -517,8 +553,8 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
}
|
||||
|
||||
static String get _tosText => tr(
|
||||
'msg.userfront.signup.tos_full',
|
||||
fallback: """
|
||||
'msg.userfront.signup.tos_full',
|
||||
fallback: """
|
||||
바론 소프트웨어 이용약관
|
||||
|
||||
제1장 총칙
|
||||
@@ -589,11 +625,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
부칙
|
||||
본 약관은 2024년 10월 1일부터 시행됩니다.
|
||||
""",
|
||||
);
|
||||
);
|
||||
|
||||
static String get _privacyText => tr(
|
||||
'msg.userfront.signup.privacy_full',
|
||||
fallback: """
|
||||
'msg.userfront.signup.privacy_full',
|
||||
fallback: """
|
||||
개인정보 수집 및 이용 동의
|
||||
|
||||
바론서비스 개인정보처리방침
|
||||
@@ -702,7 +738,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
제8조 (기타)
|
||||
본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.
|
||||
""",
|
||||
);
|
||||
);
|
||||
|
||||
Widget _buildStepAuth() {
|
||||
return Column(
|
||||
@@ -711,7 +747,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.auth.title',
|
||||
fallback: '본인 확인을 위해\n인증을 진행해주세요',
|
||||
),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -719,7 +754,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
// 가족사 이메일 안내 문구
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(color: Colors.blue[50], borderRadius: BorderRadius.circular(6)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 16, color: Colors.blue),
|
||||
@@ -728,9 +766,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.auth.affiliate_notice',
|
||||
fallback: '가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.',
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.blue, fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -738,7 +779,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
tr('ui.userfront.signup.auth.email.title', fallback: '이메일 인증'),
|
||||
tr('ui.userfront.signup.auth.email.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -751,9 +792,8 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.email.label',
|
||||
fallback: '이메일 주소',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _emailError,
|
||||
hintText: 'example@hanmaceng.co.kr',
|
||||
),
|
||||
@@ -764,14 +804,19 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
SizedBox(
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_isEmailVerified || _isLoading) ? null : _sendEmailCode,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[100], foregroundColor: Colors.black, elevation: 0),
|
||||
onPressed: (_isEmailVerified || _isLoading)
|
||||
? null
|
||||
: _sendEmailCode,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[100],
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text(
|
||||
_emailSeconds > 0
|
||||
? tr('ui.common.resend', fallback: '재발송')
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.signup.auth.request_code',
|
||||
fallback: '인증요청',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -785,14 +830,18 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.code_label',
|
||||
fallback: '인증코드 6자리',
|
||||
),
|
||||
suffixText: _formatTime(_emailSeconds),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)],
|
||||
onChanged: (val) { if(val.length == 6) _verifyEmailCode(); },
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(6),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val.length == 6) _verifyEmailCode();
|
||||
},
|
||||
),
|
||||
],
|
||||
if (_isEmailVerified)
|
||||
@@ -801,7 +850,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.email.verified',
|
||||
fallback: '✅ 이메일 인증 완료',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
@@ -812,7 +860,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
tr('ui.userfront.signup.phone.title', fallback: '휴대폰 인증'),
|
||||
tr('ui.userfront.signup.phone.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -824,7 +872,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.phone.label',
|
||||
fallback: '휴대폰 번호 (-없이)',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _phoneError,
|
||||
@@ -837,14 +884,19 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
SizedBox(
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_isPhoneVerified || _isLoading) ? null : _sendPhoneCode,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[100], foregroundColor: Colors.black, elevation: 0),
|
||||
onPressed: (_isPhoneVerified || _isLoading)
|
||||
? null
|
||||
: _sendPhoneCode,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[100],
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text(
|
||||
_phoneSeconds > 0
|
||||
? tr('ui.common.resend', fallback: '재발송')
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.signup.auth.request_code',
|
||||
fallback: '인증요청',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -858,14 +910,18 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.code_label',
|
||||
fallback: '인증코드 6자리',
|
||||
),
|
||||
suffixText: _formatTime(_phoneSeconds),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)],
|
||||
onChanged: (val) { if(val.length == 6) _verifyPhoneCode(); },
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(6),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val.length == 6) _verifyPhoneCode();
|
||||
},
|
||||
),
|
||||
],
|
||||
if (_isPhoneVerified)
|
||||
@@ -874,7 +930,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.phone.verified',
|
||||
fallback: '✅ 휴대폰 인증 완료',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
@@ -894,7 +949,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.profile.title',
|
||||
fallback: '회원님의\n소속 정보를 알려주세요',
|
||||
),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -903,10 +957,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
controller: _nameController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.profile.name',
|
||||
fallback: '이름',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.profile.name'),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
@@ -922,13 +973,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.profile.affiliation_type',
|
||||
fallback: '소속 유형',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: _isAffiliateEmail
|
||||
? tr(
|
||||
'msg.userfront.signup.profile.affiliate_hint',
|
||||
fallback: '가족사 이메일 사용 시 자동으로 선택됩니다.',
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@@ -936,19 +985,13 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
DropdownMenuItem(
|
||||
value: 'GENERAL',
|
||||
child: Text(
|
||||
tr(
|
||||
'domain.affiliation.general',
|
||||
fallback: '일반 사용자',
|
||||
),
|
||||
tr('domain.affiliation.general'),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'AFFILIATE',
|
||||
child: Text(
|
||||
tr(
|
||||
'domain.affiliation.affiliate',
|
||||
fallback: '가족사 임직원',
|
||||
),
|
||||
tr('domain.affiliation.affiliate'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -978,46 +1021,33 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.profile.company',
|
||||
fallback: '가족사 선택',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'HANMAC',
|
||||
child: Text(
|
||||
tr('domain.company.hanmac', fallback: '한맥'),
|
||||
),
|
||||
child: Text(tr('domain.company.hanmac')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'SAMAN',
|
||||
child: Text(
|
||||
tr('domain.company.saman', fallback: '삼안'),
|
||||
),
|
||||
child: Text(tr('domain.company.saman')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'PTC',
|
||||
child: Text(
|
||||
tr('domain.company.ptc', fallback: 'PTC'),
|
||||
),
|
||||
child: Text(tr('domain.company.ptc', fallback: 'PTC')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'JANGHEON',
|
||||
child: Text(
|
||||
tr('domain.company.jangheon', fallback: '장헌'),
|
||||
),
|
||||
child: Text(tr('domain.company.jangheon')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'BARON',
|
||||
child: Text(
|
||||
tr('domain.company.baron', fallback: '바론'),
|
||||
),
|
||||
child: Text(tr('domain.company.baron')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'HALLA',
|
||||
child: Text(
|
||||
tr('domain.company.halla', fallback: '한라'),
|
||||
),
|
||||
child: Text(tr('domain.company.halla')),
|
||||
),
|
||||
],
|
||||
onChanged: _isAffiliateEmail
|
||||
@@ -1033,12 +1063,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
labelText: _affiliationType == 'AFFILIATE'
|
||||
? tr('ui.userfront.signup.profile.department', fallback: '부서명')
|
||||
? tr('ui.userfront.signup.profile.department')
|
||||
: tr(
|
||||
'ui.userfront.signup.profile.department_optional',
|
||||
fallback: '소속 정보 (선택)',
|
||||
),
|
||||
border: const OutlineInputBorder()
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1049,7 +1078,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
if (_isPolicyLoading) {
|
||||
return tr(
|
||||
'msg.userfront.signup.policy.loading',
|
||||
fallback: '비밀번호 정책을 불러오는 중입니다...',
|
||||
);
|
||||
}
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
@@ -1062,7 +1090,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
final parts = <String>[
|
||||
tr(
|
||||
'msg.userfront.signup.policy.min_length',
|
||||
fallback: '최소 {{count}}자 이상',
|
||||
params: {'count': minLength.toString()},
|
||||
),
|
||||
];
|
||||
@@ -1070,54 +1097,32 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
parts.add(
|
||||
tr(
|
||||
'msg.userfront.signup.policy.min_types',
|
||||
fallback: '영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상',
|
||||
params: {'count': minTypes.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (requiresUpper) {
|
||||
parts.add(
|
||||
tr(
|
||||
'msg.userfront.signup.policy.uppercase',
|
||||
fallback: '대문자',
|
||||
),
|
||||
);
|
||||
parts.add(tr('msg.userfront.signup.policy.uppercase'));
|
||||
}
|
||||
if (requiresLower) {
|
||||
parts.add(
|
||||
tr(
|
||||
'msg.userfront.signup.policy.lowercase',
|
||||
fallback: '소문자',
|
||||
),
|
||||
);
|
||||
parts.add(tr('msg.userfront.signup.policy.lowercase'));
|
||||
}
|
||||
if (requiresNumber) {
|
||||
parts.add(
|
||||
tr(
|
||||
'msg.userfront.signup.policy.number',
|
||||
fallback: '숫자',
|
||||
),
|
||||
);
|
||||
parts.add(tr('msg.userfront.signup.policy.number'));
|
||||
}
|
||||
if (requiresSymbol) {
|
||||
parts.add(
|
||||
tr(
|
||||
'msg.userfront.signup.policy.symbol',
|
||||
fallback: '특수문자',
|
||||
),
|
||||
);
|
||||
parts.add(tr('msg.userfront.signup.policy.symbol'));
|
||||
}
|
||||
|
||||
return tr(
|
||||
'msg.userfront.signup.policy.summary',
|
||||
fallback: '보안 정책: {{rules}}',
|
||||
params: {'rules': parts.join(', ')},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepPassword() {
|
||||
String p = _passwordController.text;
|
||||
|
||||
|
||||
// Default Policy Fallback
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||
@@ -1144,7 +1149,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.password.title',
|
||||
fallback: '마지막으로\n비밀번호를 설정해주세요',
|
||||
),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -1152,7 +1156,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
// 비밀번호 정책 안내 박스
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: Colors.blue[50], borderRadius: BorderRadius.circular(8)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.security, size: 18, color: Colors.blue),
|
||||
@@ -1160,7 +1167,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
_buildPolicyDescription(),
|
||||
style: TextStyle(fontSize: 12, color: Colors.blue[800], fontWeight: FontWeight.w500),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue[800],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1174,7 +1185,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.password.label',
|
||||
fallback: '비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _passwordError,
|
||||
@@ -1187,7 +1197,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.min_length',
|
||||
fallback: '{{count}}자 이상',
|
||||
params: {'count': minLength.toString()},
|
||||
),
|
||||
hasLength,
|
||||
@@ -1196,7 +1205,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.min_types',
|
||||
fallback: '문자 유형 {{count}}가지 이상',
|
||||
params: {'count': minTypes.toString()},
|
||||
),
|
||||
hasTypeCount,
|
||||
@@ -1205,7 +1213,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.uppercase',
|
||||
fallback: '대문자',
|
||||
),
|
||||
hasUpper,
|
||||
),
|
||||
@@ -1213,23 +1220,18 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.lowercase',
|
||||
fallback: '소문자',
|
||||
),
|
||||
hasLower,
|
||||
),
|
||||
if (requiresNumber)
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.number',
|
||||
fallback: '숫자',
|
||||
),
|
||||
tr('msg.userfront.signup.password.rule.number'),
|
||||
hasDigit,
|
||||
),
|
||||
if (requiresSymbol)
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.symbol',
|
||||
fallback: '특수문자',
|
||||
),
|
||||
hasSpecial,
|
||||
),
|
||||
@@ -1244,7 +1246,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_confirmPasswordError = (val != _passwordController.text)
|
||||
? tr(
|
||||
'msg.userfront.signup.password.mismatch',
|
||||
fallback: '비밀번호가 일치하지 않습니다.',
|
||||
)
|
||||
: null;
|
||||
});
|
||||
@@ -1252,8 +1253,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.password.confirm_label',
|
||||
fallback: '비밀번호 확인',
|
||||
),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _confirmPasswordError,
|
||||
),
|
||||
@@ -1266,9 +1266,19 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(isValid ? Icons.check_circle : Icons.circle_outlined, size: 14, color: isValid ? Colors.green : Colors.grey),
|
||||
Icon(
|
||||
isValid ? Icons.check_circle : Icons.circle_outlined,
|
||||
size: 14,
|
||||
color: isValid ? Colors.green : Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: TextStyle(fontSize: 11, color: isValid ? Colors.green : Colors.grey)),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isValid ? Colors.green : Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1276,8 +1286,10 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool canGoNext = false;
|
||||
if (_currentStep == 1 && _termsAccepted && _privacyAccepted) canGoNext = true;
|
||||
if (_currentStep == 2 && _isEmailVerified && _isPhoneVerified) canGoNext = true;
|
||||
if (_currentStep == 1 && _termsAccepted && _privacyAccepted)
|
||||
canGoNext = true;
|
||||
if (_currentStep == 2 && _isEmailVerified && _isPhoneVerified)
|
||||
canGoNext = true;
|
||||
if (_currentStep == 3) {
|
||||
final nameOk = _nameController.text.trim().isNotEmpty;
|
||||
if (_affiliationType == 'GENERAL') {
|
||||
@@ -1294,7 +1306,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr('ui.userfront.signup.title', fallback: '회원가입'),
|
||||
tr('ui.userfront.signup.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
@@ -1313,11 +1325,13 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: _currentStep == 1
|
||||
? _buildStepAgreement()
|
||||
: (_currentStep == 2
|
||||
? _buildStepAuth()
|
||||
: (_currentStep == 3 ? _buildStepInfo() : _buildStepPassword())),
|
||||
child: _currentStep == 1
|
||||
? _buildStepAgreement()
|
||||
: (_currentStep == 2
|
||||
? _buildStepAuth()
|
||||
: (_currentStep == 3
|
||||
? _buildStepInfo()
|
||||
: _buildStepPassword())),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1329,9 +1343,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => setState(() => _currentStep--),
|
||||
style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(55), side: const BorderSide(color: Colors.black)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(55),
|
||||
side: const BorderSide(color: Colors.black),
|
||||
),
|
||||
child: Text(
|
||||
tr('ui.common.prev', fallback: '이전'),
|
||||
tr('ui.common.prev'),
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
),
|
||||
@@ -1340,20 +1357,33 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
],
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: _currentStep < 4
|
||||
? (canGoNext ? () => setState(() => _currentStep++) : null)
|
||||
: (_isLoading ? null : _handleSignup),
|
||||
onPressed: _currentStep < 4
|
||||
? (canGoNext
|
||||
? () => setState(() => _currentStep++)
|
||||
: null)
|
||||
: (_isLoading ? null : _handleSignup),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(55),
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: Text(
|
||||
_currentStep < 4
|
||||
? tr('ui.userfront.signup.next_step', fallback: '다음 단계')
|
||||
: tr('ui.userfront.signup.complete', fallback: '가입 완료'),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_currentStep < 4
|
||||
? tr(
|
||||
'ui.userfront.signup.next_step',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.signup.complete',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user