forked from baron/baron-sso
userfront 이력 session ID기반 작업 완료.
This commit is contained in:
@@ -1,251 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../domain/notifiers/profile_notifier.dart';
|
||||
|
||||
class EditProfilePage extends ConsumerStatefulWidget {
|
||||
const EditProfilePage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<EditProfilePage> createState() => _EditProfilePageState();
|
||||
}
|
||||
|
||||
class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _nameController;
|
||||
late TextEditingController _phoneController;
|
||||
late TextEditingController _codeController;
|
||||
late TextEditingController _departmentController;
|
||||
|
||||
String? _initialPhone;
|
||||
bool _isPhoneChanged = false;
|
||||
bool _isPhoneVerified = false;
|
||||
bool _isCodeSent = false;
|
||||
bool _isVerifying = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final profile = ref.read(profileProvider).value;
|
||||
_initialPhone = profile?.phone ?? '';
|
||||
_nameController = TextEditingController(text: profile?.name ?? '');
|
||||
_phoneController = TextEditingController(text: _initialPhone);
|
||||
_codeController = TextEditingController();
|
||||
_departmentController = TextEditingController(text: profile?.department ?? '');
|
||||
|
||||
_phoneController.addListener(() {
|
||||
setState(() {
|
||||
_isPhoneChanged = _phoneController.text != _initialPhone;
|
||||
if (_isPhoneChanged) {
|
||||
_isPhoneVerified = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_codeController.dispose();
|
||||
_departmentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _sendCode() async {
|
||||
final phone = _phoneController.text;
|
||||
if (phone.isEmpty) return;
|
||||
|
||||
setState(() => _isVerifying = true);
|
||||
try {
|
||||
await ref.read(profileRepositoryProvider).sendUpdateCode(phone);
|
||||
setState(() {
|
||||
_isCodeSent = true;
|
||||
_isVerifying = false;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('인증번호가 전송되었습니다.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isVerifying = false);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('전송 실패: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyCode() async {
|
||||
final phone = _phoneController.text;
|
||||
final code = _codeController.text;
|
||||
if (code.isEmpty) return;
|
||||
|
||||
setState(() => _isVerifying = true);
|
||||
try {
|
||||
await ref.read(profileRepositoryProvider).verifyUpdateCode(phone, code);
|
||||
setState(() {
|
||||
_isPhoneVerified = true;
|
||||
_isVerifying = false;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('인증되었습니다.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isVerifying = false);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('인증 실패: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
if (_isPhoneChanged && !_isPhoneVerified) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('휴대폰 번호 인증이 필요합니다.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ref.read(profileProvider.notifier).updateProfile(
|
||||
name: _nameController.text,
|
||||
phone: _phoneController.text,
|
||||
department: _departmentController.text,
|
||||
);
|
||||
if (mounted) {
|
||||
context.pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('정보가 수정되었습니다.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('수정 실패: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileState = ref.watch(profileProvider);
|
||||
final isUpdating = profileState.isLoading;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('내 정보 수정'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: (isUpdating || (_isPhoneChanged && !_isPhoneVerified)) ? null : _save,
|
||||
child: const Text('저장'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '이름',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
),
|
||||
validator: (value) => (value == null || value.isEmpty) ? '이름을 입력해주세요.' : null,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Phone Number Field
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _phoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '휴대폰 번호',
|
||||
hintText: '01012345678',
|
||||
prefixIcon: const Icon(Icons.phone_android),
|
||||
suffixIcon: _isPhoneVerified
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: null,
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
enabled: !_isPhoneVerified,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (_isPhoneChanged && !_isPhoneVerified)
|
||||
ElevatedButton(
|
||||
onPressed: _isVerifying ? null : _sendCode,
|
||||
child: Text(_isCodeSent ? '재전송' : '인증요청'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// OTP Code Field
|
||||
if (_isCodeSent && !_isPhoneVerified) ...[
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _codeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '인증번호',
|
||||
hintText: '6자리 입력',
|
||||
prefixIcon: Icon(Icons.security),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isVerifying ? null : _verifyCode,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue[700], foregroundColor: Colors.white),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
if (_isPhoneChanged && !_isPhoneVerified)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8.0, left: 4.0),
|
||||
child: Text(
|
||||
'휴대폰 번호를 변경하려면 SMS 인증이 필요합니다.',
|
||||
style: TextStyle(color: Colors.orange, fontSize: 12),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: _departmentController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '소속 (부서)',
|
||||
prefixIcon: Icon(Icons.business),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
if (isUpdating || _isVerifying)
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,691 @@
|
||||
import 'package:descope/descope.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../../core/notifiers/auth_notifier.dart';
|
||||
import '../../../../core/services/auth_token_store.dart';
|
||||
import '../../../../core/ui/layout_breakpoints.dart';
|
||||
import '../../data/models/user_profile_model.dart';
|
||||
import '../../domain/notifiers/profile_notifier.dart';
|
||||
import '../widgets/profile_info_row.dart';
|
||||
|
||||
class ProfilePage extends ConsumerWidget {
|
||||
class ProfilePage extends ConsumerStatefulWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// profileState is AsyncValue<UserProfile?>
|
||||
final profileState = ref.watch(profileProvider);
|
||||
ConsumerState<ProfilePage> createState() => _ProfilePageState();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('내 정보'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => context.push('/profile/edit'),
|
||||
class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
static const _ink = Color(0xFF1A1F2C);
|
||||
static const _surface = Colors.white;
|
||||
static const _border = Color(0xFFE5E7EB);
|
||||
static const _subtle = Color(0xFFF7F8FA);
|
||||
|
||||
UserProfile? _cachedProfile;
|
||||
String? _editingField;
|
||||
TextEditingController? _nameController;
|
||||
TextEditingController? _phoneController;
|
||||
TextEditingController? _departmentController;
|
||||
TextEditingController? _codeController;
|
||||
|
||||
String _initialPhone = '';
|
||||
bool _isPhoneChanged = false;
|
||||
bool _isPhoneVerified = false;
|
||||
bool _isCodeSent = false;
|
||||
bool _isVerifying = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController?.dispose();
|
||||
_phoneController?.dispose();
|
||||
_departmentController?.dispose();
|
||||
_codeController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
Descope.sessionManager.clearSession();
|
||||
AuthTokenStore.clear();
|
||||
AuthNotifier.instance.notify();
|
||||
}
|
||||
|
||||
void _ensureControllers(UserProfile profile) {
|
||||
_nameController ??= TextEditingController(text: profile.name);
|
||||
_departmentController ??= TextEditingController(text: profile.department);
|
||||
_codeController ??= TextEditingController();
|
||||
|
||||
if (_phoneController == null) {
|
||||
_phoneController = TextEditingController(text: profile.phone);
|
||||
_initialPhone = profile.phone;
|
||||
_phoneController!.addListener(_onPhoneChanged);
|
||||
}
|
||||
|
||||
if (_editingField != 'name' && _nameController!.text != profile.name) {
|
||||
_nameController!.text = profile.name;
|
||||
}
|
||||
if (_editingField != 'department' && _departmentController!.text != profile.department) {
|
||||
_departmentController!.text = profile.department;
|
||||
}
|
||||
if (_editingField != 'phone' && _phoneController!.text != profile.phone) {
|
||||
_phoneController!.text = profile.phone;
|
||||
_initialPhone = profile.phone;
|
||||
_resetPhoneState();
|
||||
}
|
||||
}
|
||||
|
||||
void _onPhoneChanged() {
|
||||
if (_phoneController == null) return;
|
||||
final changed = _phoneController!.text != _initialPhone;
|
||||
if (changed != _isPhoneChanged) {
|
||||
setState(() {
|
||||
_isPhoneChanged = changed;
|
||||
if (_isPhoneChanged) {
|
||||
_isPhoneVerified = false;
|
||||
_isCodeSent = false;
|
||||
_codeController?.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _resetPhoneState() {
|
||||
_isPhoneChanged = false;
|
||||
_isPhoneVerified = false;
|
||||
_isCodeSent = false;
|
||||
_isVerifying = false;
|
||||
_codeController?.clear();
|
||||
}
|
||||
|
||||
void _startEditing(String field, UserProfile profile) {
|
||||
setState(() {
|
||||
_editingField = field;
|
||||
if (field == 'name') {
|
||||
_nameController?.text = profile.name;
|
||||
} else if (field == 'department') {
|
||||
_departmentController?.text = profile.department;
|
||||
} else if (field == 'phone') {
|
||||
_phoneController?.text = profile.phone;
|
||||
_initialPhone = profile.phone;
|
||||
_resetPhoneState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelEditing(UserProfile profile) {
|
||||
setState(() {
|
||||
if (_editingField == 'name') {
|
||||
_nameController?.text = profile.name;
|
||||
} else if (_editingField == 'department') {
|
||||
_departmentController?.text = profile.department;
|
||||
} else if (_editingField == 'phone') {
|
||||
_phoneController?.text = profile.phone;
|
||||
_initialPhone = profile.phone;
|
||||
_resetPhoneState();
|
||||
}
|
||||
_editingField = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _sendCode() async {
|
||||
final phone = _phoneController?.text ?? '';
|
||||
if (phone.isEmpty) return;
|
||||
|
||||
setState(() => _isVerifying = true);
|
||||
try {
|
||||
await ref.read(profileRepositoryProvider).sendUpdateCode(phone);
|
||||
setState(() {
|
||||
_isCodeSent = true;
|
||||
_isVerifying = false;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('인증번호가 전송되었습니다.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isVerifying = false);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('전송 실패: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _verifyCode() async {
|
||||
final phone = _phoneController?.text ?? '';
|
||||
final code = _codeController?.text ?? '';
|
||||
if (code.isEmpty) return;
|
||||
|
||||
setState(() => _isVerifying = true);
|
||||
try {
|
||||
await ref.read(profileRepositoryProvider).verifyUpdateCode(phone, code);
|
||||
setState(() {
|
||||
_isPhoneVerified = true;
|
||||
_isVerifying = false;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('인증되었습니다.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isVerifying = false);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('인증 실패: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveField(UserProfile profile) async {
|
||||
if (_editingField == null) return;
|
||||
|
||||
final nextName = _editingField == 'name'
|
||||
? _nameController!.text.trim()
|
||||
: profile.name;
|
||||
final nextPhone = _editingField == 'phone'
|
||||
? _phoneController!.text.trim()
|
||||
: profile.phone;
|
||||
final nextDepartment = _editingField == 'department'
|
||||
? _departmentController!.text.trim()
|
||||
: profile.department;
|
||||
|
||||
if (_editingField == 'name' && nextName.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('이름을 입력해주세요.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_editingField == 'department' && nextDepartment.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('소속을 입력해주세요.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_editingField == 'phone') {
|
||||
if (nextPhone.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('휴대폰 번호를 입력해주세요.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_isPhoneChanged && !_isPhoneVerified) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('휴대폰 번호 인증이 필요합니다.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await ref.read(profileProvider.notifier).updateProfile(
|
||||
name: nextName,
|
||||
phone: nextPhone,
|
||||
department: nextDepartment,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (_editingField == 'phone') {
|
||||
_initialPhone = nextPhone;
|
||||
_resetPhoneState();
|
||||
}
|
||||
_editingField = null;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('정보가 수정되었습니다.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('수정 실패: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSideMenu(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home_outlined),
|
||||
title: const Text('대시보드'),
|
||||
onTap: () => context.go('/'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: const Text('내 정보'),
|
||||
selected: true,
|
||||
onTap: () => context.go('/profile'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.qr_code_scanner),
|
||||
title: const Text('QR 스캔'),
|
||||
onTap: () => context.go('/scan'),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('로그아웃'),
|
||||
onTap: _logout,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title, String subtitle) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: _ink),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoChip(IconData icon, String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _subtle,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: _border),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: _ink),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: _ink, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: profileState.when(
|
||||
data: (profile) {
|
||||
if (profile == null) {
|
||||
return const Center(child: Text('정보를 불러올 수 없습니다.'));
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(profileProvider.notifier).loadProfile(),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: _surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: _border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 32,
|
||||
child: Icon(Icons.person, size: 32),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Center(
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
child: Icon(Icons.person, size: 40),
|
||||
),
|
||||
Text(
|
||||
'안녕하세요, $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)),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildInfoChip(Icons.badge_outlined, '프로필 관리'),
|
||||
_buildInfoChip(Icons.apartment, department),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ProfileInfoRow(label: '이름', value: profile.name),
|
||||
ProfileInfoRow(label: '이메일', value: profile.email),
|
||||
ProfileInfoRow(label: '전화번호', value: profile.phone),
|
||||
const Divider(height: 32),
|
||||
ProfileInfoRow(label: '소속', value: profile.department),
|
||||
ProfileInfoRow(label: '구분', value: profile.affiliationType),
|
||||
if (profile.companyCode.isNotEmpty)
|
||||
ProfileInfoRow(label: '회사코드', value: profile.companyCode),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('오류 발생: $err'),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(profileProvider.notifier).loadProfile(),
|
||||
child: const Text('재시도'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCard(Widget child) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: _border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReadOnlyTile(String label, String value) {
|
||||
final displayValue = value.isEmpty ? '-' : value;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(label),
|
||||
subtitle: Text(displayValue),
|
||||
trailing: Text(
|
||||
'읽기 전용',
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditableTile({
|
||||
required String field,
|
||||
required String label,
|
||||
required String value,
|
||||
required UserProfile profile,
|
||||
required bool isUpdating,
|
||||
required TextEditingController controller,
|
||||
}) {
|
||||
final isEditing = _editingField == field;
|
||||
final displayValue = value.isEmpty ? '-' : value;
|
||||
|
||||
if (!isEditing) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(label),
|
||||
subtitle: Text(displayValue),
|
||||
trailing: TextButton(
|
||||
onPressed: isUpdating ? null : () => _startEditing(field, profile),
|
||||
child: const Text('수정'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: label,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: isUpdating ? null : () => _saveField(profile),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneEditor(UserProfile profile, bool isUpdating) {
|
||||
final isEditing = _editingField == 'phone';
|
||||
final displayValue = profile.phone.isEmpty ? '-' : profile.phone;
|
||||
|
||||
if (!isEditing) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('전화번호'),
|
||||
subtitle: Text(displayValue),
|
||||
trailing: TextButton(
|
||||
onPressed: isUpdating ? null : () => _startEditing('phone', profile),
|
||||
child: const Text('수정'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('전화번호', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: '01012345678',
|
||||
suffixIcon: _isPhoneVerified
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: null,
|
||||
),
|
||||
enabled: !_isPhoneVerified,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (_isPhoneChanged && !_isPhoneVerified)
|
||||
ElevatedButton(
|
||||
onPressed: _isVerifying ? null : _sendCode,
|
||||
child: Text(_isCodeSent ? '재전송' : '인증요청'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isCodeSent && !_isPhoneVerified) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '인증번호 6자리',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isVerifying ? null : _verifyCode,
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (_isPhoneChanged && !_isPhoneVerified)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'휴대폰 번호를 변경하려면 SMS 인증이 필요합니다.',
|
||||
style: TextStyle(color: Colors.orange, fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: isUpdating ? null : () => _cancelEditing(profile),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: isUpdating ? null : () => _saveField(profile),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(UserProfile profile, bool isUpdating) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(profileProvider.notifier).loadProfile(),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: [
|
||||
_buildHeaderCard(profile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle('기본 정보', '계정 기본 정보를 관리합니다.'),
|
||||
const SizedBox(height: 12),
|
||||
_buildCard(
|
||||
Column(
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'name',
|
||||
label: '이름',
|
||||
value: profile.name,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
controller: _nameController!,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('이메일', profile.email),
|
||||
const Divider(height: 24),
|
||||
_buildPhoneEditor(profile, isUpdating),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle('조직 정보', '소속 및 구분 정보입니다.'),
|
||||
const SizedBox(height: 12),
|
||||
_buildCard(
|
||||
Column(
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'department',
|
||||
label: '소속',
|
||||
value: profile.department,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
controller: _departmentController!,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('구분', profile.affiliationType),
|
||||
if (profile.companyCode.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile('회사코드', profile.companyCode),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isUpdating || _isVerifying) ...[
|
||||
const SizedBox(height: 24),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileState = ref.watch(profileProvider);
|
||||
if (profileState.value != null) {
|
||||
_cachedProfile = profileState.value;
|
||||
}
|
||||
|
||||
final profile = profileState.value ?? _cachedProfile;
|
||||
if (profile == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('내 정보')),
|
||||
body: profileState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('정보를 불러올 수 없습니다.'),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(profileProvider.notifier).loadProfile(),
|
||||
child: const Text('재시도'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_ensureControllers(profile);
|
||||
|
||||
final isWide = MediaQuery.of(context).size.width >= sideMenuBreakpoint;
|
||||
final isUpdating = profileState.isLoading;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _subtle,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Baron 통합로그인',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold),
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: _surface,
|
||||
foregroundColor: Colors.black,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: '대시보드',
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner),
|
||||
tooltip: 'QR 스캔',
|
||||
onPressed: () => context.push('/scan'),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: '로그아웃',
|
||||
onPressed: _logout,
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: isWide ? null : Drawer(child: _buildSideMenu(context)),
|
||||
body: Row(
|
||||
children: [
|
||||
if (isWide)
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: _buildSideMenu(context),
|
||||
),
|
||||
Expanded(child: _buildContent(profile, isUpdating)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user