feat: 사용자 관리 시스템 백엔드 API 호환성 대폭 개선
- UserRemoteDataSource: API v0.2.1 스펙 완전 대응 • 응답 형식 통일 (success: true 구조) • 페이지네이션 처리 개선 • 에러 핸들링 강화 • 불필요한 파라미터 제거 (includeInactive 등) - UserDto 모델 현대화: • 서버 응답 구조와 100% 일치 • 도메인 모델 변환 메서드 추가 • Freezed 불변성 패턴 완성 - User 도메인 모델 신규 구현: • Clean Architecture 원칙 준수 • UserRole enum 타입 안전성 강화 • 비즈니스 로직 캡슐화 - 사용자 관련 UseCase 리팩토링: • Repository 패턴 완전 적용 • Either<Failure, Success> 에러 처리 • 의존성 주입 최적화 - UI 컨트롤러 및 화면 개선: • API 응답 변경사항 반영 • 사용자 권한 표시 정확성 향상 • 폼 검증 로직 강화 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ class UserList extends StatefulWidget {
|
||||
|
||||
class _UserListState extends State<UserList> {
|
||||
// MockDataService 제거 - 실제 API 사용
|
||||
late UserListController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
@@ -26,9 +27,9 @@ class _UserListState extends State<UserList> {
|
||||
super.initState();
|
||||
|
||||
// 초기 데이터 로드
|
||||
_controller = UserListController();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final controller = context.read<UserListController>();
|
||||
controller.initialize(pageSize: 10); // 통일된 초기화 방식
|
||||
_controller.initialize(pageSize: 10); // 통일된 초기화 방식
|
||||
});
|
||||
|
||||
// 검색 디바운싱
|
||||
@@ -39,6 +40,7 @@ class _UserListState extends State<UserList> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -49,7 +51,7 @@ class _UserListState extends State<UserList> {
|
||||
void _onSearchChanged(String query) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 300), () {
|
||||
context.read<UserListController>().setSearchQuery(query); // Controller가 페이지 리셋 처리
|
||||
_controller.setSearchQuery(query); // Controller가 페이지 리셋 처리
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,20 +66,21 @@ class _UserListState extends State<UserList> {
|
||||
return isActive ? Colors.green : Colors.red;
|
||||
}
|
||||
|
||||
/// 사용자 권한 표시 배지
|
||||
Widget _buildUserRoleBadge(String role) {
|
||||
final roleName = getRoleName(role);
|
||||
/// 사용자 권한 표시 배지 (새 UserRole 시스템)
|
||||
Widget _buildUserRoleBadge(UserRole role) {
|
||||
final roleName = role.displayName;
|
||||
ShadcnBadgeVariant variant;
|
||||
|
||||
switch (role) {
|
||||
case 'S':
|
||||
case UserRole.admin:
|
||||
variant = ShadcnBadgeVariant.destructive;
|
||||
break;
|
||||
case 'M':
|
||||
case UserRole.manager:
|
||||
variant = ShadcnBadgeVariant.primary;
|
||||
break;
|
||||
default:
|
||||
variant = ShadcnBadgeVariant.outline;
|
||||
case UserRole.staff:
|
||||
variant = ShadcnBadgeVariant.secondary;
|
||||
break;
|
||||
}
|
||||
|
||||
return ShadcnBadge(
|
||||
@@ -91,7 +94,7 @@ class _UserListState extends State<UserList> {
|
||||
void _navigateToAdd() async {
|
||||
final result = await Navigator.pushNamed(context, Routes.userAdd);
|
||||
if (result == true && mounted) {
|
||||
context.read<UserListController>().loadUsers(refresh: true);
|
||||
_controller.loadUsers(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +106,7 @@ class _UserListState extends State<UserList> {
|
||||
arguments: userId,
|
||||
);
|
||||
if (result == true && mounted) {
|
||||
context.read<UserListController>().loadUsers(refresh: true);
|
||||
_controller.loadUsers(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +126,7 @@ class _UserListState extends State<UserList> {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await context.read<UserListController>().deleteUser(userId);
|
||||
await _controller.deleteUser(userId);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('사용자가 삭제되었습니다')),
|
||||
);
|
||||
@@ -154,7 +157,7 @@ class _UserListState extends State<UserList> {
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await context.read<UserListController>().changeUserStatus(user, newStatus);
|
||||
await _controller.changeUserStatus(user, newStatus);
|
||||
},
|
||||
child: Text(statusText),
|
||||
),
|
||||
@@ -165,17 +168,16 @@ class _UserListState extends State<UserList> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => UserListController(),
|
||||
child: Consumer<UserListController>(
|
||||
builder: (context, controller, child) {
|
||||
if (controller.isLoading && controller.users.isEmpty) {
|
||||
return ListenableBuilder(
|
||||
listenable: _controller,
|
||||
builder: (context, child) {
|
||||
if (_controller.isLoading && _controller.users.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.error != null && controller.users.isEmpty) {
|
||||
if (_controller.error != null && _controller.users.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -188,14 +190,14 @@ class _UserListState extends State<UserList> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
controller.error!,
|
||||
_controller.error!,
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ShadcnButton(
|
||||
text: '다시 시도',
|
||||
onPressed: () => controller.loadUsers(refresh: true),
|
||||
onPressed: () => _controller.loadUsers(refresh: true),
|
||||
variant: ShadcnButtonVariant.primary,
|
||||
),
|
||||
],
|
||||
@@ -204,8 +206,8 @@ class _UserListState extends State<UserList> {
|
||||
}
|
||||
|
||||
// Controller가 이미 페이징된 데이터를 제공
|
||||
final List<User> pagedUsers = controller.users; // 이미 페이징됨
|
||||
final int totalUsers = controller.total; // 실제 전체 개수
|
||||
final List<User> pagedUsers = _controller.users; // 이미 페이징됨
|
||||
final int totalUsers = _controller.total; // 실제 전체 개수
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
@@ -234,7 +236,7 @@ class _UserListState extends State<UserList> {
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
controller.setSearchQuery('');
|
||||
_controller.setSearchQuery('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
@@ -253,16 +255,16 @@ class _UserListState extends State<UserList> {
|
||||
children: [
|
||||
// 상태 필터
|
||||
ShadcnButton(
|
||||
text: controller.filterIsActive == null
|
||||
text: _controller.filterIsActive == null
|
||||
? '모든 상태'
|
||||
: controller.filterIsActive!
|
||||
: _controller.filterIsActive!
|
||||
? '활성 사용자'
|
||||
: '비활성 사용자',
|
||||
onPressed: () {
|
||||
controller.setFilters(
|
||||
isActive: controller.filterIsActive == null
|
||||
_controller.setFilters(
|
||||
isActive: _controller.filterIsActive == null
|
||||
? true
|
||||
: controller.filterIsActive!
|
||||
: _controller.filterIsActive!
|
||||
? false
|
||||
: null,
|
||||
);
|
||||
@@ -271,18 +273,18 @@ class _UserListState extends State<UserList> {
|
||||
icon: const Icon(Icons.filter_list),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
// 권한 필터
|
||||
// 권한 필터 (새 UserRole 시스템)
|
||||
PopupMenuButton<String?>(
|
||||
child: ShadcnButton(
|
||||
text: controller.filterRole == null
|
||||
text: _controller.filterRole == null
|
||||
? '모든 권한'
|
||||
: getRoleName(controller.filterRole!),
|
||||
: _controller.filterRole!.displayName,
|
||||
onPressed: null,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.person),
|
||||
),
|
||||
onSelected: (role) {
|
||||
controller.setFilters(role: role);
|
||||
onSelected: (roleString) {
|
||||
_controller.setFilters(roleString: roleString);
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
@@ -290,12 +292,16 @@ class _UserListState extends State<UserList> {
|
||||
child: Text('모든 권한'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'S',
|
||||
value: 'admin',
|
||||
child: Text('관리자'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'M',
|
||||
child: Text('맴버'),
|
||||
value: 'manager',
|
||||
child: Text('매니저'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'staff',
|
||||
child: Text('직원'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -304,9 +310,9 @@ class _UserListState extends State<UserList> {
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: controller.includeInactive,
|
||||
value: _controller.includeInactive,
|
||||
onChanged: (_) => setState(() {
|
||||
controller.toggleIncludeInactive();
|
||||
_controller.toggleIncludeInactive();
|
||||
}),
|
||||
),
|
||||
const Text('비활성 포함'),
|
||||
@@ -314,14 +320,14 @@ class _UserListState extends State<UserList> {
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
// 필터 초기화
|
||||
if (controller.searchQuery.isNotEmpty ||
|
||||
controller.filterIsActive != null ||
|
||||
controller.filterRole != null)
|
||||
if (_controller.searchQuery.isNotEmpty ||
|
||||
_controller.filterIsActive != null ||
|
||||
_controller.filterRole != null)
|
||||
ShadcnButton(
|
||||
text: '필터 초기화',
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
controller.clearFilters();
|
||||
_controller.clearFilters();
|
||||
},
|
||||
variant: ShadcnButtonVariant.ghost,
|
||||
icon: const Icon(Icons.clear_all),
|
||||
@@ -340,14 +346,14 @@ class _UserListState extends State<UserList> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'총 ${controller.users.length}명 사용자',
|
||||
'총 ${_controller.users.length}명 사용자',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '새로고침',
|
||||
onPressed: () => controller.loadUsers(refresh: true),
|
||||
onPressed: () => _controller.loadUsers(refresh: true),
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
@@ -393,8 +399,8 @@ class _UserListState extends State<UserList> {
|
||||
const SizedBox(width: 50, child: Text('번호', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const Expanded(flex: 2, child: Text('사용자명', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const Expanded(flex: 2, child: Text('이메일', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const Expanded(flex: 2, child: Text('회사명', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const Expanded(flex: 2, child: Text('지점명', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const Expanded(flex: 2, child: Text('전화번호', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const Expanded(flex: 2, child: Text('생성일', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const SizedBox(width: 100, child: Text('권한', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const SizedBox(width: 80, child: Text('상태', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
const SizedBox(width: 120, child: Text('관리', style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
@@ -403,14 +409,14 @@ class _UserListState extends State<UserList> {
|
||||
),
|
||||
|
||||
// 테이블 데이터
|
||||
if (controller.users.isEmpty)
|
||||
if (_controller.users.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
|
||||
child: Center(
|
||||
child: Text(
|
||||
controller.searchQuery.isNotEmpty ||
|
||||
controller.filterIsActive != null ||
|
||||
controller.filterRole != null
|
||||
_controller.searchQuery.isNotEmpty ||
|
||||
_controller.filterIsActive != null ||
|
||||
_controller.filterRole != null
|
||||
? '검색 결과가 없습니다.'
|
||||
: '등록된 사용자가 없습니다.',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
@@ -419,7 +425,7 @@ class _UserListState extends State<UserList> {
|
||||
)
|
||||
else
|
||||
...pagedUsers.asMap().entries.map((entry) {
|
||||
final int index = ((controller.currentPage - 1) * controller.pageSize) + entry.key;
|
||||
final int index = ((_controller.currentPage - 1) * _controller.pageSize) + entry.key;
|
||||
final User user = entry.value;
|
||||
|
||||
return Container(
|
||||
@@ -450,13 +456,12 @@ class _UserListState extends State<UserList> {
|
||||
user.name,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
if (user.username != null)
|
||||
Text(
|
||||
'@${user.username}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
),
|
||||
Text(
|
||||
'@${user.username}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -464,23 +469,25 @@ class _UserListState extends State<UserList> {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
user.email ?? '미등록',
|
||||
user.email,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 회사명
|
||||
// 전화번호
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_getCompanyName(user.companyId),
|
||||
user.phone ?? '미등록',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 지점명
|
||||
// 생성일
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
controller.getBranchName(user.branchId),
|
||||
user.createdAt != null
|
||||
? '${user.createdAt!.year}-${user.createdAt!.month.toString().padLeft(2, '0')}-${user.createdAt!.day.toString().padLeft(2, '0')}'
|
||||
: '미설정',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
@@ -581,25 +588,20 @@ class _UserListState extends State<UserList> {
|
||||
),
|
||||
|
||||
// 페이지네이션 컴포넌트 (Controller 상태 사용)
|
||||
if (controller.total > controller.pageSize)
|
||||
if (_controller.total > _controller.pageSize)
|
||||
Pagination(
|
||||
totalCount: controller.total,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
totalCount: _controller.total,
|
||||
currentPage: _controller.currentPage,
|
||||
pageSize: _controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
// 다음 페이지 로드
|
||||
if (page > controller.currentPage) {
|
||||
controller.loadNextPage();
|
||||
} else if (page == 1) {
|
||||
controller.refresh();
|
||||
}
|
||||
// 특정 페이지로 이동 (데이터 교체)
|
||||
_controller.goToPage(page);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user