1
0
forked from baron/baron-sso

i18n 대대적 변경

This commit is contained in:
Lectom C Han
2026-02-10 19:13:00 +09:00
parent 798d37bed9
commit 8df95c8a13
27 changed files with 3910 additions and 594 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:userfront/i18n.dart';
import '../../../core/services/auth_proxy_service.dart';
class SignupScreen extends StatefulWidget {
@@ -130,8 +131,11 @@ class _SignupScreenState extends State<SignupScreen> {
_emailTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) return;
setState(() {
if (_emailSeconds > 0) _emailSeconds--;
else timer.cancel();
if (_emailSeconds > 0) {
_emailSeconds--;
} else {
timer.cancel();
}
});
});
} else {
@@ -140,8 +144,11 @@ class _SignupScreenState extends State<SignupScreen> {
_phoneTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) return;
setState(() {
if (_phoneSeconds > 0) _phoneSeconds--;
else timer.cancel();
if (_phoneSeconds > 0) {
_phoneSeconds--;
} else {
timer.cancel();
}
});
});
}
@@ -157,20 +164,30 @@ 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 = '유효한 이메일 형식이 아닙니다.');
setState(() => _emailError = tr(
'msg.userfront.signup.email.invalid',
fallback: '유효한 이메일 형식이 아닙니다.',
));
return;
}
setState(() { _isLoading = true; _emailError = null; });
try {
final available = await AuthProxyService.checkEmailAvailability(email);
if (!available) {
setState(() => _emailError = '이미 가입된 이메일입니다.');
setState(() => _emailError = tr(
'msg.userfront.signup.email.duplicate',
fallback: '이미 가입된 이메일입니다.',
));
return;
}
await AuthProxyService.sendSignupCode(email, 'email');
_startTimer('email');
} catch (e) {
setState(() => _emailError = '발송 실패: $e');
setState(() => _emailError = tr(
'msg.userfront.signup.email.send_failed',
fallback: '발송 실패: {{error}}',
params: {'error': e.toString()},
));
} finally {
setState(() => _isLoading = false);
}
@@ -189,10 +206,17 @@ class _SignupScreenState extends State<SignupScreen> {
_emailError = null;
});
} else {
setState(() => _emailError = '인증코드가 일치하지 않습니다.');
setState(() => _emailError = tr(
'msg.userfront.signup.email.code_mismatch',
fallback: '인증코드가 일치하지 않습니다.',
));
}
} catch (e) {
setState(() => _emailError = '인증 실패: $e');
setState(() => _emailError = tr(
'msg.userfront.signup.email.verify_failed',
fallback: '인증 실패: {{error}}',
params: {'error': e.toString()},
));
}
}
@@ -204,7 +228,11 @@ class _SignupScreenState extends State<SignupScreen> {
await AuthProxyService.sendSignupCode(phone, 'phone');
_startTimer('phone');
} catch (e) {
setState(() => _phoneError = '발송 실패: $e');
setState(() => _phoneError = tr(
'msg.userfront.signup.phone.send_failed',
fallback: '발송 실패: {{error}}',
params: {'error': e.toString()},
));
} finally {
setState(() => _isLoading = false);
}
@@ -223,16 +251,26 @@ class _SignupScreenState extends State<SignupScreen> {
_phoneError = null;
});
} else {
setState(() => _phoneError = '인증코드가 일치하지 않습니다.');
setState(() => _phoneError = tr(
'msg.userfront.signup.phone.code_mismatch',
fallback: '인증코드가 일치하지 않습니다.',
));
}
} catch (e) {
setState(() => _phoneError = '인증 실패: $e');
setState(() => _phoneError = tr(
'msg.userfront.signup.phone.verify_failed',
fallback: '인증 실패: {{error}}',
params: {'error': e.toString()},
));
}
}
Future<void> _handleSignup() async {
if (_passwordController.text != _confirmPasswordController.text) {
setState(() => _confirmPasswordError = '비밀번호가 일치하지 않습니다.');
setState(() => _confirmPasswordError = tr(
'msg.userfront.signup.password.mismatch',
fallback: '비밀번호가 일치하지 않습니다.',
));
return;
}
if (!_formKey.currentState!.validate()) return;
@@ -257,12 +295,38 @@ class _SignupScreenState extends State<SignupScreen> {
} catch (e) {
String eStr = e.toString().toLowerCase();
setState(() {
if (eStr.contains('uppercase')) _passwordError = '대문자가 최소 1개 이상 포함되어야 합니다.';
else if (eStr.contains('lowercase')) _passwordError = '소문자가 최소 1개 이상 포함되어야 합니다.';
else if (eStr.contains('digit') || eStr.contains('number')) _passwordError = '숫자가 최소 1개 이상 포함되어야 합니다.';
else if (eStr.contains('symbol') || eStr.contains('special')) _passwordError = '특수문자가 최소 1개 이상 포함되어야 합니다.';
else if (eStr.contains('length') || eStr.contains('12 characters')) _passwordError = '비밀번호는 최소 12자 이상이어야 합니다.';
else _passwordError = '가입 실패: $e';
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()},
);
}
});
} finally {
setState(() => _isLoading = false);
@@ -274,9 +338,20 @@ class _SignupScreenState extends State<SignupScreen> {
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('회원가입 완료'),
content: const Text('성공적으로 가입되었습니다.'),
actions: [TextButton(onPressed: () => context.go('/signin'), child: const Text('로그인하기'))],
title: Text(
tr('msg.userfront.signup.success.title', fallback: '회원가입 완료'),
),
content: Text(
tr('msg.userfront.signup.success.body', fallback: '성공적으로 가입되었습니다.'),
),
actions: [
TextButton(
onPressed: () => context.go('/signin'),
child: Text(
tr('ui.userfront.signup.success.action', fallback: '로그인하기'),
),
),
],
),
);
}
@@ -288,13 +363,25 @@ class _SignupScreenState extends State<SignupScreen> {
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
children: [
_stepCircle(1, '약관동의'),
_stepCircle(
1,
tr('ui.userfront.signup.steps.agreement', fallback: '약관동의'),
),
_stepLine(1),
_stepCircle(2, '본인인증'),
_stepCircle(
2,
tr('ui.userfront.signup.steps.verify', fallback: '본인인증'),
),
_stepLine(2),
_stepCircle(3, '정보입력'),
_stepCircle(
3,
tr('ui.userfront.signup.steps.profile', fallback: '정보입력'),
),
_stepLine(3),
_stepCircle(4, '비밀번호'),
_stepCircle(
4,
tr('ui.userfront.signup.steps.password', fallback: '비밀번호'),
),
],
),
);
@@ -330,9 +417,17 @@ class _SignupScreenState extends State<SignupScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('서비스 이용을 위해\n약관에 동의해주세요',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold, height: 1.3)),
Text(
tr(
'msg.userfront.signup.agreement.title',
fallback: '서비스 이용을 위해\n약관에 동의해주세요',
),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.3,
),
),
const SizedBox(height: 24),
// 모두 동의 버튼
Container(
@@ -342,8 +437,13 @@ class _SignupScreenState extends State<SignupScreen> {
border: Border.all(color: Colors.grey[200]!),
),
child: CheckboxListTile(
title: const Text('모두 동의합니다',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
title: Text(
tr(
'ui.userfront.signup.agreement.all',
fallback: '모두 동의합니다',
),
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
value: _termsAccepted && _privacyAccepted,
onChanged: (val) {
setState(() {
@@ -357,14 +457,20 @@ class _SignupScreenState extends State<SignupScreen> {
),
const SizedBox(height: 16),
_agreementSection(
title: '바론 소프트웨어 이용약관 (필수)',
title: tr(
'ui.userfront.signup.agreement.tos_title',
fallback: '바론 소프트웨어 이용약관 (필수)',
),
content: _tosText,
value: _termsAccepted,
onChanged: (val) => setState(() => _termsAccepted = val!),
),
const SizedBox(height: 16),
_agreementSection(
title: '개인정보 수집 및 이용 동의 (필수)',
title: tr(
'ui.userfront.signup.agreement.privacy_title',
fallback: '개인정보 수집 및 이용 동의 (필수)',
),
content: _privacyText,
value: _privacyAccepted,
onChanged: (val) => setState(() => _privacyAccepted = val!),
@@ -410,7 +516,9 @@ class _SignupScreenState extends State<SignupScreen> {
);
}
static const String _tosText = """
static String get _tosText => tr(
'msg.userfront.signup.tos_full',
fallback: """
바론 소프트웨어 이용약관
제1장 총칙
@@ -480,9 +588,12 @@ class _SignupScreenState extends State<SignupScreen> {
본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.
부칙
본 약관은 2024년 10월 1일부터 시행됩니다.
""";
""",
);
static const String _privacyText = """
static String get _privacyText => tr(
'msg.userfront.signup.privacy_full',
fallback: """
개인정보 수집 및 이용 동의
바론서비스 개인정보처리방침
@@ -590,33 +701,46 @@ class _SignupScreenState extends State<SignupScreen> {
회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.
제8조 (기타)
본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.
""";
""",
);
Widget _buildStepAuth() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('본인 확인을 위해\n인증을 진행해주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Text(
tr(
'msg.userfront.signup.auth.title',
fallback: '본인 확인을 위해\n인증을 진행해주세요',
),
style: const TextStyle(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(
child: Row(
children: [
Icon(Icons.info_outline, size: 16, color: Colors.blue),
SizedBox(width: 8),
const Icon(Icons.info_outline, size: 16, color: Colors.blue),
const SizedBox(width: 8),
Expanded(
child: Text(
'가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.',
style: TextStyle(fontSize: 12, color: Colors.blue, fontWeight: FontWeight.w500),
tr(
'msg.userfront.signup.auth.affiliate_notice',
fallback: '가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.',
),
style: const TextStyle(fontSize: 12, color: Colors.blue, fontWeight: FontWeight.w500),
),
),
],
),
),
const SizedBox(height: 24),
Text('이메일 인증', style: const TextStyle(fontWeight: FontWeight.bold)),
Text(
tr('ui.userfront.signup.auth.email.title', fallback: '이메일 인증'),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
children: [
@@ -625,7 +749,10 @@ class _SignupScreenState extends State<SignupScreen> {
controller: _emailController,
onChanged: _checkEmailAffiliation, // 도메인 실시간 체크
decoration: InputDecoration(
labelText: '이메일 주소',
labelText: tr(
'ui.userfront.signup.auth.email.label',
fallback: '이메일 주소',
),
border: const OutlineInputBorder(),
errorText: _emailError,
hintText: 'example@hanmaceng.co.kr',
@@ -639,7 +766,14 @@ class _SignupScreenState extends State<SignupScreen> {
child: ElevatedButton(
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
? tr('ui.common.resend', fallback: '재발송')
: tr(
'ui.userfront.signup.auth.request_code',
fallback: '인증요청',
),
),
),
),
],
@@ -649,7 +783,10 @@ class _SignupScreenState extends State<SignupScreen> {
TextFormField(
controller: _emailCodeController,
decoration: InputDecoration(
labelText: '인증코드 6자리',
labelText: tr(
'ui.userfront.signup.auth.code_label',
fallback: '인증코드 6자리',
),
suffixText: _formatTime(_emailSeconds),
border: const OutlineInputBorder(),
),
@@ -658,19 +795,40 @@ class _SignupScreenState extends State<SignupScreen> {
onChanged: (val) { if(val.length == 6) _verifyEmailCode(); },
),
],
if (_isEmailVerified) const Padding(
padding: EdgeInsets.only(top: 8),
child: Text('✅ 이메일 인증 완료', style: TextStyle(color: Colors.green, fontSize: 13, fontWeight: FontWeight.bold)),
),
if (_isEmailVerified)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
tr(
'msg.userfront.signup.email.verified',
fallback: '✅ 이메일 인증 완료',
),
style: const TextStyle(
color: Colors.green,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 24),
Text('휴대폰 인증', style: const TextStyle(fontWeight: FontWeight.bold)),
Text(
tr('ui.userfront.signup.phone.title', fallback: '휴대폰 인증'),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextFormField(
controller: _phoneController,
decoration: InputDecoration(labelText: '휴대폰 번호 (-없이)', border: const OutlineInputBorder(), errorText: _phoneError),
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.phone.label',
fallback: '휴대폰 번호 (-없이)',
),
border: const OutlineInputBorder(),
errorText: _phoneError,
),
readOnly: _isPhoneVerified,
keyboardType: TextInputType.phone,
),
@@ -681,7 +839,14 @@ class _SignupScreenState extends State<SignupScreen> {
child: ElevatedButton(
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
? tr('ui.common.resend', fallback: '재발송')
: tr(
'ui.userfront.signup.auth.request_code',
fallback: '인증요청',
),
),
),
),
],
@@ -691,7 +856,10 @@ class _SignupScreenState extends State<SignupScreen> {
TextFormField(
controller: _phoneCodeController,
decoration: InputDecoration(
labelText: '인증코드 6자리',
labelText: tr(
'ui.userfront.signup.auth.code_label',
fallback: '인증코드 6자리',
),
suffixText: _formatTime(_phoneSeconds),
border: const OutlineInputBorder(),
),
@@ -700,10 +868,21 @@ class _SignupScreenState extends State<SignupScreen> {
onChanged: (val) { if(val.length == 6) _verifyPhoneCode(); },
),
],
if (_isPhoneVerified) const Padding(
padding: EdgeInsets.only(top: 8),
child: Text('✅ 휴대폰 인증 완료', style: TextStyle(color: Colors.green, fontSize: 13, fontWeight: FontWeight.bold)),
),
if (_isPhoneVerified)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
tr(
'msg.userfront.signup.phone.verified',
fallback: '✅ 휴대폰 인증 완료',
),
style: const TextStyle(
color: Colors.green,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
@@ -712,12 +891,24 @@ class _SignupScreenState extends State<SignupScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('회원님의\n소속 정보를 알려주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Text(
tr(
'msg.userfront.signup.profile.title',
fallback: '회원님의\n소속 정보를 알려주세요',
),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 24),
TextFormField(
controller: _nameController,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(labelText: '이름', border: OutlineInputBorder()),
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.profile.name',
fallback: '이름',
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 소속 유형 선택 (가족사 메일일 경우 비활성화)
@@ -726,17 +917,51 @@ class _SignupScreenState extends State<SignupScreen> {
child: Opacity(
opacity: _isAffiliateEmail ? 0.7 : 1.0,
child: DropdownButtonFormField<String>(
value: _affiliationType,
key: ValueKey(_affiliationType),
initialValue: _affiliationType,
decoration: InputDecoration(
labelText: '소속 유형',
labelText: tr(
'ui.userfront.signup.profile.affiliation_type',
fallback: '소속 유형',
),
border: const OutlineInputBorder(),
helperText: _isAffiliateEmail ? '가족사 이메일 사용 시 자동으로 선택됩니다.' : null,
helperText: _isAffiliateEmail
? tr(
'msg.userfront.signup.profile.affiliate_hint',
fallback: '가족사 이메일 사용 시 자동으로 선택됩니다.',
)
: null,
),
items: const [
DropdownMenuItem(value: 'GENERAL', child: Text('일반 사용자')),
DropdownMenuItem(value: 'AFFILIATE', child: Text('가족사 임직원')),
items: [
DropdownMenuItem(
value: 'GENERAL',
child: Text(
tr(
'domain.affiliation.general',
fallback: '일반 사용자',
),
),
),
DropdownMenuItem(
value: 'AFFILIATE',
child: Text(
tr(
'domain.affiliation.affiliate',
fallback: '가족사 임직원',
),
),
),
],
onChanged: _isAffiliateEmail ? null : (val) => setState(() { _affiliationType = val!; }),
onChanged: _isAffiliateEmail
? null
: (val) {
if (val == null) {
return;
}
setState(() {
_affiliationType = val;
});
},
),
),
),
@@ -748,17 +973,56 @@ class _SignupScreenState extends State<SignupScreen> {
child: Opacity(
opacity: _isAffiliateEmail ? 0.7 : 1.0,
child: DropdownButtonFormField<String>(
value: _companyCode,
decoration: const InputDecoration(labelText: '가족사 선택', border: OutlineInputBorder()),
items: const [
DropdownMenuItem(value: 'HANMAC', child: Text('한맥')),
DropdownMenuItem(value: 'SAMAN', child: Text('삼안')),
DropdownMenuItem(value: 'PTC', child: Text('PTC')),
DropdownMenuItem(value: 'JANGHEON', child: Text('장헌')),
DropdownMenuItem(value: 'BARON', child: Text('바론')),
DropdownMenuItem(value: 'HALLA', child: Text('한라')),
key: ValueKey(_companyCode ?? 'none'),
initialValue: _companyCode,
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.profile.company',
fallback: '가족사 선택',
),
border: const OutlineInputBorder(),
),
items: [
DropdownMenuItem(
value: 'HANMAC',
child: Text(
tr('domain.company.hanmac', fallback: '한맥'),
),
),
DropdownMenuItem(
value: 'SAMAN',
child: Text(
tr('domain.company.saman', fallback: '삼안'),
),
),
DropdownMenuItem(
value: 'PTC',
child: Text(
tr('domain.company.ptc', fallback: 'PTC'),
),
),
DropdownMenuItem(
value: 'JANGHEON',
child: Text(
tr('domain.company.jangheon', fallback: '장헌'),
),
),
DropdownMenuItem(
value: 'BARON',
child: Text(
tr('domain.company.baron', fallback: '바론'),
),
),
DropdownMenuItem(
value: 'HALLA',
child: Text(
tr('domain.company.halla', fallback: '한라'),
),
),
],
onChanged: _isAffiliateEmail ? null : (val) => setState(() => _companyCode = val),
onChanged: _isAffiliateEmail
? null
: (val) => setState(() => _companyCode = val),
),
),
),
@@ -768,7 +1032,12 @@ class _SignupScreenState extends State<SignupScreen> {
controller: _deptController,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
labelText: _affiliationType == 'AFFILIATE' ? '부서명' : '소속 정보 (선택)',
labelText: _affiliationType == 'AFFILIATE'
? tr('ui.userfront.signup.profile.department', fallback: '부서명')
: tr(
'ui.userfront.signup.profile.department_optional',
fallback: '소속 정보 (선택)',
),
border: const OutlineInputBorder()
),
),
@@ -778,7 +1047,10 @@ class _SignupScreenState extends State<SignupScreen> {
String _buildPolicyDescription() {
if (_isPolicyLoading) {
return "비밀번호 정책을 불러오는 중입니다...";
return tr(
'msg.userfront.signup.policy.loading',
fallback: '비밀번호 정책을 불러오는 중입니다...',
);
}
final minLength = (_policy?['minLength'] as int?) ?? 12;
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
@@ -787,16 +1059,60 @@ class _SignupScreenState extends State<SignupScreen> {
final requiresNumber = _policy?['number'] ?? true;
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
final parts = <String>["최소 $minLength자 이상"];
final parts = <String>[
tr(
'msg.userfront.signup.policy.min_length',
fallback: '최소 {{count}}자 이상',
params: {'count': minLength.toString()},
),
];
if (minTypes > 0) {
parts.add("영문 대/소문자/숫자/특수문자 중 ${minTypes}가지 이상");
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: '대문자',
),
);
}
if (requiresLower) {
parts.add(
tr(
'msg.userfront.signup.policy.lowercase',
fallback: '소문자',
),
);
}
if (requiresNumber) {
parts.add(
tr(
'msg.userfront.signup.policy.number',
fallback: '숫자',
),
);
}
if (requiresSymbol) {
parts.add(
tr(
'msg.userfront.signup.policy.symbol',
fallback: '특수문자',
),
);
}
if (requiresUpper) parts.add("대문자");
if (requiresLower) parts.add("소문자");
if (requiresNumber) parts.add("숫자");
if (requiresSymbol) parts.add("특수문자");
return "보안 정책: ${parts.join(', ')}";
return tr(
'msg.userfront.signup.policy.summary',
fallback: '보안 정책: {{rules}}',
params: {'rules': parts.join(', ')},
);
}
Widget _buildStepPassword() {
@@ -825,7 +1141,13 @@ class _SignupScreenState extends State<SignupScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('마지막으로\n비밀번호를 설정해주세요', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Text(
tr(
'msg.userfront.signup.password.title',
fallback: '마지막으로\n비밀번호를 설정해주세요',
),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// 비밀번호 정책 안내 박스
Container(
@@ -850,7 +1172,10 @@ class _SignupScreenState extends State<SignupScreen> {
obscureText: true,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
labelText: '비밀번호',
labelText: tr(
'ui.userfront.signup.password.label',
fallback: '비밀번호',
),
border: const OutlineInputBorder(),
errorText: _passwordError,
),
@@ -859,12 +1184,55 @@ class _SignupScreenState extends State<SignupScreen> {
Wrap(
spacing: 10,
children: [
_cryptoCheck('$minLength자 이상', hasLength),
if (minTypes > 0) _cryptoCheck('문자 유형 ${minTypes}가지 이상', hasTypeCount),
if (requiresUpper) _cryptoCheck('대문자', hasUpper),
if (requiresLower) _cryptoCheck('소문자', hasLower),
if (requiresNumber) _cryptoCheck('숫자', hasDigit),
if (requiresSymbol) _cryptoCheck('특수문자', hasSpecial),
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.min_length',
fallback: '{{count}}자 이상',
params: {'count': minLength.toString()},
),
hasLength,
),
if (minTypes > 0)
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.min_types',
fallback: '문자 유형 {{count}}가지 이상',
params: {'count': minTypes.toString()},
),
hasTypeCount,
),
if (requiresUpper)
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.uppercase',
fallback: '대문자',
),
hasUpper,
),
if (requiresLower)
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.lowercase',
fallback: '소문자',
),
hasLower,
),
if (requiresNumber)
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.number',
fallback: '숫자',
),
hasDigit,
),
if (requiresSymbol)
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.symbol',
fallback: '특수문자',
),
hasSpecial,
),
],
),
const SizedBox(height: 16),
@@ -873,11 +1241,19 @@ class _SignupScreenState extends State<SignupScreen> {
obscureText: true,
onChanged: (val) {
setState(() {
_confirmPasswordError = (val != _passwordController.text) ? '비밀번호가 일치하지 않습니다.' : null;
_confirmPasswordError = (val != _passwordController.text)
? tr(
'msg.userfront.signup.password.mismatch',
fallback: '비밀번호가 일치하지 않습니다.',
)
: null;
});
},
decoration: InputDecoration(
labelText: '비밀번호 확인',
labelText: tr(
'ui.userfront.signup.password.confirm_label',
fallback: '비밀번호 확인',
),
border: const OutlineInputBorder(),
errorText: _confirmPasswordError,
),
@@ -917,7 +1293,10 @@ class _SignupScreenState extends State<SignupScreen> {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('회원가입', style: TextStyle(fontWeight: FontWeight.bold)),
title: Text(
tr('ui.userfront.signup.title', fallback: '회원가입'),
style: const TextStyle(fontWeight: FontWeight.bold),
),
elevation: 0,
backgroundColor: Colors.white,
foregroundColor: Colors.black,
@@ -951,7 +1330,10 @@ class _SignupScreenState extends State<SignupScreen> {
child: OutlinedButton(
onPressed: () => setState(() => _currentStep--),
style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(55), side: const BorderSide(color: Colors.black)),
child: const Text('이전', style: TextStyle(color: Colors.black)),
child: Text(
tr('ui.common.prev', fallback: '이전'),
style: const TextStyle(color: Colors.black),
),
),
),
const SizedBox(width: 12),
@@ -967,7 +1349,11 @@ class _SignupScreenState extends State<SignupScreen> {
),
child: _isLoading
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text(_currentStep < 4 ? '다음 단계' : '가입 완료'),
: Text(
_currentStep < 4
? tr('ui.userfront.signup.next_step', fallback: '다음 단계')
: tr('ui.userfront.signup.complete', fallback: '가입 완료'),
),
),
),
],