1
0
forked from baron/baron-sso

i18n 대대적 변경

This commit is contained in:
Lectom C Han
2026-02-10 19:13:00 +09:00
parent 798d37bed9
commit 8df95c8a13
27 changed files with 3910 additions and 594 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:userfront/i18n.dart';
import '../../../../core/notifiers/auth_notifier.dart';
import '../../../../core/services/auth_token_store.dart';
import '../../../../core/ui/layout_breakpoints.dart';
@@ -229,14 +230,29 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('인증번호가 전송되었습니다.')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.phone.code_sent',
fallback: '인증번호가 전송되었습니다.',
),
),
),
);
}
} catch (e) {
setState(() => _isVerifying = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('전송 실패: $e')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.phone.send_failed',
fallback: '전송 실패: {{error}}',
params: {'error': e.toString()},
),
),
),
);
}
}
@@ -256,7 +272,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('인증되었습니다.')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.phone.verified',
fallback: '인증되었습니다.',
),
),
),
);
}
if (_editingField == 'phone') {
@@ -266,7 +289,15 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
setState(() => _isVerifying = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('인증 실패: $e')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.phone.verify_failed',
fallback: '인증 실패: {{error}}',
params: {'error': e.toString()},
),
),
),
);
}
}
@@ -279,15 +310,24 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
final confirmPassword = _confirmPasswordController?.text.trim() ?? '';
if (currentPassword.isEmpty) {
setState(() => _passwordError = '현재 비밀번호를 입력해 주세요.');
setState(() => _passwordError = tr(
'msg.userfront.profile.password.current_required',
fallback: '현재 비밀번호를 입력해 주세요.',
));
return;
}
if (newPassword.isEmpty) {
setState(() => _passwordError = '새 비밀번호를 입력해 주세요.');
setState(() => _passwordError = tr(
'msg.userfront.profile.password.new_required',
fallback: '새 비밀번호를 입력해 주세요.',
));
return;
}
if (newPassword != confirmPassword) {
setState(() => _passwordError = '새 비밀번호가 일치하지 않습니다.');
setState(() => _passwordError = tr(
'msg.userfront.profile.password.mismatch',
fallback: '새 비밀번호가 일치하지 않습니다.',
));
return;
}
@@ -306,12 +346,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_newPasswordController?.clear();
_confirmPasswordController?.clear();
setState(() {
_passwordSuccess = '비밀번호가 변경되었습니다.';
_passwordSuccess = tr(
'msg.userfront.profile.password.changed',
fallback: '비밀번호가 변경되었습니다.',
);
});
} catch (e) {
final message = e.toString().replaceFirst('Exception: ', '');
setState(() {
_passwordError = '비밀번호 변경 실패: $message';
_passwordError = tr(
'msg.userfront.profile.password.change_failed',
fallback: '비밀번호 변경 실패: {{error}}',
params: {'error': message},
);
});
} finally {
if (mounted) {
@@ -385,26 +432,54 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
if (_editingField == 'name' && nextName.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('이름을 입력해주세요.')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.name_required',
fallback: '이름을 입력해주세요.',
),
),
),
);
return;
}
if (_editingField == 'department' && nextDepartment.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('소속을 입력해주세요.')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.department_required',
fallback: '소속을 입력해주세요.',
),
),
),
);
return;
}
if (_editingField == 'phone') {
if (nextPhone.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('휴대폰 번호를 입력해주세요.')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.phone_required',
fallback: '휴대폰 번호를 입력해주세요.',
),
),
),
);
return;
}
if (_isPhoneChanged && !_isPhoneVerified) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('휴대폰 번호 인증이 필요합니다.')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.phone_verify_required',
fallback: '휴대폰 번호 인증이 필요합니다.',
),
),
),
);
return;
}
@@ -441,13 +516,28 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_departmentTouched = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('정보가 수정되었습니다.')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.update_success',
fallback: '정보가 수정되었습니다.',
),
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('수정 실패: $e')),
SnackBar(
content: Text(
tr(
'msg.userfront.profile.update_failed',
fallback: '수정 실패: {{error}}',
params: {'error': e.toString()},
),
),
),
);
}
} finally {
@@ -461,24 +551,32 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
children: [
ListTile(
leading: const Icon(Icons.home_outlined),
title: const Text('대시보드'),
title: Text(
tr('ui.userfront.nav.dashboard', fallback: '대시보드'),
),
onTap: () => context.go('/'),
),
ListTile(
leading: const Icon(Icons.person_outline),
title: const Text('내 정보'),
title: Text(
tr('ui.userfront.nav.profile', fallback: '내 정보'),
),
selected: true,
onTap: () => context.go('/profile'),
),
ListTile(
leading: const Icon(Icons.qr_code_scanner),
title: const Text('QR 스캔'),
title: Text(
tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔'),
),
onTap: () => context.go('/scan'),
),
const Divider(),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('로그아웃'),
title: Text(
tr('ui.userfront.nav.logout', fallback: '로그아웃'),
),
onTap: _logout,
),
],
@@ -525,9 +623,15 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
Widget _buildHeaderCard(UserProfile profile) {
final name = profile.name.isEmpty ? '이름 없음' : profile.name;
final email = profile.email.isEmpty ? '메일 없음' : profile.email;
final department = profile.department.isEmpty ? '소속 정보 없음' : profile.department;
final name = profile.name.isEmpty
? tr('msg.userfront.profile.name_missing', fallback: ' 없음')
: profile.name;
final email = profile.email.isEmpty
? tr('msg.userfront.profile.email_missing', fallback: '이메일 없음')
: profile.email;
final department = profile.department.isEmpty
? tr('msg.userfront.profile.department_missing', fallback: '소속 정보 없음')
: profile.department;
return Container(
width: double.infinity,
@@ -538,7 +642,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
border: Border.all(color: _border),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
color: Colors.black.withValues(alpha: 10),
blurRadius: 18,
offset: const Offset(0, 8),
),
@@ -556,8 +660,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'안녕하세요, $name님',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: _ink),
tr(
'msg.userfront.profile.greeting',
fallback: '안녕하세요, {{name}}님',
params: {'name': name},
),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: _ink,
),
),
const SizedBox(height: 6),
Text(email, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
@@ -566,7 +678,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
spacing: 8,
runSpacing: 8,
children: [
_buildInfoChip(Icons.badge_outlined, '프로필 관리'),
_buildInfoChip(
Icons.badge_outlined,
tr('ui.userfront.profile.manage', fallback: '프로필 관리'),
),
_buildInfoChip(Icons.apartment, profile.tenant?.name ?? department),
],
),
@@ -588,7 +703,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
border: Border.all(color: _border),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
color: Colors.black.withValues(alpha: 8),
blurRadius: 12,
offset: const Offset(0, 6),
),
@@ -605,7 +720,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
title: Text(label),
subtitle: Text(displayValue),
trailing: Text(
'읽기 전용',
tr('ui.common.read_only', fallback: '읽기 전용'),
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
);
@@ -629,7 +744,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
subtitle: Text(displayValue),
trailing: TextButton(
onPressed: isUpdating ? null : () => _startEditing(field, profile),
child: const Text('수정'),
child: Text(tr('ui.common.edit', fallback: '수정')),
),
);
}
@@ -657,7 +772,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
const SizedBox(width: 12),
OutlinedButton(
onPressed: isUpdating ? null : () => _cancelEditing(profile),
child: const Text('취소'),
child: Text(tr('ui.common.cancel', fallback: '취소')),
),
],
),
@@ -672,11 +787,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
if (!isEditing) {
return ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('전화번호'),
title: Text(
tr('ui.userfront.profile.phone.title', fallback: '전화번호'),
),
subtitle: Text(displayValue),
trailing: TextButton(
onPressed: isUpdating ? null : () => _startEditing('phone', profile),
child: const Text('수정'),
child: Text(tr('ui.common.edit', fallback: '수정')),
),
);
}
@@ -684,7 +801,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('전화번호', style: TextStyle(fontWeight: FontWeight.w600)),
Text(
tr('ui.userfront.profile.phone.title', fallback: '전화번호'),
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
@@ -710,12 +830,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
if (_isPhoneChanged && !_isPhoneVerified)
ElevatedButton(
onPressed: _isVerifying ? null : _sendCode,
child: Text(_isCodeSent ? '재전송' : '인증요청'),
child: Text(
_isCodeSent
? tr('ui.common.resend', fallback: '재전송')
: tr(
'ui.userfront.profile.phone.request_code',
fallback: '인증요청',
),
),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: isUpdating ? null : () => _cancelEditing(profile),
child: const Text('취소'),
child: Text(tr('ui.common.cancel', fallback: '취소')),
),
],
),
@@ -731,26 +858,32 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _verifyCode(profile),
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '인증번호 6자리',
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: tr(
'ui.userfront.profile.phone.code_hint',
fallback: '인증번호 6자리',
),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _isVerifying ? null : () => _verifyCode(profile),
child: const Text('확인'),
child: Text(tr('ui.common.confirm', fallback: '확인')),
),
],
),
],
if (_isPhoneChanged && !_isPhoneVerified)
const Padding(
padding: EdgeInsets.only(top: 8.0),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'휴대폰 번호를 변경하려면 SMS 인증이 필요합니다.',
style: TextStyle(color: Colors.orange, fontSize: 12),
tr(
'msg.userfront.profile.phone.verify_notice',
fallback: '휴대폰 번호를 변경하려면 SMS 인증이 필요합니다.',
),
style: const TextStyle(color: Colors.orange, fontSize: 12),
),
),
],
@@ -763,20 +896,26 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'비밀번호 변경',
tr('ui.userfront.profile.password.title', fallback: '비밀번호 변경'),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
const Text(
'현재 비밀번호 확인 후 새 비밀번호로 변경합니다.',
style: TextStyle(color: Color(0xFF6B7280)),
Text(
tr(
'msg.userfront.profile.password.subtitle',
fallback: '현재 비밀번호 확인 후 새 비밀번호로 변경합니다.',
),
style: const TextStyle(color: Color(0xFF6B7280)),
),
const SizedBox(height: 16),
TextField(
controller: _currentPasswordController,
obscureText: !_showCurrentPassword,
decoration: InputDecoration(
labelText: '현재 비밀번호',
labelText: tr(
'ui.userfront.profile.password.current',
fallback: '현재 비밀번호',
),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(_showCurrentPassword ? Icons.visibility_off : Icons.visibility),
@@ -791,7 +930,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
controller: _newPasswordController,
obscureText: !_showNewPassword,
decoration: InputDecoration(
labelText: '새 비밀번호',
labelText: tr(
'ui.userfront.profile.password.new',
fallback: '새 비밀번호',
),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(_showNewPassword ? Icons.visibility_off : Icons.visibility),
@@ -806,7 +948,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
controller: _confirmPasswordController,
obscureText: !_showConfirmPassword,
decoration: InputDecoration(
labelText: '새 비밀번호 확인',
labelText: tr(
'ui.userfront.profile.password.confirm',
fallback: '새 비밀번호 확인',
),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(_showConfirmPassword ? Icons.visibility_off : Icons.visibility),
@@ -841,12 +986,22 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('비밀번호 변경'),
: Text(
tr(
'ui.userfront.profile.password.change',
fallback: '비밀번호 변경',
),
),
),
const SizedBox(width: 12),
TextButton(
onPressed: () => context.go('/recovery'),
child: const Text('비밀번호를 잊으셨나요?'),
child: Text(
tr(
'ui.userfront.profile.password.forgot',
fallback: '비밀번호를 잊으셨나요?',
),
),
),
],
),
@@ -869,55 +1024,88 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
children: [
_buildHeaderCard(profile),
const SizedBox(height: 28),
_buildSectionTitle('기본 정보', '계정 기본 정보를 관리합니다.'),
_buildSectionTitle(
tr('ui.userfront.profile.section.basic', fallback: '기본 정보'),
tr(
'msg.userfront.profile.section.basic',
fallback: '계정 기본 정보를 관리합니다.',
),
),
const SizedBox(height: 12),
_buildCard(
Column(
children: [
_buildEditableTile(
field: 'name',
label: '이름',
label: tr('ui.userfront.profile.field.name', fallback: '이름'),
value: profile.name,
profile: profile,
isUpdating: isUpdating,
controller: _nameController!,
),
const Divider(height: 24),
_buildReadOnlyTile('이메일', profile.email),
_buildReadOnlyTile(
tr('ui.userfront.profile.field.email', fallback: '이메일'),
profile.email,
),
const Divider(height: 24),
_buildPhoneEditor(profile, isUpdating),
],
),
),
const SizedBox(height: 28),
_buildSectionTitle('조직 정보', '소속 및 구분 정보입니다.'),
_buildSectionTitle(
tr('ui.userfront.profile.section.organization', fallback: '조직 정보'),
tr(
'msg.userfront.profile.section.organization',
fallback: '소속 및 구분 정보입니다.',
),
),
const SizedBox(height: 12),
_buildCard(
Column(
children: [
_buildEditableTile(
field: 'department',
label: '소속',
label: tr('ui.userfront.profile.field.department', fallback: '소속'),
value: profile.department,
profile: profile,
isUpdating: isUpdating,
controller: _departmentController!,
),
const Divider(height: 24),
_buildReadOnlyTile('구분', profile.affiliationType),
_buildReadOnlyTile(
tr('ui.userfront.profile.field.affiliation', fallback: '구분'),
profile.affiliationType,
),
if (profile.tenant != null) ...[
const Divider(height: 24),
_buildReadOnlyTile('소속 테넌트', profile.tenant!.name),
_buildReadOnlyTile(
tr(
'ui.userfront.profile.field.tenant',
fallback: '소속 테넌트',
),
profile.tenant!.name,
),
],
if (profile.companyCode.isNotEmpty) ...[
const Divider(height: 24),
_buildReadOnlyTile('회사코드', profile.companyCode),
_buildReadOnlyTile(
tr('ui.userfront.profile.field.company_code', fallback: '회사코드'),
profile.companyCode,
),
],
],
),
),
const SizedBox(height: 28),
_buildSectionTitle('보안', '비밀번호를 안전하게 관리합니다.'),
_buildSectionTitle(
tr('ui.userfront.profile.section.security', fallback: '보안'),
tr(
'msg.userfront.profile.section.security',
fallback: '비밀번호를 안전하게 관리합니다.',
),
),
const SizedBox(height: 12),
_buildPasswordSection(),
if (isUpdating || _isVerifying) ...[
@@ -943,18 +1131,25 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
final profile = profileState.value ?? _cachedProfile;
if (profile == null) {
return Scaffold(
appBar: AppBar(title: const Text('내 정보')),
appBar: AppBar(
title: Text(tr('ui.userfront.nav.profile', fallback: '내 정보')),
),
body: profileState.isLoading
? const Center(child: CircularProgressIndicator())
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('정보를 불러올 수 없습니다.'),
Text(
tr(
'msg.userfront.profile.load_failed',
fallback: '정보를 불러올 수 없습니다.',
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.read(profileProvider.notifier).loadProfile(),
child: const Text('재시도'),
child: Text(tr('ui.common.retry', fallback: '재시도')),
),
],
),
@@ -971,8 +1166,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
backgroundColor: _subtle,
appBar: AppBar(
title: Text(
'Baron 로그인',
style: TextStyle(fontWeight: FontWeight.bold),
tr('ui.userfront.app_title', fallback: 'Baron 로그인'),
style: const TextStyle(fontWeight: FontWeight.bold),
),
elevation: 0,
backgroundColor: _surface,
@@ -980,17 +1175,17 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
actions: [
IconButton(
icon: const Icon(Icons.home_outlined),
tooltip: '대시보드',
tooltip: tr('ui.userfront.nav.dashboard', fallback: '대시보드'),
onPressed: () => context.go('/'),
),
IconButton(
icon: const Icon(Icons.qr_code_scanner),
tooltip: 'QR 스캔',
tooltip: tr('ui.userfront.nav.qr_scan', fallback: 'QR 스캔'),
onPressed: () => context.push('/scan'),
),
IconButton(
icon: const Icon(Icons.logout),
tooltip: '로그아웃',
tooltip: tr('ui.userfront.nav.logout', fallback: '로그아웃'),
onPressed: _logout,
),
],