1
0
forked from baron/baron-sso

Merge feature/i18n into dev (userfront only)

This commit is contained in:
Lectom C Han
2026-02-12 21:53:42 +09:00
55 changed files with 3982 additions and 1104 deletions

View File

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

View File

@@ -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

View File

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

View File

@@ -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: '카메라 권한 요청하기',
),
),
),

View File

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

View File

@@ -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: '가입 완료',
),
),
),

View File

@@ -174,7 +174,6 @@ class AuthTimelineNotifier extends Notifier<AuthTimelineState> {
isLoadingMore: false,
error: tr(
'msg.userfront.dashboard.timeline.load_error',
fallback: '접속이력을 불러오지 못했습니다.',
),
);
}

View File

@@ -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 {

View File

@@ -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},
),
);

View File

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