forked from baron/baron-sso
린트 적용
This commit is contained in:
@@ -21,12 +21,7 @@ class Tenant {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'slug': slug,
|
||||
'description': description,
|
||||
};
|
||||
return {'id': id, 'name': name, 'slug': slug, 'description': description};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +57,9 @@ class UserProfile {
|
||||
department: json['department'] ?? '',
|
||||
affiliationType: json['affiliationType'] ?? '',
|
||||
companyCode: json['companyCode'] ?? '',
|
||||
metadata: json['metadata'] != null ? Map<String, dynamic>.from(json['metadata']) : null,
|
||||
metadata: json['metadata'] != null
|
||||
? Map<String, dynamic>.from(json['metadata'])
|
||||
: null,
|
||||
tenant: json['tenant'] != null ? Tenant.fromJson(json['tenant']) : null,
|
||||
);
|
||||
}
|
||||
@@ -81,11 +78,7 @@ class UserProfile {
|
||||
};
|
||||
}
|
||||
|
||||
UserProfile copyWith({
|
||||
String? name,
|
||||
String? phone,
|
||||
String? department,
|
||||
}) {
|
||||
UserProfile copyWith({String? name, String? phone, String? department}) {
|
||||
return UserProfile(
|
||||
id: id,
|
||||
email: email,
|
||||
|
||||
@@ -13,7 +13,8 @@ class ProfileRepository {
|
||||
return dotenv.env[key] ?? fallback;
|
||||
}
|
||||
|
||||
static String get _baseUrl => _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
static String get _baseUrl =>
|
||||
_envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
|
||||
// Helper to get session token
|
||||
static Future<String?> _getToken() async {
|
||||
@@ -31,9 +32,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -68,9 +67,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -107,9 +104,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/send-code');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -145,9 +140,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/password');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -183,9 +176,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/verify-code');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
@@ -32,20 +32,20 @@ class ProfileNotifier extends AsyncNotifier<UserProfile?> {
|
||||
}) async {
|
||||
// Show loading state
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
|
||||
// Perform update and then re-fetch profile
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(profileRepositoryProvider).updateMyProfile(
|
||||
name: name,
|
||||
phone: phone,
|
||||
department: department,
|
||||
);
|
||||
await ref
|
||||
.read(profileRepositoryProvider)
|
||||
.updateMyProfile(name: name, phone: phone, department: department);
|
||||
return _fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Provider definition
|
||||
final profileProvider = AsyncNotifierProvider<ProfileNotifier, UserProfile?>(() {
|
||||
return ProfileNotifier();
|
||||
});
|
||||
final profileProvider = AsyncNotifierProvider<ProfileNotifier, UserProfile?>(
|
||||
() {
|
||||
return ProfileNotifier();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -140,7 +140,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
if (_editingField != 'name' && _nameController!.text != profile.name) {
|
||||
_nameController!.text = profile.name;
|
||||
}
|
||||
if (_editingField != 'department' && _departmentController!.text != profile.department) {
|
||||
if (_editingField != 'department' &&
|
||||
_departmentController!.text != profile.department) {
|
||||
_departmentController!.text = profile.department;
|
||||
}
|
||||
if (_editingField != 'phone' && _phoneController!.text != profile.phone) {
|
||||
@@ -274,10 +275,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', fallback: '인증되었습니다.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -310,24 +308,30 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
final confirmPassword = _confirmPasswordController?.text.trim() ?? '';
|
||||
|
||||
if (currentPassword.isEmpty) {
|
||||
setState(() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.current_required',
|
||||
fallback: '현재 비밀번호를 입력해 주세요.',
|
||||
));
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.current_required',
|
||||
fallback: '현재 비밀번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (newPassword.isEmpty) {
|
||||
setState(() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.new_required',
|
||||
fallback: '새 비밀번호를 입력해 주세요.',
|
||||
));
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.new_required',
|
||||
fallback: '새 비밀번호를 입력해 주세요.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (newPassword != confirmPassword) {
|
||||
setState(() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.mismatch',
|
||||
fallback: '새 비밀번호가 일치하지 않습니다.',
|
||||
));
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.mismatch',
|
||||
fallback: '새 비밀번호가 일치하지 않습니다.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,7 +342,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
});
|
||||
|
||||
try {
|
||||
await ref.read(profileRepositoryProvider).changePassword(
|
||||
await ref
|
||||
.read(profileRepositoryProvider)
|
||||
.changePassword(
|
||||
currentPassword: currentPassword,
|
||||
newPassword: newPassword,
|
||||
);
|
||||
@@ -434,10 +440,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', fallback: '이름을 입력해주세요.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -500,7 +503,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_isSavingField = true;
|
||||
|
||||
try {
|
||||
await ref.read(profileProvider.notifier).updateProfile(
|
||||
await ref
|
||||
.read(profileProvider.notifier)
|
||||
.updateProfile(
|
||||
name: nextName,
|
||||
phone: nextPhone,
|
||||
department: nextDepartment,
|
||||
@@ -551,32 +556,24 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title: Text(
|
||||
tr('ui.userfront.nav.dashboard', fallback: '대시보드'),
|
||||
),
|
||||
title: Text(tr('ui.userfront.nav.dashboard', fallback: '대시보드')),
|
||||
onTap: () => context.go('/'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(
|
||||
tr('ui.userfront.nav.profile', fallback: '내 정보'),
|
||||
),
|
||||
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 스캔'),
|
||||
),
|
||||
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: '로그아웃'),
|
||||
),
|
||||
title: Text(tr('ui.userfront.nav.logout', fallback: '로그아웃')),
|
||||
onTap: _logout,
|
||||
),
|
||||
],
|
||||
@@ -589,13 +586,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: _ink),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
|
||||
),
|
||||
Text(subtitle, style: TextStyle(fontSize: 13, color: Colors.grey[600])),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -615,7 +613,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: _ink, fontWeight: FontWeight.w600),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _ink,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -650,10 +652,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 32,
|
||||
child: Icon(Icons.person, size: 32),
|
||||
),
|
||||
const CircleAvatar(radius: 32, child: Icon(Icons.person, size: 32)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -672,7 +671,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(email, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
||||
Text(
|
||||
email,
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@@ -682,7 +684,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
Icons.badge_outlined,
|
||||
tr('ui.userfront.profile.manage', fallback: '프로필 관리'),
|
||||
),
|
||||
_buildInfoChip(Icons.apartment, profile.tenant?.name ?? department),
|
||||
_buildInfoChip(
|
||||
Icons.apartment,
|
||||
profile.tenant?.name ?? department,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -787,9 +792,7 @@ 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', fallback: '전화번호')),
|
||||
subtitle: Text(displayValue),
|
||||
trailing: TextButton(
|
||||
onPressed: isUpdating ? null : () => _startEditing('phone', profile),
|
||||
@@ -918,7 +921,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_showCurrentPassword ? Icons.visibility_off : Icons.visibility),
|
||||
icon: Icon(
|
||||
_showCurrentPassword
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
),
|
||||
onPressed: () => setState(() {
|
||||
_showCurrentPassword = !_showCurrentPassword;
|
||||
}),
|
||||
@@ -936,7 +943,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_showNewPassword ? Icons.visibility_off : Icons.visibility),
|
||||
icon: Icon(
|
||||
_showNewPassword ? Icons.visibility_off : Icons.visibility,
|
||||
),
|
||||
onPressed: () => setState(() {
|
||||
_showNewPassword = !_showNewPassword;
|
||||
}),
|
||||
@@ -954,7 +963,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_showConfirmPassword ? Icons.visibility_off : Icons.visibility),
|
||||
icon: Icon(
|
||||
_showConfirmPassword
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
),
|
||||
onPressed: () => setState(() {
|
||||
_showConfirmPassword = !_showConfirmPassword;
|
||||
}),
|
||||
@@ -963,10 +976,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
if (_passwordError != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_passwordError!,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
Text(_passwordError!, style: const TextStyle(color: Colors.red)),
|
||||
],
|
||||
if (_passwordSuccess != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
@@ -1037,7 +1047,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'name',
|
||||
label: tr('ui.userfront.profile.field.name', fallback: '이름'),
|
||||
label: tr(
|
||||
'ui.userfront.profile.field.name',
|
||||
fallback: '이름',
|
||||
),
|
||||
value: profile.name,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
@@ -1045,7 +1058,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile(
|
||||
tr('ui.userfront.profile.field.email', fallback: '이메일'),
|
||||
tr(
|
||||
'ui.userfront.profile.field.email',
|
||||
fallback: '이메일',
|
||||
),
|
||||
profile.email,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
@@ -1055,7 +1071,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.profile.section.organization', fallback: '조직 정보'),
|
||||
tr(
|
||||
'ui.userfront.profile.section.organization',
|
||||
fallback: '조직 정보',
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.profile.section.organization',
|
||||
fallback: '소속 및 구분 정보입니다.',
|
||||
@@ -1067,7 +1086,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'department',
|
||||
label: tr('ui.userfront.profile.field.department', fallback: '소속'),
|
||||
label: tr(
|
||||
'ui.userfront.profile.field.department',
|
||||
fallback: '소속',
|
||||
),
|
||||
value: profile.department,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
@@ -1075,7 +1097,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile(
|
||||
tr('ui.userfront.profile.field.affiliation', fallback: '구분'),
|
||||
tr(
|
||||
'ui.userfront.profile.field.affiliation',
|
||||
fallback: '구분',
|
||||
),
|
||||
profile.affiliationType,
|
||||
),
|
||||
if (profile.tenant != null) ...[
|
||||
@@ -1091,7 +1116,10 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
if (profile.companyCode.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile(
|
||||
tr('ui.userfront.profile.field.company_code', fallback: '회사코드'),
|
||||
tr(
|
||||
'ui.userfront.profile.field.company_code',
|
||||
fallback: '회사코드',
|
||||
),
|
||||
profile.companyCode,
|
||||
),
|
||||
],
|
||||
@@ -1148,7 +1176,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(profileProvider.notifier).loadProfile(),
|
||||
onPressed: () =>
|
||||
ref.read(profileProvider.notifier).loadProfile(),
|
||||
child: Text(tr('ui.common.retry', fallback: '재시도')),
|
||||
),
|
||||
],
|
||||
@@ -1193,11 +1222,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
drawer: isWide ? null : Drawer(child: _buildSideMenu(context)),
|
||||
body: Row(
|
||||
children: [
|
||||
if (isWide)
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: _buildSideMenu(context),
|
||||
),
|
||||
if (isWide) SizedBox(width: 240, child: _buildSideMenu(context)),
|
||||
Expanded(child: _buildContent(profile, isUpdating)),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -4,11 +4,7 @@ class ProfileInfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const ProfileInfoRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
const ProfileInfoRow({super.key, required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Reference in New Issue
Block a user