1
0
forked from baron/baron-sso

userfront로 리펙토링 완료

This commit is contained in:
Lectom C Han
2026-01-28 08:28:25 +09:00
parent 6d88c81217
commit 1aaa772907
154 changed files with 339 additions and 314 deletions

View File

@@ -0,0 +1,251 @@
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()),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,71 @@
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';
import '../widgets/profile_info_row.dart';
class ProfilePage extends ConsumerWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// profileState is AsyncValue<UserProfile?>
final profileState = ref.watch(profileProvider);
return Scaffold(
appBar: AppBar(
title: const Text('내 정보'),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => context.push('/profile/edit'),
),
],
),
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),
children: [
const Center(
child: CircleAvatar(
radius: 40,
child: Icon(Icons.person, size: 40),
),
),
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('재시도'),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
class ProfileInfoRow extends StatelessWidget {
final String label;
final String value;
const ProfileInfoRow({
super.key,
required this.label,
required this.value,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
),
Expanded(
child: Text(
value.isEmpty ? '-' : value,
style: const TextStyle(fontSize: 16),
),
),
],
),
);
}
}