forked from baron/baron-sso
i18n 대대적 변경
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
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/services/web_auth_integration.dart';
|
||||
import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/services/auth_token_store.dart';
|
||||
@@ -51,10 +52,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
bool _verificationOnly = false;
|
||||
bool _verificationApproved = false;
|
||||
String _verificationMessage = '';
|
||||
String _verificationTitle = '승인 완료';
|
||||
String _verificationPageTitle = '로그인 승인';
|
||||
String _verificationActionLabel = '확인';
|
||||
String _verificationActionPath = '/';
|
||||
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;
|
||||
bool _drySendEnabled = false;
|
||||
@@ -92,7 +101,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
if (!_noticeHandled && notice == 'qr_login_required') {
|
||||
_noticeHandled = true;
|
||||
_showInfo('로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다');
|
||||
_showInfo(
|
||||
tr(
|
||||
'msg.userfront.login.qr_login_required',
|
||||
fallback: '로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!_verificationOnly) {
|
||||
@@ -125,7 +139,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
} catch (e) {
|
||||
if (!silent) {
|
||||
_showError("로그인 확인 실패: ${e.toString().replaceFirst("Exception: ", "")}");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.cookie_check_failed',
|
||||
fallback: '로그인 확인 실패: {{error}}',
|
||||
params: {
|
||||
'error': e.toString().replaceFirst('Exception: ', ''),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +316,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_startCountdown();
|
||||
}
|
||||
} catch (e) {
|
||||
_showError("Failed to init QR: $e");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.qr_init_failed',
|
||||
fallback: 'QR 초기화에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
if (mounted) setState(() => _isQrLoading = false);
|
||||
}
|
||||
}
|
||||
@@ -346,7 +374,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (res['error'] == 'expired_token') {
|
||||
timer.cancel();
|
||||
_qrCountdownTimer?.cancel();
|
||||
_showError("QR 세션이 만료되었습니다.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.qr_expired',
|
||||
fallback: 'QR 세션이 만료되었습니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -357,7 +390,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (token is String && token.isNotEmpty) {
|
||||
_completeLoginFromToken(token);
|
||||
} else {
|
||||
_showError("로그인 토큰을 확인할 수 없습니다.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.token_missing',
|
||||
fallback: '로그인 토큰을 확인할 수 없습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -423,21 +461,35 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
void _markVerificationApproved(
|
||||
String message, {
|
||||
String title = '승인 완료',
|
||||
String pageTitle = '로그인 승인',
|
||||
String actionLabel = '확인',
|
||||
String? title,
|
||||
String? pageTitle,
|
||||
String? actionLabel,
|
||||
String actionPath = '/',
|
||||
bool autoRedirect = false,
|
||||
Duration redirectDelay = const Duration(seconds: 2),
|
||||
}) {
|
||||
if (!mounted) return;
|
||||
final resolvedTitle = title ??
|
||||
tr(
|
||||
'ui.userfront.login.verification.title',
|
||||
fallback: '승인 완료',
|
||||
);
|
||||
final resolvedPageTitle = pageTitle ??
|
||||
tr(
|
||||
'ui.userfront.login.verification.page_title',
|
||||
fallback: '로그인 승인',
|
||||
);
|
||||
final resolvedActionLabel = actionLabel ??
|
||||
tr(
|
||||
'ui.userfront.login.verification.action_label',
|
||||
fallback: '확인',
|
||||
);
|
||||
setState(() {
|
||||
_verificationApproved = true;
|
||||
_verificationMessage = message;
|
||||
_verificationTitle = title;
|
||||
_verificationPageTitle = pageTitle;
|
||||
_verificationActionLabel = actionLabel;
|
||||
_verificationActionPath = actionPath;
|
||||
_verificationTitle = resolvedTitle;
|
||||
_verificationPageTitle = resolvedPageTitle;
|
||||
_verificationActionLabel = resolvedActionLabel;
|
||||
});
|
||||
_verificationRedirectTimer?.cancel();
|
||||
if (autoRedirect) {
|
||||
@@ -463,7 +515,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_verificationMessage.isEmpty ? '로그인 승인에 성공했습니다.' : _verificationMessage,
|
||||
_verificationMessage.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.login.verification.success',
|
||||
fallback: '로그인 승인에 성공했습니다.',
|
||||
)
|
||||
: _verificationMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.black54),
|
||||
),
|
||||
@@ -490,6 +547,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
Future<void> _verifyToken(String token) async {
|
||||
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)
|
||||
final res = await AuthProxyService.verifyMagicLink(
|
||||
@@ -505,7 +570,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (status == 'approved' || (jwt == null && _verificationOnly)) {
|
||||
if (mounted) {
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
}
|
||||
@@ -515,13 +580,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved(
|
||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
||||
localSessionMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
return;
|
||||
@@ -529,14 +594,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
if (mounted) {
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Verification FAILED for token: $token. Error: $e");
|
||||
if (mounted) {
|
||||
_showError("Verification failed: $e");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.verification_failed',
|
||||
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -544,6 +615,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
Future<void> _verifyLoginCode(String loginId, String code, {String? pendingRef}) async {
|
||||
final sanitizedLoginId = loginId.replaceAll(' ', '+');
|
||||
debugPrint("[Auth] Starting code verification for loginId: $sanitizedLoginId");
|
||||
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(
|
||||
sanitizedLoginId,
|
||||
@@ -560,7 +643,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (jwt == null && status == 'approved') {
|
||||
if (mounted) {
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
}
|
||||
@@ -570,22 +653,32 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved(
|
||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
||||
localSessionMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_verificationOnly) {
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_markVerificationApproved("링크로 로그인 되었습니다. 잠시 후 로그인 화면으로 이동합니다.",
|
||||
title: '링크 로그인 완료',
|
||||
pageTitle: '링크 로그인',
|
||||
actionLabel: '로그인 화면으로 이동',
|
||||
_markVerificationApproved(
|
||||
linkLoginMessage,
|
||||
title: tr(
|
||||
'ui.userfront.login.link.title',
|
||||
fallback: '링크 로그인 완료',
|
||||
),
|
||||
pageTitle: tr(
|
||||
'ui.userfront.login.link.page_title',
|
||||
fallback: '링크 로그인',
|
||||
),
|
||||
actionLabel: tr(
|
||||
'ui.userfront.login.link.action_label',
|
||||
fallback: '로그인 화면으로 이동',
|
||||
),
|
||||
actionPath: '/signin',
|
||||
autoRedirect: true,
|
||||
);
|
||||
@@ -594,14 +687,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
if (_verificationOnly && mounted) {
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Code verification FAILED for loginId: $sanitizedLoginId. Error: $e");
|
||||
if (mounted) {
|
||||
_showError("Verification failed: $e");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.verification_failed',
|
||||
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -610,6 +709,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final sanitized = shortCode.trim().toUpperCase();
|
||||
if (sanitized.isEmpty) return;
|
||||
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,
|
||||
@@ -624,7 +731,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (jwt == null && status == 'approved') {
|
||||
if (mounted) {
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
}
|
||||
@@ -634,14 +741,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (jwt is String && jwt.isNotEmpty) {
|
||||
if (hasLocalSession) {
|
||||
_markVerificationApproved(
|
||||
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
|
||||
localSessionMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_verificationOnly) {
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
return;
|
||||
@@ -652,14 +759,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
if (_verificationOnly && mounted) {
|
||||
_markVerificationApproved(
|
||||
"승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.",
|
||||
approvedMessage,
|
||||
actionPath: actionPath,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("[Auth] Short code verification FAILED. Error: $e");
|
||||
if (mounted) {
|
||||
_showError("Verification failed: $e");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.verification_failed',
|
||||
fallback: '승인 처리에 실패했습니다: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,7 +795,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final input = _passwordLoginIdController.text.trim();
|
||||
final password = _passwordController.text.trim();
|
||||
if (input.isEmpty || password.isEmpty) {
|
||||
_showError("이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.password.missing_credentials',
|
||||
fallback: '이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -721,7 +839,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (e.toString().contains("User not registered")) {
|
||||
_showUnregisteredDialog();
|
||||
} else {
|
||||
_showError("로그인 실패: ${e.toString().replaceFirst("Exception: ", "")}");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.password.failed',
|
||||
fallback: '로그인 실패: {{error}}',
|
||||
params: {
|
||||
'error': e.toString().replaceFirst('Exception: ', ''),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -746,7 +872,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (e.toString().contains("User not registered")) {
|
||||
_showUnregisteredDialog();
|
||||
} else {
|
||||
_showError("오류: $e");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link_failed',
|
||||
fallback: '오류: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -782,9 +914,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
|
||||
_showInfo(isEmail
|
||||
? "입력하신 이메일로 로그인 링크를 보냈습니다."
|
||||
: "입력하신 번호로 로그인 링크를 보냈습니다.");
|
||||
_showInfo(
|
||||
isEmail
|
||||
? tr(
|
||||
'msg.userfront.login.link_sent_email',
|
||||
fallback: '입력하신 이메일로 로그인 링크를 보냈습니다.',
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.login.link_sent_phone',
|
||||
fallback: '입력하신 번호로 로그인 링크를 보냈습니다.',
|
||||
),
|
||||
);
|
||||
|
||||
final initialInterval = (interval is int && interval > 0)
|
||||
? Duration(seconds: interval)
|
||||
@@ -806,7 +946,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (e.toString().contains("User not registered")) {
|
||||
_showUnregisteredDialog();
|
||||
} else {
|
||||
_showError("전송 실패: $e");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link_send_failed',
|
||||
fallback: '전송 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -842,7 +988,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (result['error'] == 'expired_token') {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
_showError("Login timed out.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link_timeout',
|
||||
fallback: '로그인 요청 시간이 초과되었습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -862,7 +1013,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (mounted && Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
_showError("로그인 토큰을 확인할 수 없습니다.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.token_missing',
|
||||
fallback: '로그인 토큰을 확인할 수 없습니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -873,7 +1029,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (mounted) {
|
||||
debugPrint("[Auth] Polling timed out for ref: $pendingRef");
|
||||
Navigator.of(context).pop();
|
||||
_showError("Login timed out.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link_timeout',
|
||||
fallback: '로그인 요청 시간이 초과되었습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,7 +1111,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
_showError("OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.oidc_failed',
|
||||
fallback: 'OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -978,12 +1144,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("미등록 회원"),
|
||||
content: const Text("가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요."),
|
||||
title: Text(
|
||||
tr('ui.userfront.login.unregistered.title', fallback: '미등록 회원'),
|
||||
),
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.login.unregistered.body',
|
||||
fallback: '가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요.',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("취소"),
|
||||
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
@@ -991,7 +1164,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_resetLinkLoginState();
|
||||
context.push('/signup');
|
||||
},
|
||||
child: const Text("회원가입 하기"),
|
||||
child: Text(
|
||||
tr('ui.userfront.login.unregistered.action', fallback: '회원가입 하기'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1028,8 +1203,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"Baron 로그인",
|
||||
style: TextStyle(
|
||||
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -1045,13 +1220,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
border: Border.all(color: const Color(0xFFFFC107)),
|
||||
),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
|
||||
SizedBox(width: 8),
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, color: Color(0xFF8A6D3B)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.",
|
||||
style: TextStyle(color: Color(0xFF8A6D3B), fontSize: 12),
|
||||
tr(
|
||||
'msg.userfront.login.dry_send',
|
||||
fallback: 'drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF8A6D3B),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1062,10 +1243,25 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: "비밀번호"),
|
||||
Tab(text: "로그인 링크"),
|
||||
Tab(text: "QR 코드"),
|
||||
tabs: [
|
||||
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 코드',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -1081,10 +1277,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
children: [
|
||||
TextField(
|
||||
controller: _passwordLoginIdController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "이메일 또는 휴대폰 번호",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.login.field.login_id',
|
||||
fallback: '이메일 또는 휴대폰 번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
),
|
||||
onSubmitted: (_) => _handlePasswordLogin(),
|
||||
),
|
||||
@@ -1092,10 +1291,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "비밀번호",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.login.field.password',
|
||||
fallback: '비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
),
|
||||
onSubmitted: (_) => _handlePasswordLogin(),
|
||||
),
|
||||
@@ -1105,7 +1307,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
),
|
||||
child: const Text("로그인"),
|
||||
child: Text(
|
||||
tr('ui.userfront.login.action.submit', fallback: '로그인'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1118,11 +1322,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
if (_linkPendingRef == null) ...[
|
||||
TextField(
|
||||
controller: _linkIdController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "이메일 또는 휴대폰 번호",
|
||||
hintText: "",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.login.field.login_id',
|
||||
fallback: '이메일 또는 휴대폰 번호',
|
||||
),
|
||||
hintText: '',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
),
|
||||
onSubmitted: (_) => _handleLinkLogin(),
|
||||
),
|
||||
@@ -1132,19 +1339,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
),
|
||||
child: const Text("로그인 링크 전송"),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.link.send',
|
||||
fallback: '로그인 링크 전송',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
"입력하신 정보로 로그인 링크를 전송합니다.",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.link.helper',
|
||||
fallback: '입력하신 정보로 로그인 링크를 전송합니다.',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (_linkPendingRef != null) ...[
|
||||
const Text(
|
||||
"링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
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),
|
||||
@@ -1155,11 +1373,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
child: TextField(
|
||||
controller: _shortCodePrefixController,
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "영문 2자리",
|
||||
border: OutlineInputBorder(),
|
||||
hintText: "AB",
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
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,
|
||||
),
|
||||
@@ -1171,12 +1392,21 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
controller: _shortCodeDigitsController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: "숫자 6자리",
|
||||
labelText: tr(
|
||||
'ui.userfront.login.short_code.digits',
|
||||
fallback: '숫자 6자리',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: "345678",
|
||||
hintText: '345678',
|
||||
hintStyle: const TextStyle(color: Colors.grey),
|
||||
suffixText: _linkExpireSeconds > 0
|
||||
? "유효시간 ${_formatTime(_linkExpireSeconds)}"
|
||||
? tr(
|
||||
'ui.userfront.login.short_code.expire_time',
|
||||
fallback: '유효시간 {{time}}',
|
||||
params: {
|
||||
'time': _formatTime(_linkExpireSeconds),
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
maxLength: 6,
|
||||
@@ -1190,7 +1420,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
final prefix = _shortCodePrefixController.text.trim().toUpperCase();
|
||||
final digits = _shortCodeDigitsController.text.trim();
|
||||
if (prefix.length != 2 || digits.length != 6) {
|
||||
_showError("문자 2개와 숫자 6자리를 입력해 주세요.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.short_code.invalid',
|
||||
fallback: '문자 2개와 숫자 6자리를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_verifyShortCode(prefix + digits);
|
||||
@@ -1198,18 +1433,36 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(45),
|
||||
),
|
||||
child: const Text("코드로 로그인"),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.short_code.submit',
|
||||
fallback: '코드로 로그인',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (_linkResendSeconds > 0) {
|
||||
_showInfo("재발송은 ${_formatTime(_linkResendSeconds)} 후 가능합니다.");
|
||||
_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("이메일 또는 휴대폰 번호를 입력해 주세요.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link.missing_login_id',
|
||||
fallback: '이메일 또는 휴대폰 번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_startEnchantedFlow(
|
||||
@@ -1220,8 +1473,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
},
|
||||
child: Text(
|
||||
_linkResendSeconds > 0
|
||||
? "재발송 (${_formatTime(_linkResendSeconds)})"
|
||||
: "재발송",
|
||||
? tr(
|
||||
'ui.userfront.login.link.resend_with_time',
|
||||
fallback: '재발송 ({{time}})',
|
||||
params: {
|
||||
'time': _formatTime(_linkResendSeconds),
|
||||
},
|
||||
)
|
||||
: tr(
|
||||
'ui.common.resend',
|
||||
fallback: '재발송',
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_lastLinkIsEmail) ...[
|
||||
@@ -1229,12 +1491,25 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (_linkResendSeconds > 0) {
|
||||
_showInfo("재발송은 ${_formatTime(_linkResendSeconds)} 후 가능합니다.");
|
||||
_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("휴대폰 번호를 입력해 주세요.");
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.link.missing_phone',
|
||||
fallback: '휴대폰 번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_startEnchantedFlow(
|
||||
@@ -1243,7 +1518,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
codeOnly: true,
|
||||
);
|
||||
},
|
||||
child: Text("코드만 받기(${_formatTime(_linkResendSeconds)})"),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.link.code_only',
|
||||
fallback: '코드만 받기({{time}})',
|
||||
params: {
|
||||
'time': _formatTime(_linkResendSeconds),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -1276,8 +1559,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_qrRemainingSeconds > 0
|
||||
? "남은 시간: ${_formatTime(_qrRemainingSeconds)}"
|
||||
: "QR 코드 만료됨",
|
||||
? tr(
|
||||
'ui.userfront.login.qr.remaining',
|
||||
fallback: '남은 시간: {{time}}',
|
||||
params: {
|
||||
'time': _formatTime(_qrRemainingSeconds),
|
||||
},
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.login.qr.expired',
|
||||
fallback: 'QR 코드 만료됨',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: _qrRemainingSeconds > 30 ? Colors.blue : Colors.red,
|
||||
@@ -1285,19 +1577,33 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
"모바일 앱으로 스캔하세요",
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.qr.scan_hint',
|
||||
fallback: '모바일 앱으로 스캔하세요',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _startQrFlow,
|
||||
child: const Text("QR 코드 새로고침")
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.qr.refresh',
|
||||
fallback: 'QR 코드 새로고침',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
const Text("QR 코드를 불러오지 못했습니다.", textAlign: TextAlign.center),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.qr.load_failed',
|
||||
fallback: 'QR 코드를 불러오지 못했습니다.',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -1308,15 +1614,31 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => context.push('/forgot-password'),
|
||||
child: const Text("비밀번호를 잊으셨나요?"),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.forgot_password',
|
||||
fallback: '비밀번호를 잊으셨나요?',
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text("계정이 없으신가요?", style: TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login.no_account',
|
||||
fallback: '계정이 없으신가요?',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.push('/signup'),
|
||||
child: const Text("회원가입"),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login.signup',
|
||||
fallback: '회원가입',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user