Merge feature/i18n into dev (userfront only)

This commit is contained in:
Lectom C Han committed 2026-02-12 21:53:42 +09:00
commit 47193e56ff
55 files changed
+3982 -1104

No files matched your search

@@ -30,16 +30,14 @@ 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: '오류가 발생했습니다',
));
final detail = isProd
? (isWhitelisted
@@ -49,18 +47,15 @@ class ErrorScreen extends StatelessWidget {
)
: tr(
'msg.userfront.error.detail_contact',
fallback: '에러가 계속되면 관리자에게 문의해주세요',
))
: ((description?.isNotEmpty == true)
? description!
: (hasCode
? tr(
'msg.userfront.error.detail_generic',
fallback: '오류가 발생했습니다.',
)
: tr(
'msg.userfront.error.detail_request',
fallback: '요청을 처리하는 중 문제가 발생했습니다.',
)));
return Scaffold(
@@ -100,7 +95,6 @@ class ErrorScreen extends StatelessWidget {
Text(
tr(
'msg.userfront.error.type',
fallback: '오류 종류: {{type}}',
params: {'type': errorType},
),
style: theme.textTheme.bodySmall?.copyWith(
@@ -112,7 +106,6 @@ class ErrorScreen extends StatelessWidget {
Text(
tr(
'msg.userfront.error.id',
fallback: '오류 ID: {{id}}',
params: {'id': errorId!},
),
style: theme.textTheme.bodySmall?.copyWith(
@@ -141,7 +134,6 @@ class ErrorScreen extends StatelessWidget {
child: Text(
tr(
'ui.userfront.error.go_login',
fallback: '로그인으로 이동',
),
),
),
@@ -159,7 +151,7 @@ class ErrorScreen extends StatelessWidget {
),
),
child: Text(
tr('ui.userfront.error.go_home', fallback: '홈으로 이동'),
tr('ui.userfront.error.go_home'),
),
),
],
@@ -28,7 +28,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
_showError(
tr(
'msg.userfront.forgot.input_required',
fallback: '이메일 또는 휴대폰 번호를 입력해주세요.',
),
);
return;
@@ -56,7 +55,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
content: Text(
tr(
'msg.userfront.forgot.sent',
fallback: '비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요.',
),
),
backgroundColor: Colors.green,
@@ -69,7 +67,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
_showError(
tr(
'msg.userfront.forgot.error',
fallback: '전송에 실패했습니다: {{error}}',
params: {'error': e.toString()},
),
);
@@ -99,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(
@@ -111,7 +108,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
tr('ui.userfront.forgot.heading', fallback: '비밀번호를 잊으셨나요?'),
tr('ui.userfront.forgot.heading'),
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
@@ -138,7 +135,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
child: Text(
tr(
'msg.userfront.forgot.dry_send',
fallback: 'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
),
style: const TextStyle(
color: Color(0xFF8A6D3B),
@@ -154,8 +150,6 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
Text(
tr(
'msg.userfront.forgot.description',
fallback:
'계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.',
),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey),
@@ -166,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),
@@ -189,7 +182,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
),
)
: Text(
tr('ui.userfront.forgot.submit', fallback: '재설정 링크 전송'),
tr('ui.userfront.forgot.submit'),
),
),
],
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:userfront/i18n.dart';
import '../../../core/widgets/language_selector.dart';
import '../../../core/services/web_auth_integration.dart';
import '../../../core/services/auth_proxy_service.dart';
import '../../../core/services/auth_token_store.dart';
@@ -43,6 +44,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
String? _qrImageBase64;
String? _qrPendingRef;
bool _isQrLoading = false;
bool _qrExpired = false;
Timer? _qrPollingTimer;
int _qrRemainingSeconds = 0;
Timer? _qrCountdownTimer;
@@ -58,20 +60,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Timer? _linkResendTimer;
int _linkExpireSeconds = 0;
Timer? _linkExpireTimer;
bool _linkExpired = false;
bool _verificationOnly = false;
bool _verificationApproved = false;
bool _dismissedOverlays = false;
String _verificationMessage = '';
String _verificationTitle = tr(
'ui.userfront.login.verification.title',
fallback: '승인 완료',
);
String _verificationPageTitle = tr(
'ui.userfront.login.verification.page_title',
fallback: '로그인 승인',
);
String _verificationActionLabel = tr(
'ui.userfront.login.verification.action_label',
fallback: '확인',
);
Timer? _verificationRedirectTimer;
bool _noticeHandled = false;
@@ -129,7 +130,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showInfo(
tr(
'msg.userfront.login.qr_login_required',
fallback: '로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다',
),
);
}
@@ -163,7 +163,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.cookie_check_failed',
fallback: '로그인 확인 실패: {{error}}',
params: {'error': e.toString().replaceFirst('Exception: ', '')},
),
);
@@ -255,10 +254,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_linkExpireTimer?.cancel();
_linkExpireTimer = null;
_linkExpireSeconds = 0;
_linkExpired = false;
_shortCodePrefixController.clear();
_shortCodeDigitsController.clear();
}
void _dismissOverlays() {
if (!mounted || _dismissedOverlays) {
return;
}
_dismissedOverlays = true;
final navigator = Navigator.of(context, rootNavigator: true);
navigator.popUntil((route) => route is! PopupRoute);
}
bool _parseBoolParam(String? value) {
if (value == null) {
return false;
@@ -295,8 +304,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
timer.cancel();
if (mounted) {
setState(_resetLinkLoginState);
context.go('/signin');
setState(() {
_linkExpired = true;
});
_showInfo(
tr('msg.userfront.login.link_timeout'),
);
}
});
}
@@ -331,6 +344,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_isQrLoading = true;
_qrImageBase64 = null;
_qrRemainingSeconds = 0;
_qrExpired = false;
});
try {
@@ -355,7 +369,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.qr_init_failed',
fallback: 'QR 초기화에 실패했습니다: {{error}}',
params: {'error': e.toString()},
),
);
@@ -368,7 +381,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_qrCountdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted || _qrRemainingSeconds <= 0) {
timer.cancel();
if (_qrRemainingSeconds <= 0) _stopQrPolling();
if (_qrRemainingSeconds <= 0) {
_stopQrPolling();
if (mounted) {
setState(() {
_qrExpired = true;
});
_showInfo(
tr('msg.userfront.login.qr_expired'),
);
}
}
return;
}
setState(() {
@@ -412,8 +435,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (res['error'] == 'expired_token') {
timer.cancel();
_qrCountdownTimer?.cancel();
if (mounted) {
setState(() {
_qrExpired = true;
});
}
_showError(
tr('msg.userfront.login.qr_expired', fallback: 'QR 세션이 만료되었습니다.'),
tr('msg.userfront.login.qr_expired'),
);
return;
}
@@ -421,14 +449,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (res['status'] == 'ok') {
timer.cancel();
_qrCountdownTimer?.cancel();
final token = res['sessionJwt'] ?? res['token'];
final token =
res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
if (token is String && token.isNotEmpty) {
_completeLoginFromToken(token);
} else {
_showError(
tr(
'msg.userfront.login.token_missing',
fallback: '로그인 토큰을 확인할 수 없습니다.',
),
);
}
@@ -506,13 +534,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}) {
if (!mounted) return;
final resolvedTitle =
title ?? tr('ui.userfront.login.verification.title', fallback: '승인 완료');
title ?? tr('ui.userfront.login.verification.title');
final resolvedPageTitle =
pageTitle ??
tr('ui.userfront.login.verification.page_title', fallback: '로그인 승인');
tr('ui.userfront.login.verification.page_title');
final resolvedActionLabel =
actionLabel ??
tr('ui.userfront.login.verification.action_label', fallback: '확인');
tr('ui.userfront.login.verification.action_label');
setState(() {
_verificationApproved = true;
_verificationMessage = message;
@@ -555,7 +583,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_verificationMessage.isEmpty
? tr(
'msg.userfront.login.verification.success',
fallback: '로그인 승인에 성공했습니다.',
)
: _verificationMessage,
textAlign: TextAlign.center,
@@ -588,11 +615,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
debugPrint("[Auth] Starting verification for token: $token");
final approvedMessage = tr(
'msg.userfront.login.verification.approved',
fallback: '승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.',
);
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
fallback: '승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다',
);
try {
// Use Backend to verify the token (Backend-Driven Flow)
@@ -601,7 +626,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
verifyOnly: _verificationOnly,
);
debugPrint("[Auth] Verification successful for token: $token");
final jwt = res['token'] ?? res['sessionJwt'];
final jwt = res['token'] ?? res['sessionJwt'] ?? res['sessionToken'];
final status = res['status']?.toString();
final hasLocalSession = await _hasValidLocalSession();
final actionPath = hasLocalSession ? '/' : '/signin';
@@ -634,7 +659,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.verification_failed',
fallback: '승인 처리에 실패했습니다: {{error}}',
params: {'error': e.toString()},
),
);
@@ -653,15 +677,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
);
final approvedMessage = tr(
'msg.userfront.login.verification.approved',
fallback: '승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.',
);
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
fallback: '승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다',
);
final linkLoginMessage = tr(
'msg.userfront.login.link.approved',
fallback: '링크로 로그인 되었습니다. 잠시 후 로그인 화면으로 이동합니다.',
);
try {
final res = await AuthProxyService.verifyLoginCode(
@@ -670,7 +691,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
pendingRef: pendingRef,
verifyOnly: _verificationOnly,
);
final jwt = res['sessionJwt'] ?? res['token'];
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
final status = res['status']?.toString();
debugPrint(
"[Auth] Code verification successful for loginId: $sanitizedLoginId",
@@ -699,14 +720,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
_markVerificationApproved(
linkLoginMessage,
title: tr('ui.userfront.login.link.title', fallback: '링크 로그인 완료'),
title: tr('ui.userfront.login.link.title'),
pageTitle: tr(
'ui.userfront.login.link.page_title',
fallback: '링크 로그인',
),
actionLabel: tr(
'ui.userfront.login.link.action_label',
fallback: '로그인 화면으로 이동',
),
actionPath: '/signin',
autoRedirect: true,
@@ -725,7 +744,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.verification_failed',
fallback: '승인 처리에 실패했습니다: {{error}}',
params: {'error': e.toString()},
),
);
@@ -739,18 +757,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
debugPrint("[Auth] Starting short code verification for code: $sanitized");
final approvedMessage = tr(
'msg.userfront.login.verification.approved',
fallback: '승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.',
);
final localSessionMessage = tr(
'msg.userfront.login.verification.approved_local',
fallback: '승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다',
);
try {
final res = await AuthProxyService.verifyLoginShortCode(
sanitized,
verifyOnly: _verificationOnly,
);
final jwt = res['sessionJwt'] ?? res['token'];
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
final status = res['status']?.toString();
debugPrint("[Auth] Short code verification successful");
final hasLocalSession = await _hasValidLocalSession();
@@ -788,7 +804,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.verification_failed',
fallback: '승인 처리에 실패했습니다: {{error}}',
params: {'error': e.toString()},
),
);
@@ -817,7 +832,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.password.missing_credentials',
fallback: '이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.',
),
);
return;
@@ -831,24 +845,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(child: CircularProgressIndicator()),
);
try {
final res = await AuthProxyService.loginWithPassword(
loginId,
password,
loginChallenge: _loginChallenge,
);
final jwt = res['sessionJwt'];
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
final provider = res['provider'] as String?;
final redirectTo = res['redirectTo'] as String?;
if (mounted) Navigator.of(context).pop();
if (redirectTo != null && redirectTo.isNotEmpty) {
webWindow.redirectTo(redirectTo);
return;
@@ -858,14 +864,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_onLoginSuccess(jwt, provider: provider);
}
} catch (e) {
if (mounted) Navigator.of(context).pop();
if (e.toString().contains("User not registered")) {
_showUnregisteredDialog();
} else {
_showError(
tr(
'msg.userfront.login.password.failed',
fallback: '로그인 실패: {{error}}',
params: {'error': e.toString().replaceFirst('Exception: ', '')},
),
);
@@ -896,7 +900,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.link_failed',
fallback: '오류: {{error}}',
params: {'error': e.toString()},
),
);
@@ -910,15 +913,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
bool codeOnly = false,
}) async {
try {
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) =>
const Center(child: CircularProgressIndicator()),
);
}
final initResponse = await AuthProxyService.initEnchantedLink(
loginId,
codeOnly: codeOnly,
@@ -939,18 +933,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_linkPendingRef = pendingRef?.toString();
_lastLinkLoginId = loginId;
_lastLinkIsEmail = isEmail;
_linkExpired = false;
});
Navigator.of(context).pop();
_dismissOverlays();
_showInfo(
isEmail
? tr(
'msg.userfront.login.link_sent_email',
fallback: '입력하신 이메일로 로그인 링크를 보냈습니다.',
)
: tr(
'msg.userfront.login.link_sent_phone',
fallback: '입력하신 번호로 로그인 링크를 보냈습니다.',
),
);
@@ -967,7 +959,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
} catch (e) {
debugPrint("[Auth] Initialization failed: $e");
if (mounted && Navigator.canPop(context)) Navigator.of(context).pop();
if (mounted) {
setState(_resetLinkLoginState);
}
@@ -977,7 +968,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.link_send_failed',
fallback: '전송 실패: {{error}}',
params: {'error': e.toString()},
),
);
@@ -1022,7 +1012,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.link_timeout',
fallback: '로그인 요청 시간이 초과되었습니다.',
),
);
}
@@ -1030,7 +1019,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
if (result['status'] == 'ok') {
final token = result['sessionJwt'] ?? result['token'];
final token =
result['sessionJwt'] ?? result['sessionToken'] ?? result['token'];
if (token is String && token.isNotEmpty) {
debugPrint("[Auth] Polling SUCCESS. Token received.");
_completeLoginFromToken(
@@ -1047,7 +1037,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.token_missing',
fallback: '로그인 토큰을 확인할 수 없습니다.',
),
);
return;
@@ -1061,7 +1050,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
debugPrint("[Auth] Polling timed out for ref: $pendingRef");
Navigator.of(context).pop();
_showError(
tr('msg.userfront.login.link_timeout', fallback: '로그인 요청 시간이 초과되었습니다.'),
tr('msg.userfront.login.link_timeout'),
);
}
}
@@ -1119,6 +1108,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
AuthTokenStore.setToken(token, provider: providerName);
AuthTokenStore.clearPendingProvider();
_dismissOverlays();
try {
await ref.read(profileProvider.notifier).loadProfile();
@@ -1142,7 +1132,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_showError(
tr(
'msg.userfront.login.oidc_failed',
fallback: 'OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요.',
),
);
return;
@@ -1175,18 +1164,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
context: context,
builder: (context) => AlertDialog(
title: Text(
tr('ui.userfront.login.unregistered.title', fallback: '미등록 회원'),
tr('ui.userfront.login.unregistered.title'),
),
content: Text(
tr(
'msg.userfront.login.unregistered.body',
fallback: '가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요.',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(tr('ui.common.cancel', fallback: '취소')),
child: Text(tr('ui.common.cancel')),
),
FilledButton(
onPressed: () {
@@ -1195,7 +1183,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
context.push('/signup');
},
child: Text(
tr('ui.userfront.login.unregistered.action', fallback: '회원가입 하기'),
tr('ui.userfront.login.unregistered.action'),
),
),
],
@@ -1233,7 +1221,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
tr('ui.userfront.app_title'),
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
@@ -1263,8 +1251,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
child: Text(
tr(
'msg.userfront.login.dry_send',
fallback:
'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
),
style: const TextStyle(
color: Color(0xFF8A6D3B),
@@ -1284,19 +1270,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Tab(
text: tr(
'ui.userfront.login.tabs.password',
fallback: '비밀번호',
),
),
Tab(
text: tr(
'ui.userfront.login.tabs.link',
fallback: '로그인 링크',
),
),
Tab(
text: tr(
'ui.userfront.login.tabs.qr',
fallback: 'QR 코드',
),
),
],
@@ -1317,7 +1300,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.field.login_id',
fallback: '이메일 또는 휴대폰 번호',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(
@@ -1333,7 +1315,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.field.password',
fallback: '비밀번호',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(
@@ -1351,7 +1332,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
child: Text(
tr(
'ui.userfront.login.action.submit',
fallback: '로그인',
),
),
),
@@ -1369,7 +1349,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.field.login_id',
fallback: '이메일 또는 휴대폰 번호',
),
hintText: '',
border: const OutlineInputBorder(),
@@ -1388,7 +1367,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
child: Text(
tr(
'ui.userfront.login.link.send',
fallback: '로그인 링크 전송',
),
),
),
@@ -1396,7 +1374,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Text(
tr(
'msg.userfront.login.link.helper',
fallback: '입력하신 정보로 로그인 링크를 전송합니다.',
),
style: const TextStyle(
color: Colors.grey,
@@ -1406,177 +1383,134 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
],
if (_linkPendingRef != null) ...[
Text(
tr(
'msg.userfront.login.link.short_code_help',
fallback:
'링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.',
),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 2,
child: TextField(
controller:
_shortCodePrefixController,
textCapitalization:
TextCapitalization.characters,
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.short_code.prefix',
fallback: '영문 2자리',
),
border:
const OutlineInputBorder(),
hintText: 'AB',
hintStyle: const TextStyle(
color: Colors.grey,
),
),
maxLength: 2,
),
if (_linkExpired) ...[
Text(
tr('msg.userfront.login.link_timeout'),
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
const SizedBox(width: 8),
Expanded(
flex: 4,
child: TextField(
controller:
_shortCodeDigitsController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.short_code.digits',
fallback: '숫자 6자리',
),
border:
const OutlineInputBorder(),
hintText: '345678',
hintStyle: const TextStyle(
color: Colors.grey,
),
suffixText: _linkExpireSeconds > 0
? tr(
'ui.userfront.login.short_code.expire_time',
fallback: '유효시간 {{time}}',
params: {
'time': _formatTime(
_linkExpireSeconds,
),
},
)
: null,
),
maxLength: 6,
),
),
],
),
const SizedBox(height: 12),
FilledButton(
onPressed: () {
final prefix =
_shortCodePrefixController.text
.trim()
.toUpperCase();
final digits =
_shortCodeDigitsController.text
.trim();
if (prefix.length != 2 ||
digits.length != 6) {
_showError(
tr(
'msg.userfront.login.short_code.invalid',
fallback:
'문자 2개와 숫자 6자리를 입력해 주세요.',
),
);
return;
}
_verifyShortCode(prefix + digits);
},
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(45),
),
child: Text(
const SizedBox(height: 12),
FilledButton(
onPressed: () {
setState(_resetLinkLoginState);
},
style: FilledButton.styleFrom(
minimumSize:
const Size.fromHeight(45),
),
child: Text(tr('ui.common.refresh')),
),
] else ...[
Text(
tr(
'ui.userfront.login.short_code.submit',
fallback: '코드로 로그인',
'msg.userfront.login.link.short_code_help',
),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 2,
child: TextField(
controller:
_shortCodePrefixController,
textCapitalization:
TextCapitalization.characters,
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.short_code.prefix',
),
border:
const OutlineInputBorder(),
hintText: 'AB',
hintStyle: const TextStyle(
color: Colors.grey,
),
),
maxLength: 2,
),
),
const SizedBox(width: 8),
Expanded(
flex: 4,
child: TextField(
controller:
_shortCodeDigitsController,
keyboardType:
TextInputType.number,
decoration: InputDecoration(
labelText: tr(
'ui.userfront.login.short_code.digits',
),
border:
const OutlineInputBorder(),
hintText: '345678',
hintStyle: const TextStyle(
color: Colors.grey,
),
suffixText:
_linkExpireSeconds > 0
? tr(
'ui.userfront.login.short_code.expire_time',
params: {
'time': _formatTime(
_linkExpireSeconds,
),
},
)
: null,
),
maxLength: 6,
),
),
],
),
const SizedBox(height: 12),
FilledButton(
onPressed: () {
final prefix =
_shortCodePrefixController.text
.trim()
.toUpperCase();
final digits =
_shortCodeDigitsController.text
.trim();
if (prefix.length != 2 ||
digits.length != 6) {
_showError(
tr(
'msg.userfront.login.short_code.invalid',
),
);
return;
}
_verifyShortCode(prefix + digits);
},
style: FilledButton.styleFrom(
minimumSize:
const Size.fromHeight(45),
),
child: Text(
tr(
'ui.userfront.login.short_code.submit',
),
),
),
),
const SizedBox(height: 12),
TextButton(
onPressed: () {
if (_linkResendSeconds > 0) {
_showInfo(
tr(
'msg.userfront.login.link.resend_wait',
fallback:
'재발송은 {{time}} 후 가능합니다.',
params: {
'time': _formatTime(
_linkResendSeconds,
),
},
),
);
return;
}
final loginId =
_lastLinkLoginId ??
_linkIdController.text.trim();
if (loginId.isEmpty) {
_showError(
tr(
'msg.userfront.login.link.missing_login_id',
fallback:
'이메일 또는 휴대폰 번호를 입력해 주세요.',
),
);
return;
}
_startEnchantedFlow(
loginId,
isEmail:
_lastLinkIsEmail ||
loginId.contains('@'),
codeOnly: false,
);
},
child: Text(
_linkResendSeconds > 0
? tr(
'ui.userfront.login.link.resend_with_time',
fallback: '재발송 ({{time}})',
params: {
'time': _formatTime(
_linkResendSeconds,
),
},
)
: tr(
'ui.common.resend',
fallback: '재발송',
),
),
),
if (!_lastLinkIsEmail) ...[
const SizedBox(height: 4),
const SizedBox(height: 12),
TextButton(
onPressed: () {
if (_linkResendSeconds > 0) {
_showInfo(
tr(
'msg.userfront.login.link.resend_wait',
fallback:
'재발송은 {{time}} 후 가능합니다.',
params: {
'time': _formatTime(
_linkResendSeconds,
@@ -1592,30 +1526,80 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (loginId.isEmpty) {
_showError(
tr(
'msg.userfront.login.link.missing_phone',
fallback: '휴대폰 번호를 입력해 주세요.',
'msg.userfront.login.link.missing_login_id',
),
);
return;
}
_startEnchantedFlow(
loginId,
isEmail: false,
codeOnly: true,
isEmail:
_lastLinkIsEmail ||
loginId.contains('@'),
codeOnly: false,
);
},
child: Text(
tr(
'ui.userfront.login.link.code_only',
fallback: '코드만 받기({{time}})',
params: {
'time': _formatTime(
_linkResendSeconds,
),
},
),
_linkResendSeconds > 0
? tr(
'ui.userfront.login.link.resend_with_time',
params: {
'time': _formatTime(
_linkResendSeconds,
),
},
)
: tr(
'ui.common.resend',
),
),
),
if (!_lastLinkIsEmail) ...[
const SizedBox(height: 4),
TextButton(
onPressed: () {
if (_linkResendSeconds > 0) {
_showInfo(
tr(
'msg.userfront.login.link.resend_wait',
params: {
'time': _formatTime(
_linkResendSeconds,
),
},
),
);
return;
}
final loginId =
_lastLinkLoginId ??
_linkIdController.text.trim();
if (loginId.isEmpty) {
_showError(
tr(
'msg.userfront.login.link.missing_phone',
),
);
return;
}
_startEnchantedFlow(
loginId,
isEmail: false,
codeOnly: true,
);
},
child: Text(
tr(
'ui.userfront.login.link.code_only',
params: {
'time': _formatTime(
_linkResendSeconds,
),
},
),
),
),
],
],
],
],
@@ -1628,6 +1612,28 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
children: [
if (_isQrLoading)
const CircularProgressIndicator()
else if (_qrExpired)
Column(
children: [
Text(
tr('msg.userfront.login.qr_expired'),
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: _startQrFlow,
style: FilledButton.styleFrom(
minimumSize:
const Size.fromHeight(45),
),
child: Text(tr('ui.common.refresh')),
),
],
)
else if (_qrImageBase64 != null)
Column(
crossAxisAlignment:
@@ -1654,7 +1660,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_qrRemainingSeconds > 0
? tr(
'ui.userfront.login.qr.remaining',
fallback: '남은 시간: {{time}}',
params: {
'time': _formatTime(
_qrRemainingSeconds,
@@ -1663,7 +1668,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
)
: tr(
'ui.userfront.login.qr.expired',
fallback: 'QR 코드 만료됨',
),
textAlign: TextAlign.center,
style: TextStyle(
@@ -1677,7 +1681,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Text(
tr(
'msg.userfront.login.qr.scan_hint',
fallback: '모바일 앱으로 스캔하세요',
),
textAlign: TextAlign.center,
style: const TextStyle(
@@ -1690,7 +1693,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
child: Text(
tr(
'ui.userfront.login.qr.refresh',
fallback: 'QR 코드 새로고침',
),
),
),
@@ -1700,7 +1702,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Text(
tr(
'msg.userfront.login.qr.load_failed',
fallback: 'QR 코드를 불러오지 못했습니다.',
),
textAlign: TextAlign.center,
),
@@ -1717,7 +1718,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
child: Text(
tr(
'ui.userfront.login.forgot_password',
fallback: '비밀번호를 잊으셨나요?',
),
),
),
@@ -1727,7 +1727,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Text(
tr(
'msg.userfront.login.no_account',
fallback: '계정이 없으신가요?',
),
style: const TextStyle(
color: Colors.grey,
@@ -1739,7 +1738,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
child: Text(
tr(
'ui.userfront.login.signup',
fallback: '회원가입',
),
),
),
@@ -1747,6 +1745,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
],
),
const SizedBox(height: 6),
const Align(
alignment: Alignment.center,
child: LanguageSelector(),
),
],
),
),
@@ -21,14 +21,13 @@ class LoginSuccessScreen extends StatelessWidget {
),
const SizedBox(height: 24),
Text(
tr('ui.userfront.login_success.title', fallback: '로그인 완료'),
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: '성공적으로 로그인되었습니다.',
),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey, fontSize: 16),
@@ -44,7 +43,6 @@ class LoginSuccessScreen extends StatelessWidget {
label: Text(
tr(
'ui.userfront.login_success.qr',
fallback: 'QR 인증 (카메라 켜기)',
),
),
style: FilledButton.styleFrom(
@@ -67,7 +65,6 @@ class LoginSuccessScreen extends StatelessWidget {
child: Text(
tr(
'ui.userfront.login_success.later',
fallback: '나중에 하기 (대시보드로 이동)',
),
style: const TextStyle(color: Colors.grey),
),
@@ -146,7 +146,6 @@ class _QRScanScreenState extends State<QRScanScreen> {
_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(
@@ -242,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')),
),
],
),
@@ -285,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}'},
),
),
@@ -302,11 +297,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
_isRequestingCamera
? tr(
'ui.common.requesting',
fallback: '요청 중...',
)
: tr(
'ui.userfront.qr.request_permission',
fallback: '카메라 권한 요청하기',
),
),
),
@@ -72,7 +72,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
_showError(
tr(
'msg.userfront.reset.invalid_link',
fallback: '유효하지 않은 재설정 링크입니다. (loginId/token 누락)',
),
);
return;
@@ -93,7 +92,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
content: Text(
tr(
'msg.userfront.reset.success',
fallback: '비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요.',
),
),
backgroundColor: Colors.green,
@@ -106,7 +104,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
_showError(
tr(
'msg.userfront.reset.error.generic',
fallback: '비밀번호 변경에 실패했습니다: {{error}}',
params: {'error': e.toString()},
),
);
@@ -128,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;
@@ -141,7 +137,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
final parts = <String>[
tr(
'msg.userfront.reset.policy.min_length',
fallback: '최소 {{count}}자 이상',
params: {'count': '$minLength'},
),
];
@@ -149,27 +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,7 +174,7 @@ 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(
@@ -200,7 +194,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
Text(
tr(
'ui.userfront.reset.subtitle',
fallback: '새로운 비밀번호 설정',
),
style: TextStyle(
fontSize: 28,
@@ -221,7 +214,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.reset.new_password',
fallback: '새 비밀번호',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock_outline),
@@ -243,7 +235,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if (val.isEmpty) {
return tr(
'msg.userfront.reset.error.empty_password',
fallback: '비밀번호를 입력해주세요.',
);
}
final minLength =
@@ -251,7 +242,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if (val.length < minLength) {
return tr(
'msg.userfront.reset.error.min_length',
fallback: '비밀번호는 최소 {{count}}자 이상이어야 합니다.',
params: {'count': '$minLength'},
);
}
@@ -270,8 +260,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if (minTypes > 0 && typeCount < minTypes) {
return tr(
'msg.userfront.reset.error.min_types',
fallback:
'비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.',
params: {'count': '$minTypes'},
);
}
@@ -279,26 +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) {
return tr(
'msg.userfront.reset.error.symbol',
fallback: '최소 1개 이상의 특수문자를 포함해야 합니다.',
);
}
return null;
@@ -311,7 +295,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.reset.confirm_password',
fallback: '새 비밀번호 확인',
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock_outline),
@@ -333,7 +316,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
if (value != _passwordController.text) {
return tr(
'msg.userfront.reset.error.mismatch',
fallback: '비밀번호가 일치하지 않습니다.',
);
}
return null;
@@ -357,7 +339,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
: Text(
tr(
'ui.userfront.reset.submit',
fallback: '비밀번호 변경',
),
),
),
@@ -377,7 +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,
),
@@ -385,7 +366,6 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
Text(
tr(
'msg.userfront.reset.invalid_body',
fallback: '비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요.',
),
textAlign: TextAlign.center,
),
@@ -167,7 +167,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _emailError = tr(
'msg.userfront.signup.email.invalid',
fallback: '유효한 이메일 형식이 아닙니다.',
),
);
return;
@@ -182,7 +181,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _emailError = tr(
'msg.userfront.signup.email.duplicate',
fallback: '이미 가입된 이메일입니다.',
),
);
return;
@@ -193,7 +191,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _emailError = tr(
'msg.userfront.signup.email.send_failed',
fallback: '발송 실패: {{error}}',
params: {'error': e.toString()},
),
);
@@ -222,7 +219,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _emailError = tr(
'msg.userfront.signup.email.code_mismatch',
fallback: '인증코드가 일치하지 않습니다.',
),
);
}
@@ -230,7 +226,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _emailError = tr(
'msg.userfront.signup.email.verify_failed',
fallback: '인증 실패: {{error}}',
params: {'error': e.toString()},
),
);
@@ -251,7 +246,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _phoneError = tr(
'msg.userfront.signup.phone.send_failed',
fallback: '발송 실패: {{error}}',
params: {'error': e.toString()},
),
);
@@ -280,7 +274,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _phoneError = tr(
'msg.userfront.signup.phone.code_mismatch',
fallback: '인증코드가 일치하지 않습니다.',
),
);
}
@@ -288,7 +281,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _phoneError = tr(
'msg.userfront.signup.phone.verify_failed',
fallback: '인증 실패: {{error}}',
params: {'error': e.toString()},
),
);
@@ -300,7 +292,6 @@ class _SignupScreenState extends State<SignupScreen> {
setState(
() => _confirmPasswordError = tr(
'msg.userfront.signup.password.mismatch',
fallback: '비밀번호가 일치하지 않습니다.',
),
);
return;
@@ -332,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()},
);
}
@@ -373,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'),
),
),
],
@@ -399,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'),
),
],
),
@@ -471,7 +456,6 @@ class _SignupScreenState extends State<SignupScreen> {
Text(
tr(
'msg.userfront.signup.agreement.title',
fallback: '서비스 이용을 위해\n약관에 동의해주세요',
),
style: const TextStyle(
fontSize: 20,
@@ -489,7 +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,
@@ -507,7 +491,6 @@ class _SignupScreenState extends State<SignupScreen> {
_agreementSection(
title: tr(
'ui.userfront.signup.agreement.tos_title',
fallback: '바론 소프트웨어 이용약관 (필수)',
),
content: _tosText,
value: _termsAccepted,
@@ -517,7 +500,6 @@ class _SignupScreenState extends State<SignupScreen> {
_agreementSection(
title: tr(
'ui.userfront.signup.agreement.privacy_title',
fallback: '개인정보 수집 및 이용 동의 (필수)',
),
content: _privacyText,
value: _privacyAccepted,
@@ -765,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),
),
@@ -785,7 +766,6 @@ class _SignupScreenState extends State<SignupScreen> {
child: Text(
tr(
'msg.userfront.signup.auth.affiliate_notice',
fallback: '가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.',
),
style: const TextStyle(
fontSize: 12,
@@ -799,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),
@@ -812,7 +792,6 @@ class _SignupScreenState extends State<SignupScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.auth.email.label',
fallback: '이메일 주소',
),
border: const OutlineInputBorder(),
errorText: _emailError,
@@ -835,10 +814,9 @@ class _SignupScreenState extends State<SignupScreen> {
),
child: Text(
_emailSeconds > 0
? tr('ui.common.resend', fallback: '재발송')
? tr('ui.common.resend')
: tr(
'ui.userfront.signup.auth.request_code',
fallback: '인증요청',
),
),
),
@@ -852,7 +830,6 @@ class _SignupScreenState extends State<SignupScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.auth.code_label',
fallback: '인증코드 6자리',
),
suffixText: _formatTime(_emailSeconds),
border: const OutlineInputBorder(),
@@ -873,7 +850,6 @@ class _SignupScreenState extends State<SignupScreen> {
child: Text(
tr(
'msg.userfront.signup.email.verified',
fallback: '✅ 이메일 인증 완료',
),
style: const TextStyle(
color: Colors.green,
@@ -884,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),
@@ -896,7 +872,6 @@ class _SignupScreenState extends State<SignupScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.phone.label',
fallback: '휴대폰 번호 (-없이)',
),
border: const OutlineInputBorder(),
errorText: _phoneError,
@@ -919,10 +894,9 @@ class _SignupScreenState extends State<SignupScreen> {
),
child: Text(
_phoneSeconds > 0
? tr('ui.common.resend', fallback: '재발송')
? tr('ui.common.resend')
: tr(
'ui.userfront.signup.auth.request_code',
fallback: '인증요청',
),
),
),
@@ -936,7 +910,6 @@ class _SignupScreenState extends State<SignupScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.auth.code_label',
fallback: '인증코드 6자리',
),
suffixText: _formatTime(_phoneSeconds),
border: const OutlineInputBorder(),
@@ -957,7 +930,6 @@ class _SignupScreenState extends State<SignupScreen> {
child: Text(
tr(
'msg.userfront.signup.phone.verified',
fallback: '✅ 휴대폰 인증 완료',
),
style: const TextStyle(
color: Colors.green,
@@ -977,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),
),
@@ -986,7 +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(),
),
),
@@ -1002,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,
),
@@ -1016,13 +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'),
),
),
],
@@ -1052,18 +1021,17 @@ 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',
@@ -1071,15 +1039,15 @@ class _SignupScreenState extends State<SignupScreen> {
),
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
@@ -1095,10 +1063,9 @@ 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(),
),
@@ -1111,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;
@@ -1124,7 +1090,6 @@ class _SignupScreenState extends State<SignupScreen> {
final parts = <String>[
tr(
'msg.userfront.signup.policy.min_length',
fallback: '최소 {{count}}자 이상',
params: {'count': minLength.toString()},
),
];
@@ -1132,27 +1097,25 @@ 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(', ')},
);
}
@@ -1186,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),
),
@@ -1223,7 +1185,6 @@ class _SignupScreenState extends State<SignupScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.password.label',
fallback: '비밀번호',
),
border: const OutlineInputBorder(),
errorText: _passwordError,
@@ -1236,7 +1197,6 @@ class _SignupScreenState extends State<SignupScreen> {
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.min_length',
fallback: '{{count}}자 이상',
params: {'count': minLength.toString()},
),
hasLength,
@@ -1245,7 +1205,6 @@ class _SignupScreenState extends State<SignupScreen> {
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.min_types',
fallback: '문자 유형 {{count}}가지 이상',
params: {'count': minTypes.toString()},
),
hasTypeCount,
@@ -1254,7 +1213,6 @@ class _SignupScreenState extends State<SignupScreen> {
_cryptoCheck(
tr(
'msg.userfront.signup.password.rule.uppercase',
fallback: '대문자',
),
hasUpper,
),
@@ -1262,20 +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,
),
@@ -1290,7 +1246,6 @@ class _SignupScreenState extends State<SignupScreen> {
_confirmPasswordError = (val != _passwordController.text)
? tr(
'msg.userfront.signup.password.mismatch',
fallback: '비밀번호가 일치하지 않습니다.',
)
: null;
});
@@ -1298,7 +1253,6 @@ class _SignupScreenState extends State<SignupScreen> {
decoration: InputDecoration(
labelText: tr(
'ui.userfront.signup.password.confirm_label',
fallback: '비밀번호 확인',
),
border: const OutlineInputBorder(),
errorText: _confirmPasswordError,
@@ -1352,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,
@@ -1394,7 +1348,7 @@ class _SignupScreenState extends State<SignupScreen> {
side: const BorderSide(color: Colors.black),
),
child: Text(
tr('ui.common.prev', fallback: '이전'),
tr('ui.common.prev'),
style: const TextStyle(color: Colors.black),
),
),
@@ -1425,11 +1379,9 @@ class _SignupScreenState extends State<SignupScreen> {
_currentStep < 4
? tr(
'ui.userfront.signup.next_step',
fallback: '다음 단계',
)
: tr(
'ui.userfront.signup.complete',
fallback: '가입 완료',
),
),
),