1
0
forked from baron/baron-sso

feat: i18n 개선 및 userfront 로그인/로케일 보완

This commit is contained in:
Lectom C Han
2026-02-12 21:25:26 +09:00
parent b6d3b69cda
commit dfa2fc2406
60 changed files with 5724 additions and 1734 deletions

View File

@@ -30,32 +30,33 @@ class ErrorScreen extends StatelessWidget {
? (isWhitelisted && hasCode ? normalizedCode : 'unknown_error')
: (hasCode ? normalizedCode : 'unknown_error');
final title = isProd
? tr('msg.userfront.error.title', fallback: '인증 과정에서 오류가 발생했습니다')
? tr('msg.userfront.error.title')
: (hasCode
? tr(
'msg.userfront.error.title_with_code',
fallback: '오류: {{code}}',
params: {'code': normalizedCode},
)
: tr('msg.userfront.error.title_generic', fallback: '오류가 발생했습니다'));
? tr(
'msg.userfront.error.title_with_code',
params: {'code': normalizedCode},
)
: tr(
'msg.userfront.error.title_generic',
));
final detail = isProd
? (isWhitelisted
? tr(
'msg.userfront.error.whitelist.$normalizedCode',
fallback: whitelistFallback,
)
: tr(
'msg.userfront.error.detail_contact',
fallback: '에러가 계속되면 관리자에게 문의해주세요',
))
? tr(
'msg.userfront.error.whitelist.$normalizedCode',
fallback: whitelistFallback,
)
: tr(
'msg.userfront.error.detail_contact',
))
: ((description?.isNotEmpty == true)
? description!
: (hasCode
? tr('msg.userfront.error.detail_generic', fallback: '오류가 발생했습니다.')
: tr(
'msg.userfront.error.detail_request',
fallback: '요청을 처리하는 중 문제가 발생했습니다.',
)));
? description!
: (hasCode
? tr(
'msg.userfront.error.detail_generic',
)
: tr(
'msg.userfront.error.detail_request',
)));
return Scaffold(
backgroundColor: const Color(0xFFF7F8FA),
@@ -94,7 +95,6 @@ class ErrorScreen extends StatelessWidget {
Text(
tr(
'msg.userfront.error.type',
fallback: '오류 종류: {{type}}',
params: {'type': errorType},
),
style: theme.textTheme.bodySmall?.copyWith(
@@ -106,7 +106,6 @@ class ErrorScreen extends StatelessWidget {
Text(
tr(
'msg.userfront.error.id',
fallback: '오류 ID: {{id}}',
params: {'id': errorId!},
),
style: theme.textTheme.bodySmall?.copyWith(
@@ -124,27 +123,35 @@ class ErrorScreen extends StatelessWidget {
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF111827),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
tr('ui.userfront.error.go_login', fallback: '로그인으로 이동'),
tr(
'ui.userfront.error.go_login',
),
),
),
OutlinedButton(
onPressed: () => context.go('/'),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF111827),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
side: const BorderSide(color: Color(0xFFCBD5F5)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
tr('ui.userfront.error.go_home', fallback: '홈으로 이동'),
tr('ui.userfront.error.go_home'),
),
),
],

View File

@@ -17,7 +17,9 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
@override
void initState() {
super.initState();
_drySendEnabled = _parseBoolParam(Uri.base.queryParameters['drySend']) && !AuthProxyService.isProdEnv;
_drySendEnabled =
_parseBoolParam(Uri.base.queryParameters['drySend']) &&
!AuthProxyService.isProdEnv;
}
Future<void> _handlePasswordReset() async {
@@ -26,7 +28,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
_showError(
tr(
'msg.userfront.forgot.input_required',
fallback: '이메일 또는 휴대폰 번호를 입력해주세요.',
),
);
return;
@@ -44,14 +45,16 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
setState(() => _isLoading = true);
try {
await AuthProxyService.initiatePasswordReset(loginId, drySend: _drySendEnabled);
await AuthProxyService.initiatePasswordReset(
loginId,
drySend: _drySendEnabled,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
tr(
'msg.userfront.forgot.sent',
fallback: '비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요.',
),
),
backgroundColor: Colors.green,
@@ -64,7 +67,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
_showError(
tr(
'msg.userfront.forgot.error',
fallback: '전송에 실패했습니다: {{error}}',
params: {'error': e.toString()},
),
);
@@ -94,7 +96,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(tr('ui.userfront.forgot.title', fallback: '비밀번호 재설정')),
title: Text(tr('ui.userfront.forgot.title')),
centerTitle: true,
),
body: Center(
@@ -106,17 +108,17 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
tr('ui.userfront.forgot.heading', fallback: '비밀번호를 잊으셨나요?'),
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
tr('ui.userfront.forgot.heading'),
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
if (_drySendEnabled) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
color: const Color(0xFFFFF3CD),
borderRadius: BorderRadius.circular(8),
@@ -124,15 +126,20 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
),
child: Row(
children: [
const Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
const Icon(
Icons.warning_amber_rounded,
color: Color(0xFF8A6D3B),
),
const SizedBox(width: 8),
Expanded(
child: Text(
tr(
'msg.userfront.forgot.dry_send',
fallback: 'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
),
style: const TextStyle(color: Color(0xFF8A6D3B), fontSize: 12),
style: const TextStyle(
color: Color(0xFF8A6D3B),
fontSize: 12,
),
),
),
],
@@ -143,8 +150,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
Text(
tr(
'msg.userfront.forgot.description',
fallback:
'계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.',
),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey),
@@ -155,7 +160,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.forgot.input_label',
fallback: '이메일 또는 휴대폰 번호',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.person_outline),
@@ -172,13 +176,13 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
tr(
'ui.userfront.forgot.submit',
fallback: '재설정 링크 전송',
),
tr('ui.userfront.forgot.submit'),
),
),
],

File diff suppressed because it is too large Load Diff

View File

@@ -14,23 +14,26 @@ class LoginSuccessScreen extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle_outline, size: 80, color: Colors.green),
const Icon(
Icons.check_circle_outline,
size: 80,
color: Colors.green,
),
const SizedBox(height: 24),
Text(
tr('ui.userfront.login_success.title', fallback: '로그인 완료'),
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
tr('ui.userfront.login_success.title'),
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
tr('msg.userfront.login_success.subtitle', fallback: '성공적으로 로그인되었습니다.'),
tr(
'msg.userfront.login_success.subtitle',
),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey, fontSize: 16),
),
const SizedBox(height: 48),
// 이 버튼이 QR 카메라를 켜는 버튼입니다.
FilledButton.icon(
onPressed: () {
@@ -38,12 +41,17 @@ class LoginSuccessScreen extends StatelessWidget {
},
icon: const Icon(Icons.camera_alt, size: 28),
label: Text(
tr('ui.userfront.login_success.qr', fallback: 'QR 인증 (카메라 켜기)'),
tr(
'ui.userfront.login_success.qr',
),
),
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(80), // 버튼 높이를 더 크게
backgroundColor: Colors.blue.shade700,
textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
textStyle: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
@@ -57,7 +65,6 @@ class LoginSuccessScreen extends StatelessWidget {
child: Text(
tr(
'ui.userfront.login_success.later',
fallback: '나중에 하기 (대시보드로 이동)',
),
style: const TextStyle(color: Colors.grey),
),

View File

@@ -87,7 +87,7 @@ class _QRScanScreenState extends State<QRScanScreen> {
Future<void> _onDetect(BarcodeCapture capture) async {
if (_isScanned) return;
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
if (barcode.rawValue != null) {
@@ -119,7 +119,7 @@ class _QRScanScreenState extends State<QRScanScreen> {
_log.info('QR Code detected raw: $qrData, ref: $pendingRef');
final approveRef = qrData;
final storedToken = AuthTokenStore.getToken();
final sessionToken = storedToken;
var usesCookie = AuthTokenStore.usesCookie();
@@ -140,13 +140,12 @@ class _QRScanScreenState extends State<QRScanScreen> {
token: sessionToken,
withCredentials: usesCookie,
);
if (mounted) {
setState(() {
_isSuccess = true;
_resultMessage = tr(
'msg.userfront.qr.approve_success',
fallback: 'QR 승인 완료! PC 화면에서 로그인이 진행됩니다.',
);
_isProcessing = false;
});
@@ -158,7 +157,6 @@ class _QRScanScreenState extends State<QRScanScreen> {
_isSuccess = false;
_resultMessage = tr(
'msg.userfront.qr.approve_error',
fallback: 'QR 승인 실패: {{error}}',
params: {'error': '$e'},
);
_isProcessing = false;
@@ -193,7 +191,6 @@ class _QRScanScreenState extends State<QRScanScreen> {
content: Text(
tr(
'msg.userfront.qr.permission_error',
fallback: '카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.',
),
),
backgroundColor: Colors.red,
@@ -212,8 +209,8 @@ class _QRScanScreenState extends State<QRScanScreen> {
final icon = success ? Icons.check_circle_outline : Icons.error_outline;
final color = success ? Colors.green : Colors.red;
final title = success
? tr('ui.userfront.qr.result_success', fallback: '승인 완료')
: tr('ui.userfront.qr.result_failure', fallback: '승인 실패');
? tr('ui.userfront.qr.result_success')
: tr('ui.userfront.qr.result_failure');
final message = _resultMessage ?? '';
return Center(
@@ -226,7 +223,11 @@ class _QRScanScreenState extends State<QRScanScreen> {
const SizedBox(height: 16),
Text(
title,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color),
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 12),
Text(
@@ -238,12 +239,12 @@ class _QRScanScreenState extends State<QRScanScreen> {
if (!success)
FilledButton(
onPressed: _resetScan,
child: Text(tr('ui.userfront.qr.rescan', fallback: '다시 스캔')),
child: Text(tr('ui.userfront.qr.rescan')),
),
if (success)
FilledButton(
onPressed: () => context.pop(),
child: Text(tr('ui.common.close', fallback: '닫기')),
child: Text(tr('ui.common.close')),
),
],
),
@@ -268,7 +269,8 @@ class _QRScanScreenState extends State<QRScanScreen> {
controller: controller,
onDetect: _onDetect,
errorBuilder: (context, error) {
final isPermissionDenied = error.errorCode ==
final isPermissionDenied =
error.errorCode ==
MobileScannerErrorCode.permissionDenied;
return Center(
child: Column(
@@ -280,11 +282,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
isPermissionDenied
? tr(
'msg.userfront.qr.permission_required',
fallback: '카메라 권한이 필요합니다.',
)
: tr(
'msg.userfront.qr.camera_error',
fallback: '카메라 오류: {{error}}',
params: {'error': '${error.errorCode}'},
),
),
@@ -295,10 +295,11 @@ class _QRScanScreenState extends State<QRScanScreen> {
: _requestCameraPermission,
child: Text(
_isRequestingCamera
? tr('ui.common.requesting', fallback: '요청 중...')
? tr(
'ui.common.requesting',
)
: tr(
'ui.userfront.qr.request_permission',
fallback: '카메라 권한 요청하기',
),
),
),

View File

@@ -13,7 +13,8 @@ class ResetPasswordScreen extends StatefulWidget {
class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _confirmPasswordController = TextEditingController();
final TextEditingController _confirmPasswordController =
TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
String? _loginId;
@@ -31,13 +32,13 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
// 2. Fallback to URI query parameter if not available via router
if (_loginId == null || _loginId!.isEmpty) {
final uri = Uri.base;
_loginId = uri.queryParameters['loginId'];
}
final uri = Uri.base;
_loginId = uri.queryParameters['loginId'];
}
// 토큰도 함께 읽어놓는다.
final uri = Uri.base;
_token = uri.queryParameters['token'];
// 토큰도 함께 읽어놓는다.
final uri = Uri.base;
_token = uri.queryParameters['token'];
_loadPolicy();
}
@@ -66,11 +67,11 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
Future<void> _handlePasswordReset() async {
if (_formKey.currentState?.validate() != true) return;
if ((_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty)) {
if ((_loginId == null || _loginId!.isEmpty) &&
(_token == null || _token!.isEmpty)) {
_showError(
tr(
'msg.userfront.reset.invalid_link',
fallback: '유효하지 않은 재설정 링크입니다. (loginId/token 누락)',
),
);
return;
@@ -91,7 +92,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
content: Text(
tr(
'msg.userfront.reset.success',
fallback: '비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요.',
),
),
backgroundColor: Colors.green,
@@ -104,7 +104,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
_showError(
tr(
'msg.userfront.reset.error.generic',
fallback: '비밀번호 변경에 실패했습니다: {{error}}',
params: {'error': e.toString()},
),
);
@@ -126,7 +125,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if (_isPolicyLoading) {
return tr(
'msg.userfront.reset.policy_loading',
fallback: '비밀번호 정책을 불러오는 중입니다...',
);
}
final minLength = (_policy?['minLength'] as int?) ?? 12;
@@ -139,7 +137,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
final parts = <String>[
tr(
'msg.userfront.reset.policy.min_length',
fallback: '최소 {{count}}자 이상',
params: {'count': '$minLength'},
),
];
@@ -147,29 +144,26 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
parts.add(
tr(
'msg.userfront.reset.policy.min_types',
fallback: '영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상',
params: {'count': '$minTypes'},
),
);
}
if (requiresLower) {
parts.add(
tr('msg.userfront.reset.policy.lowercase', fallback: '소문자 1개 이상'),
tr('msg.userfront.reset.policy.lowercase'),
);
}
if (requiresUpper) {
parts.add(
tr('msg.userfront.reset.policy.uppercase', fallback: '대문자 1개 이상'),
tr('msg.userfront.reset.policy.uppercase'),
);
}
if (requiresNumber) {
parts.add(
tr('msg.userfront.reset.policy.number', fallback: '숫자 1개 이상'),
);
parts.add(tr('msg.userfront.reset.policy.number'));
}
if (requiresSymbol) {
parts.add(
tr('msg.userfront.reset.policy.symbol', fallback: '특수문자 1개 이상'),
tr('msg.userfront.reset.policy.symbol'),
);
}
@@ -180,16 +174,16 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
tr('ui.userfront.reset.title', fallback: '새 비밀번호 설정'),
),
title: Text(tr('ui.userfront.reset.title')),
centerTitle: true,
),
body: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(24),
child: (_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty)
child:
(_loginId == null || _loginId!.isEmpty) &&
(_token == null || _token!.isEmpty)
? _buildInvalidTokenView()
: Form(
key: _formKey,
@@ -200,7 +194,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
Text(
tr(
'ui.userfront.reset.subtitle',
fallback: '새로운 비밀번호 설정',
),
style: TextStyle(
fontSize: 28,
@@ -221,13 +214,14 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.reset.new_password',
fallback: '새 비밀번호',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_isPasswordObscured ? Icons.visibility_off : Icons.visibility,
_isPasswordObscured
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
@@ -241,14 +235,13 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if (val.isEmpty) {
return tr(
'msg.userfront.reset.error.empty_password',
fallback: '비밀번호를 입력해주세요.',
);
}
final minLength = (_policy?['minLength'] as int?) ?? 12;
final minLength =
(_policy?['minLength'] as int?) ?? 12;
if (val.length < minLength) {
return tr(
'msg.userfront.reset.error.min_length',
fallback: '비밀번호는 최소 {{count}}자 이상이어야 합니다.',
params: {'count': '$minLength'},
);
}
@@ -262,12 +255,11 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if (hasNumber) typeCount++;
if (hasSymbol) typeCount++;
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
final minTypes =
(_policy?['minCharacterTypes'] as int?) ?? 0;
if (minTypes > 0 && typeCount < minTypes) {
return tr(
'msg.userfront.reset.error.min_types',
fallback:
'비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.',
params: {'count': '$minTypes'},
);
}
@@ -275,25 +267,22 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if ((_policy?['lowercase'] ?? true) && !hasLower) {
return tr(
'msg.userfront.reset.error.lowercase',
fallback: '최소 1개 이상의 소문자를 포함해야 합니다.',
);
}
if ((_policy?['uppercase'] ?? false) && !hasUpper) {
return tr(
'msg.userfront.reset.error.uppercase',
fallback: '최소 1개 이상의 대문자를 포함해야 합니다.',
);
}
if ((_policy?['number'] ?? true) && !hasNumber) {
return tr(
'msg.userfront.reset.error.number',
fallback: '최소 1개 이상의 숫자를 포함해야 합니다.',
);
}
if ((_policy?['nonAlphanumeric'] ?? true) && !hasSymbol) {
if ((_policy?['nonAlphanumeric'] ?? true) &&
!hasSymbol) {
return tr(
'msg.userfront.reset.error.symbol',
fallback: '최소 1개 이상의 특수문자를 포함해야 합니다.',
);
}
return null;
@@ -306,17 +295,19 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.reset.confirm_password',
fallback: '새 비밀번호 확인',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_isConfirmPasswordObscured ? Icons.visibility_off : Icons.visibility,
_isConfirmPasswordObscured
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_isConfirmPasswordObscured = !_isConfirmPasswordObscured;
_isConfirmPasswordObscured =
!_isConfirmPasswordObscured;
});
},
),
@@ -325,7 +316,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if (value != _passwordController.text) {
return tr(
'msg.userfront.reset.error.mismatch',
fallback: '비밀번호가 일치하지 않습니다.',
);
}
return null;
@@ -349,7 +339,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
: Text(
tr(
'ui.userfront.reset.submit',
fallback: '비밀번호 변경',
),
),
),
@@ -369,8 +358,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
const Icon(Icons.error_outline, color: Colors.red, size: 60),
const SizedBox(height: 16),
Text(
tr('msg.userfront.reset.invalid_title',
fallback: '유효하지 않은 링크입니다.'),
tr('msg.userfront.reset.invalid_title'),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
@@ -378,7 +366,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
Text(
tr(
'msg.userfront.reset.invalid_body',
fallback: '비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요.',
),
textAlign: TextAlign.center,
),

View File

@@ -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',
),
),
),
),
],