forked from baron/baron-sso
Merge feature/i18n into dev (userfront only)
This commit is contained in:
@@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: '가입 완료',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -174,7 +174,6 @@ class AuthTimelineNotifier extends Notifier<AuthTimelineState> {
|
||||
isLoadingMore: false,
|
||||
error: tr(
|
||||
'msg.userfront.dashboard.timeline.load_error',
|
||||
fallback: '접속이력을 불러오지 못했습니다.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import '../domain/providers/linked_rps_provider.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
import '../../../../core/services/http_client.dart';
|
||||
import '../../../../core/i18n/locale_utils.dart';
|
||||
import '../../../../core/widgets/language_selector.dart';
|
||||
import '../../../../core/ui/layout_breakpoints.dart';
|
||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||
import '../domain/dashboard_providers.dart';
|
||||
@@ -35,6 +37,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
bool _auditLoading = false;
|
||||
bool _auditLoadingMore = false;
|
||||
bool _isRevoking = false;
|
||||
bool _redirectingToSignin = false;
|
||||
|
||||
bool _showAllActivities = false;
|
||||
final Set<String> _revokedClientIds = {};
|
||||
@@ -43,7 +46,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageScrollController.addListener(_onPageScroll);
|
||||
_loadAuditLogs(reset: true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return;
|
||||
}
|
||||
_loadAuditLogs(reset: true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -63,19 +72,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
tr('ui.userfront.dashboard.revoke.title', fallback: '연동 해지'),
|
||||
tr('ui.userfront.dashboard.revoke.title'),
|
||||
),
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.revoke.confirm',
|
||||
fallback: '{{app}} 앱과의 연동을 해지하시겠습니까?\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.',
|
||||
params: {'app': appName},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||
child: Text(tr('ui.common.cancel')),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
@@ -83,7 +91,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.revoke.confirm_button',
|
||||
fallback: '해지하기',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -102,7 +109,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.revoke.success',
|
||||
fallback: '{{app}} 연동이 해지되었습니다.',
|
||||
params: {'app': appName},
|
||||
),
|
||||
),
|
||||
@@ -120,7 +126,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.revoke.error',
|
||||
fallback: '해지 실패: {{error}}',
|
||||
params: {'error': '$e'},
|
||||
),
|
||||
),
|
||||
@@ -163,7 +168,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.scopes.title',
|
||||
fallback: '권한 (Scopes)',
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -172,7 +176,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.scopes.empty',
|
||||
fallback: '요청된 권한이 없습니다.',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
)
|
||||
@@ -198,7 +201,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.status_history',
|
||||
fallback: '상태 이력',
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
@@ -209,7 +211,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.last_auth',
|
||||
fallback: '최근 인증: {{value}}',
|
||||
params: {'value': item.lastAuthAt},
|
||||
),
|
||||
),
|
||||
@@ -217,15 +218,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final statusLabel = item.status == 'active'
|
||||
? tr('ui.common.status.active', fallback: '활성')
|
||||
? tr('ui.common.status.active')
|
||||
: tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
);
|
||||
return Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.current_status',
|
||||
fallback: '현재 상태: {{status}}',
|
||||
params: {'status': statusLabel},
|
||||
),
|
||||
style: TextStyle(
|
||||
@@ -244,7 +243,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(tr('ui.common.close', fallback: '닫기')),
|
||||
child: Text(tr('ui.common.close')),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -255,50 +254,60 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
Widget _buildSideMenu(BuildContext context, {required bool closeOnTap}) {
|
||||
return SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title: Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
|
||||
selected: true,
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
context.go('/');
|
||||
},
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title: Text(tr('ui.userfront.nav.dashboard')),
|
||||
selected: true,
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
context.go('/');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(tr('ui.userfront.nav.profile')),
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
context.push('/profile');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan')),
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
_onScanQR();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text(tr('ui.userfront.nav.logout')),
|
||||
onTap: () async {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
await _logout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
context.push('/profile');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔')),
|
||||
onTap: () {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
_onScanQR();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
|
||||
onTap: () async {
|
||||
if (closeOnTap) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
await _logout();
|
||||
},
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: LanguageSelector(compact: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -306,6 +315,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
|
||||
Future<void> _refreshAll() async {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return;
|
||||
}
|
||||
await ref.read(profileProvider.notifier).loadProfile();
|
||||
setState(() {
|
||||
_revokedClientIds.clear();
|
||||
@@ -367,6 +380,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadAuditLogs({bool reset = false}) async {
|
||||
if (!_isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
if (_auditLoading || _auditLoadingMore) {
|
||||
return;
|
||||
}
|
||||
@@ -443,15 +459,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
String _authMethodLabel() {
|
||||
if (AuthTokenStore.usesCookie()) {
|
||||
return tr('ui.userfront.auth_method.ory', fallback: 'Ory 세션');
|
||||
return tr('ui.userfront.auth_method.ory');
|
||||
}
|
||||
final provider = AuthTokenStore.getProvider();
|
||||
if (provider == null || provider.isEmpty) {
|
||||
return tr('ui.userfront.auth_method.session', fallback: '세션');
|
||||
return tr('ui.userfront.auth_method.session');
|
||||
}
|
||||
final lower = provider.toLowerCase();
|
||||
if (lower.contains('ory')) {
|
||||
return tr('ui.userfront.auth_method.ory', fallback: 'Ory 세션');
|
||||
return tr('ui.userfront.auth_method.ory');
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
@@ -494,12 +510,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltip = [
|
||||
tr(
|
||||
'msg.userfront.dashboard.approved_device',
|
||||
fallback: '승인 기기: {{device}}',
|
||||
params: {'device': deviceLabel},
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.dashboard.approved_ip',
|
||||
fallback: '승인 IP: {{ip}}',
|
||||
params: {'ip': approvedIp.isEmpty ? '-' : approvedIp},
|
||||
),
|
||||
].join('\n');
|
||||
@@ -522,21 +536,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltipLabel = isOidc
|
||||
? tr(
|
||||
'ui.userfront.dashboard.approved_session.userfront',
|
||||
fallback: '승인한 Userfront 세션 ID',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.approved_session.default',
|
||||
fallback: '승인한 세션 ID',
|
||||
);
|
||||
final tooltip = approvedSessionId.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.dashboard.approved_session.none',
|
||||
fallback: '{{label}} 없음',
|
||||
params: {'label': tooltipLabel},
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.dashboard.approved_session.copy_click',
|
||||
fallback: '{{label}}: {{id}}\\n클릭하면 복사됩니다.',
|
||||
params: {'label': tooltipLabel, 'id': approvedSessionId},
|
||||
);
|
||||
return InkWell(
|
||||
@@ -550,7 +560,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.session_id_copied',
|
||||
fallback: '세션 ID가 복사되었습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -584,7 +593,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return _selectableText(
|
||||
tr(
|
||||
'msg.userfront.dashboard.auth_method',
|
||||
fallback: '인증수단: {{method}}',
|
||||
params: {'method': authMethod},
|
||||
),
|
||||
);
|
||||
@@ -593,12 +601,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltip = [
|
||||
tr(
|
||||
'msg.userfront.dashboard.approved_device',
|
||||
fallback: '승인 기기: {{device}}',
|
||||
params: {'device': deviceLabel},
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.dashboard.approved_ip',
|
||||
fallback: '승인 IP: {{ip}}',
|
||||
params: {'ip': approvedIp.isEmpty ? '-' : approvedIp},
|
||||
),
|
||||
].join('\n');
|
||||
@@ -607,7 +613,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: _selectableText(
|
||||
tr(
|
||||
'msg.userfront.dashboard.auth_method',
|
||||
fallback: '인증수단: {{method}}',
|
||||
params: {'method': authMethod},
|
||||
),
|
||||
style: const TextStyle(
|
||||
@@ -625,11 +630,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltipLabel = isOidc
|
||||
? tr(
|
||||
'ui.userfront.dashboard.approved_session.userfront',
|
||||
fallback: '승인한 Userfront 세션 ID',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.approved_session.default',
|
||||
fallback: '승인한 세션 ID',
|
||||
);
|
||||
return InkWell(
|
||||
onTap: approvedSessionId.isEmpty
|
||||
@@ -642,7 +645,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.session_id_copied',
|
||||
fallback: '세션 ID가 복사되었습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -653,18 +655,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
message: approvedSessionId.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.dashboard.approved_session.none',
|
||||
fallback: '{{label}} 없음',
|
||||
params: {'label': tooltipLabel},
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.dashboard.approved_session.copy_tap',
|
||||
fallback: '{{label}}: {{id}}\\n탭하면 복사됩니다.',
|
||||
params: {'label': tooltipLabel, 'id': approvedSessionId},
|
||||
),
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.auth_method',
|
||||
fallback: '인증수단: {{method}}',
|
||||
params: {
|
||||
'method': isOidc
|
||||
? authMethod
|
||||
@@ -695,7 +694,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final tooltip = clientId.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.dashboard.client_id_missing',
|
||||
fallback: 'Client ID 없음',
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.dashboard.client_id',
|
||||
@@ -717,10 +715,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
String _appLabelForPath(String path) {
|
||||
if (path.startsWith('/api/v1/auth')) {
|
||||
return tr('ui.userfront.app_label.baron', fallback: 'Baron 로그인');
|
||||
return tr('ui.userfront.app_label.baron');
|
||||
}
|
||||
if (path.startsWith('/api/v1/user')) {
|
||||
return tr('ui.userfront.app_label.baron', fallback: 'Baron 로그인');
|
||||
return tr('ui.userfront.app_label.baron');
|
||||
}
|
||||
if (path.startsWith('/api/v1/dev')) {
|
||||
return tr('ui.userfront.app_label.dev_console', fallback: 'Dev Console');
|
||||
@@ -731,11 +729,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
fallback: 'Admin Console',
|
||||
);
|
||||
}
|
||||
return tr('ui.userfront.app_label.baron', fallback: 'Baron 로그인');
|
||||
return tr('ui.userfront.app_label.baron');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isLoggedIn()) {
|
||||
_redirectToSignin();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final isWide = MediaQuery.of(context).size.width >= sideMenuBreakpoint;
|
||||
final profileState = ref.watch(profileProvider);
|
||||
final profile = profileState.value;
|
||||
@@ -747,14 +749,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
tr('ui.userfront.profile.user_fallback', fallback: 'User');
|
||||
final department = profile?.department.isNotEmpty == true
|
||||
? profile!.department
|
||||
: tr('ui.userfront.profile.department_empty', fallback: '소속 정보 없음');
|
||||
: tr('ui.userfront.profile.department_empty');
|
||||
final sessionIssuedAt = _getJwtIssuedAt();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _subtle,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||
tr('ui.userfront.app_title'),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
@@ -763,17 +765,17 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_outline),
|
||||
tooltip: tr('ui.userfront.nav.profile', fallback: '내 정보'),
|
||||
tooltip: tr('ui.userfront.nav.profile'),
|
||||
onPressed: () => context.push('/profile'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔'),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan'),
|
||||
onPressed: _onScanQR,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: tr('ui.userfront.nav.logout', fallback: '로그아웃'),
|
||||
tooltip: tr('ui.userfront.nav.logout'),
|
||||
onPressed: _logout,
|
||||
),
|
||||
],
|
||||
@@ -814,21 +816,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_buildSectionTitle(
|
||||
tr(
|
||||
'ui.userfront.sections.apps',
|
||||
fallback: '나의 App 현황',
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.sections.apps_subtitle',
|
||||
fallback: '현재 연결된 앱과 최근 인증 상태입니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActivitySection(isMobile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.sections.audit', fallback: '접속이력'),
|
||||
tr('ui.userfront.sections.audit'),
|
||||
tr(
|
||||
'msg.userfront.sections.audit_subtitle',
|
||||
fallback: 'Baron 로그인 기준의 최근 접근 기록입니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -853,14 +852,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
) {
|
||||
final sessionLabel = issuedAt != null
|
||||
? _formatDateTime(issuedAt)
|
||||
: tr('ui.userfront.session.unknown', fallback: '알 수 없음');
|
||||
: tr('ui.userfront.session.unknown');
|
||||
final infoColumn = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.greeting',
|
||||
fallback: '안녕하세요, {{name}}님',
|
||||
params: {'name': userName},
|
||||
),
|
||||
style: const TextStyle(
|
||||
@@ -881,7 +879,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
children: [
|
||||
_buildInfoChip(
|
||||
Icons.verified_user,
|
||||
tr('ui.userfront.session.active', fallback: '세션 활성'),
|
||||
tr('ui.userfront.session.active'),
|
||||
),
|
||||
_buildInfoChip(Icons.lock_outline, _authMethodLabel()),
|
||||
_buildInfoChip(Icons.access_time, sessionLabel),
|
||||
@@ -967,7 +965,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.empty',
|
||||
fallback: '연동된 앱이 없습니다.',
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
@@ -979,7 +976,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.empty_detail',
|
||||
fallback: '앱을 연동하면 최근 활동과 상태가 표시됩니다.',
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
@@ -998,14 +994,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.error',
|
||||
fallback: '연동 정보를 불러오지 못했습니다.',
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => ref.read(linkedRpsProvider.notifier).refresh(),
|
||||
child: Text(tr('ui.common.retry', fallback: '다시 시도')),
|
||||
child: Text(tr('ui.common.retry')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1023,7 +1018,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
final lastAuthLabel = rp.lastAuthenticatedAt != null
|
||||
? _formatDateTime(rp.lastAuthenticatedAt!)
|
||||
: tr('ui.userfront.dashboard.activity.linked', fallback: '연동됨');
|
||||
: tr('ui.userfront.dashboard.activity.linked');
|
||||
|
||||
final statusCode = isRevoked ? 'revoked' : 'active';
|
||||
final name = rp.name.isNotEmpty ? rp.name : rp.id;
|
||||
@@ -1128,8 +1123,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
label: Text(
|
||||
_showAllActivities
|
||||
? tr('ui.common.collapse', fallback: '접기')
|
||||
: tr('ui.common.show_more', fallback: '+ 더보기'),
|
||||
? tr('ui.common.collapse')
|
||||
: tr('ui.common.show_more'),
|
||||
style: TextStyle(
|
||||
color: _showAllActivities
|
||||
? Colors.grey
|
||||
@@ -1193,19 +1188,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
color: statusColor.withValues(alpha: 31),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
item.status == 'active'
|
||||
? tr('ui.common.status.active', fallback: '활성')
|
||||
? tr('ui.common.status.active')
|
||||
: tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -1214,7 +1208,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
tr('ui.userfront.dashboard.last_auth_label', fallback: '최근 인증'),
|
||||
tr('ui.userfront.dashboard.last_auth_label'),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -1238,7 +1232,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
child: Text(
|
||||
tr('ui.common.details', fallback: '상세정보'),
|
||||
tr('ui.common.details'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
@@ -1272,11 +1266,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
item.isRevoked
|
||||
? tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
fallback: '해지됨',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.revoke.title',
|
||||
fallback: '연동 해지',
|
||||
),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
@@ -1314,7 +1306,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.link_open_error',
|
||||
fallback: '해당 링크를 열 수 없습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1326,7 +1317,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.link_missing',
|
||||
fallback: '이동할 페이지 주소(Client URI)가 설정되지 않았습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1357,14 +1347,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.audit_load_error',
|
||||
fallback: '접속이력을 불러오지 못했습니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
ref.read(authTimelineProvider.notifier).refresh(),
|
||||
child: Text(tr('ui.common.retry', fallback: '다시 시도')),
|
||||
child: Text(tr('ui.common.retry')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1378,7 +1367,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.audit_empty',
|
||||
fallback: '최근 접속 이력이 없습니다.',
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
@@ -1429,14 +1417,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.date', fallback: '접속일자'),
|
||||
tr('ui.userfront.audit.table.date'),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.app',
|
||||
fallback: '애플리케이션',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1449,7 +1436,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.device',
|
||||
fallback: '접속환경',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1457,7 +1443,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.auth_method',
|
||||
fallback: '인증수단',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1465,20 +1450,19 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.result',
|
||||
fallback: '인증결과',
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.status', fallback: '현황'),
|
||||
tr('ui.userfront.audit.table.status'),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows: state.items.map((log) {
|
||||
final statusLabel = log.status == 'success'
|
||||
? tr('ui.common.status.success', fallback: '성공')
|
||||
: tr('ui.common.status.failure', fallback: '실패');
|
||||
? tr('ui.common.status.success')
|
||||
: tr('ui.common.status.failure');
|
||||
final statusColor = log.status == 'success'
|
||||
? Colors.green
|
||||
: Colors.redAccent;
|
||||
@@ -1523,7 +1507,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_selectableText(
|
||||
tr(
|
||||
'ui.userfront.audit.table.pending',
|
||||
fallback: '(준비중)',
|
||||
),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
@@ -1571,8 +1554,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
_selectableText(
|
||||
log.status == 'success'
|
||||
? tr('ui.common.status.success', fallback: '성공')
|
||||
: tr('ui.common.status.failure', fallback: '실패'),
|
||||
? tr('ui.common.status.success')
|
||||
: tr('ui.common.status.failure'),
|
||||
style: TextStyle(
|
||||
color: log.status == 'success'
|
||||
? Colors.green
|
||||
@@ -1597,14 +1580,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.date',
|
||||
fallback: '접속일자: {{value}}',
|
||||
params: {'value': _formatDateTime(log.timestamp)},
|
||||
),
|
||||
),
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.ip',
|
||||
fallback: '접속 IP: {{value}}',
|
||||
params: {
|
||||
'value': log.ipAddress.isEmpty
|
||||
? tr('ui.common.hyphen', fallback: '-')
|
||||
@@ -1615,7 +1596,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.device',
|
||||
fallback: '접속환경: {{value}}',
|
||||
params: {
|
||||
'value': _deviceLabelFromUserAgent(log.userAgent),
|
||||
},
|
||||
@@ -1630,16 +1610,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_selectableText(
|
||||
tr(
|
||||
'msg.userfront.audit.result',
|
||||
fallback: '인증결과: {{value}}',
|
||||
params: {
|
||||
'value': log.status == 'success'
|
||||
? tr('ui.common.status.success', fallback: '성공')
|
||||
: tr('ui.common.status.failure', fallback: '실패'),
|
||||
? tr('ui.common.status.success')
|
||||
: tr('ui.common.status.failure'),
|
||||
},
|
||||
),
|
||||
),
|
||||
_selectableText(
|
||||
tr('msg.userfront.audit.status', fallback: '현황: (준비중)'),
|
||||
tr('msg.userfront.audit.status'),
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
@@ -1667,13 +1646,12 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.audit.load_more_error',
|
||||
fallback: '더 불러오지 못했습니다.',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
ref.read(authTimelineProvider.notifier).loadMore(),
|
||||
child: Text(tr('ui.common.retry', fallback: '재시도')),
|
||||
child: Text(tr('ui.common.retry')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1683,13 +1661,34 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
tr('msg.userfront.audit.end', fallback: '더 이상 항목이 없습니다.'),
|
||||
tr('msg.userfront.audit.end'),
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
bool _isLoggedIn() {
|
||||
return AuthTokenStore.getToken() != null || AuthTokenStore.usesCookie();
|
||||
}
|
||||
|
||||
void _redirectToSignin() {
|
||||
if (!mounted || _redirectingToSignin) {
|
||||
return;
|
||||
}
|
||||
_redirectingToSignin = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final uri = GoRouterState.of(context).uri;
|
||||
final localeCode =
|
||||
extractLocaleFromPath(uri) ?? resolvePreferredLocaleCode();
|
||||
context.go('/$localeCode/signin');
|
||||
_redirectingToSignin = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivityItem {
|
||||
|
||||
@@ -26,7 +26,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.load_failed',
|
||||
fallback: '프로필을 불러오지 못했습니다: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
@@ -61,7 +60,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,7 +85,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.update_failed',
|
||||
fallback: '프로필 업데이트에 실패했습니다: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
@@ -98,7 +96,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,7 +117,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.send_code_failed',
|
||||
fallback: '인증번호 전송 실패: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
@@ -134,7 +131,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,7 +155,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.password_change_failed',
|
||||
fallback: '비밀번호 변경에 실패했습니다: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
@@ -170,7 +166,7 @@ class ProfileRepository {
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing', fallback: '활성 세션이 없습니다.'),
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +187,6 @@ class ProfileRepository {
|
||||
throw Exception(
|
||||
tr(
|
||||
'err.userfront.profile.verify_code_failed',
|
||||
fallback: '인증 실패: {{error}}',
|
||||
params: {'error': response.body},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:userfront/i18n.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
import '../../../../core/ui/layout_breakpoints.dart';
|
||||
import '../../../../core/widgets/language_selector.dart';
|
||||
import '../../data/models/user_profile_model.dart';
|
||||
import '../../domain/notifiers/profile_notifier.dart';
|
||||
|
||||
@@ -235,7 +236,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.code_sent',
|
||||
fallback: '인증번호가 전송되었습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -249,7 +249,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.send_failed',
|
||||
fallback: '전송 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
),
|
||||
@@ -275,7 +274,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr('msg.userfront.profile.phone.verified', fallback: '인증되었습니다.'),
|
||||
tr('msg.userfront.profile.phone.verified'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -291,7 +290,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.verify_failed',
|
||||
fallback: '인증 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
),
|
||||
@@ -311,7 +309,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.current_required',
|
||||
fallback: '현재 비밀번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -320,7 +317,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.new_required',
|
||||
fallback: '새 비밀번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -329,7 +325,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.mismatch',
|
||||
fallback: '새 비밀번호가 일치하지 않습니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -354,7 +349,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(() {
|
||||
_passwordSuccess = tr(
|
||||
'msg.userfront.profile.password.changed',
|
||||
fallback: '비밀번호가 변경되었습니다.',
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -362,7 +356,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
setState(() {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.profile.password.change_failed',
|
||||
fallback: '비밀번호 변경 실패: {{error}}',
|
||||
params: {'error': message},
|
||||
);
|
||||
});
|
||||
@@ -440,7 +433,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr('msg.userfront.profile.name_required', fallback: '이름을 입력해주세요.'),
|
||||
tr('msg.userfront.profile.name_required'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -452,7 +445,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.department_required',
|
||||
fallback: '소속을 입력해주세요.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -466,7 +458,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone_required',
|
||||
fallback: '휴대폰 번호를 입력해주세요.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -479,7 +470,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone_verify_required',
|
||||
fallback: '휴대폰 번호 인증이 필요합니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -525,7 +515,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.update_success',
|
||||
fallback: '정보가 수정되었습니다.',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -538,7 +527,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.update_failed',
|
||||
fallback: '수정 실패: {{error}}',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
),
|
||||
@@ -551,30 +539,40 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
}
|
||||
|
||||
Widget _buildSideMenu(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title: Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
|
||||
onTap: () => context.go('/'),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title: Text(tr('ui.userfront.nav.dashboard')),
|
||||
onTap: () => context.go('/'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(tr('ui.userfront.nav.profile')),
|
||||
selected: true,
|
||||
onTap: () => context.go('/profile'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan')),
|
||||
onTap: () => context.go('/scan'),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text(tr('ui.userfront.nav.logout')),
|
||||
onTap: _logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||
selected: true,
|
||||
onTap: () => context.go('/profile'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title: Text(tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔')),
|
||||
onTap: () => context.go('/scan'),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
|
||||
onTap: _logout,
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: LanguageSelector(compact: true),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -626,13 +624,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
|
||||
Widget _buildHeaderCard(UserProfile profile) {
|
||||
final name = profile.name.isEmpty
|
||||
? tr('msg.userfront.profile.name_missing', fallback: '이름 없음')
|
||||
? tr('msg.userfront.profile.name_missing')
|
||||
: profile.name;
|
||||
final email = profile.email.isEmpty
|
||||
? tr('msg.userfront.profile.email_missing', fallback: '이메일 없음')
|
||||
? tr('msg.userfront.profile.email_missing')
|
||||
: profile.email;
|
||||
final department = profile.department.isEmpty
|
||||
? tr('msg.userfront.profile.department_missing', fallback: '소속 정보 없음')
|
||||
? tr('msg.userfront.profile.department_missing')
|
||||
: profile.department;
|
||||
|
||||
return Container(
|
||||
@@ -661,7 +659,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.greeting',
|
||||
fallback: '안녕하세요, {{name}}님',
|
||||
params: {'name': name},
|
||||
),
|
||||
style: const TextStyle(
|
||||
@@ -682,7 +679,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
_buildInfoChip(
|
||||
Icons.badge_outlined,
|
||||
tr('ui.userfront.profile.manage', fallback: '프로필 관리'),
|
||||
tr('ui.userfront.profile.manage'),
|
||||
),
|
||||
_buildInfoChip(
|
||||
Icons.apartment,
|
||||
@@ -725,7 +722,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
title: Text(label),
|
||||
subtitle: Text(displayValue),
|
||||
trailing: Text(
|
||||
tr('ui.common.read_only', fallback: '읽기 전용'),
|
||||
tr('ui.common.read_only'),
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
||||
),
|
||||
);
|
||||
@@ -749,7 +746,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
subtitle: Text(displayValue),
|
||||
trailing: TextButton(
|
||||
onPressed: isUpdating ? null : () => _startEditing(field, profile),
|
||||
child: Text(tr('ui.common.edit', fallback: '수정')),
|
||||
child: Text(tr('ui.common.edit')),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -777,7 +774,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(width: 12),
|
||||
OutlinedButton(
|
||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||
child: Text(tr('ui.common.cancel')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -792,11 +789,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
if (!isEditing) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(tr('ui.userfront.profile.phone.title', fallback: '전화번호')),
|
||||
title: Text(tr('ui.userfront.profile.phone.title')),
|
||||
subtitle: Text(displayValue),
|
||||
trailing: TextButton(
|
||||
onPressed: isUpdating ? null : () => _startEditing('phone', profile),
|
||||
child: Text(tr('ui.common.edit', fallback: '수정')),
|
||||
child: Text(tr('ui.common.edit')),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -805,7 +802,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr('ui.userfront.profile.phone.title', fallback: '전화번호'),
|
||||
tr('ui.userfront.profile.phone.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -835,17 +832,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
onPressed: _isVerifying ? null : _sendCode,
|
||||
child: Text(
|
||||
_isCodeSent
|
||||
? tr('ui.common.resend', fallback: '재전송')
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.profile.phone.request_code',
|
||||
fallback: '인증요청',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton(
|
||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||
child: Text(tr('ui.common.cancel', fallback: '취소')),
|
||||
child: Text(tr('ui.common.cancel')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -865,7 +861,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: tr(
|
||||
'ui.userfront.profile.phone.code_hint',
|
||||
fallback: '인증번호 6자리',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -873,7 +868,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isVerifying ? null : () => _verifyCode(profile),
|
||||
child: Text(tr('ui.common.confirm', fallback: '확인')),
|
||||
child: Text(tr('ui.common.confirm')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -884,7 +879,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.verify_notice',
|
||||
fallback: '휴대폰 번호를 변경하려면 SMS 인증이 필요합니다.',
|
||||
),
|
||||
style: const TextStyle(color: Colors.orange, fontSize: 12),
|
||||
),
|
||||
@@ -899,14 +893,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr('ui.userfront.profile.password.title', fallback: '비밀번호 변경'),
|
||||
tr('ui.userfront.profile.password.title'),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.password.subtitle',
|
||||
fallback: '현재 비밀번호 확인 후 새 비밀번호로 변경합니다.',
|
||||
),
|
||||
style: const TextStyle(color: Color(0xFF6B7280)),
|
||||
),
|
||||
@@ -917,7 +910,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.current',
|
||||
fallback: '현재 비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
@@ -939,7 +931,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.new',
|
||||
fallback: '새 비밀번호',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
@@ -959,7 +950,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.confirm',
|
||||
fallback: '새 비밀번호 확인',
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
@@ -999,7 +989,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
: Text(
|
||||
tr(
|
||||
'ui.userfront.profile.password.change',
|
||||
fallback: '비밀번호 변경',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1009,7 +998,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.profile.password.forgot',
|
||||
fallback: '비밀번호를 잊으셨나요?',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1035,10 +1023,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildHeaderCard(profile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.profile.section.basic', fallback: '기본 정보'),
|
||||
tr('ui.userfront.profile.section.basic'),
|
||||
tr(
|
||||
'msg.userfront.profile.section.basic',
|
||||
fallback: '계정 기본 정보를 관리합니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1049,7 +1036,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
field: 'name',
|
||||
label: tr(
|
||||
'ui.userfront.profile.field.name',
|
||||
fallback: '이름',
|
||||
),
|
||||
value: profile.name,
|
||||
profile: profile,
|
||||
@@ -1060,7 +1046,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.email',
|
||||
fallback: '이메일',
|
||||
),
|
||||
profile.email,
|
||||
),
|
||||
@@ -1073,11 +1058,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildSectionTitle(
|
||||
tr(
|
||||
'ui.userfront.profile.section.organization',
|
||||
fallback: '조직 정보',
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.profile.section.organization',
|
||||
fallback: '소속 및 구분 정보입니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1088,7 +1071,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
field: 'department',
|
||||
label: tr(
|
||||
'ui.userfront.profile.field.department',
|
||||
fallback: '소속',
|
||||
),
|
||||
value: profile.department,
|
||||
profile: profile,
|
||||
@@ -1099,7 +1081,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.affiliation',
|
||||
fallback: '구분',
|
||||
),
|
||||
profile.affiliationType,
|
||||
),
|
||||
@@ -1108,7 +1089,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.tenant',
|
||||
fallback: '소속 테넌트',
|
||||
),
|
||||
profile.tenant!.name,
|
||||
),
|
||||
@@ -1118,7 +1098,6 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.company_code',
|
||||
fallback: '회사코드',
|
||||
),
|
||||
profile.companyCode,
|
||||
),
|
||||
@@ -1128,10 +1107,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.profile.section.security', fallback: '보안'),
|
||||
tr('ui.userfront.profile.section.security'),
|
||||
tr(
|
||||
'msg.userfront.profile.section.security',
|
||||
fallback: '비밀번호를 안전하게 관리합니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1160,7 +1138,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
if (profile == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
|
||||
title: Text(tr('ui.userfront.nav.profile')),
|
||||
),
|
||||
body: profileState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
@@ -1171,14 +1149,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.load_failed',
|
||||
fallback: '정보를 불러올 수 없습니다.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
ref.read(profileProvider.notifier).loadProfile(),
|
||||
child: Text(tr('ui.common.retry', fallback: '재시도')),
|
||||
child: Text(tr('ui.common.retry')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1195,7 +1172,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
backgroundColor: _subtle,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
|
||||
tr('ui.userfront.app_title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
@@ -1204,17 +1181,17 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: tr('ui.userfront.nav.dashboard', fallback: '대시보드'),
|
||||
tooltip: tr('ui.userfront.nav.dashboard'),
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔'),
|
||||
tooltip: tr('ui.userfront.nav.qr_scan'),
|
||||
onPressed: () => context.push('/scan'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: tr('ui.userfront.nav.logout', fallback: '로그아웃'),
|
||||
tooltip: tr('ui.userfront.nav.logout'),
|
||||
onPressed: _logout,
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user