feat: 사용자 관리 API 연동 구현
- UserRemoteDataSource: 사용자 CRUD, 상태 변경, 비밀번호 변경, 중복 확인 API 구현 - UserService: DTO-Model 변환 로직 및 역할/전화번호 매핑 처리 - UserListController: ChangeNotifier 패턴 적용, 페이지네이션, 검색, 필터링 기능 추가 - UserFormController: API 연동, username 중복 확인 기능 추가 - user_form.dart: username/password 필드 추가 및 실시간 검증 - user_list_redesign.dart: Provider 패턴 적용, 무한 스크롤 구현 - equipment_out_form_controller.dart: 구문 오류 수정 - API 통합 진행률: 85% (사용자 관리 100% 완료)
This commit is contained in:
@@ -1,23 +1,41 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/user_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/services/user_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/models/user_phone_field.dart';
|
||||
|
||||
// 사용자 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class UserFormController {
|
||||
class UserFormController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
final UserService _userService = GetIt.instance<UserService>();
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
// 상태 변수
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _useApi = true; // Feature flag
|
||||
|
||||
// 폼 필드
|
||||
bool isEditMode = false;
|
||||
int? userId;
|
||||
String name = '';
|
||||
String username = ''; // 추가
|
||||
String password = ''; // 추가
|
||||
int? companyId;
|
||||
int? branchId;
|
||||
String role = UserRoles.member;
|
||||
String position = '';
|
||||
String email = '';
|
||||
|
||||
// username 중복 확인
|
||||
bool _isCheckingUsername = false;
|
||||
bool? _isUsernameAvailable;
|
||||
String? _lastCheckedUsername;
|
||||
Timer? _usernameCheckTimer;
|
||||
|
||||
// 전화번호 관련 상태
|
||||
final List<UserPhoneField> phoneFields = [];
|
||||
@@ -25,12 +43,27 @@ class UserFormController {
|
||||
|
||||
List<Company> companies = [];
|
||||
List<Branch> branches = [];
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get isCheckingUsername => _isCheckingUsername;
|
||||
bool? get isUsernameAvailable => _isUsernameAvailable;
|
||||
|
||||
UserFormController({required this.dataService, this.userId});
|
||||
UserFormController({required this.dataService, this.userId}) {
|
||||
isEditMode = userId != null;
|
||||
if (isEditMode) {
|
||||
loadUser();
|
||||
} else {
|
||||
addPhoneField();
|
||||
}
|
||||
loadCompanies();
|
||||
}
|
||||
|
||||
// 회사 목록 로드
|
||||
void loadCompanies() {
|
||||
companies = dataService.getAllCompanies();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 회사 ID에 따라 지점 목록 로드
|
||||
@@ -41,41 +74,63 @@ class UserFormController {
|
||||
if (branchId != null && !branches.any((b) => b.id == branchId)) {
|
||||
branchId = null;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 사용자 정보 로드 (수정 모드)
|
||||
void loadUser() {
|
||||
Future<void> loadUser() async {
|
||||
if (userId == null) return;
|
||||
final user = dataService.getUserById(userId!);
|
||||
if (user != null) {
|
||||
name = user.name;
|
||||
companyId = user.companyId;
|
||||
branchId = user.branchId;
|
||||
role = user.role;
|
||||
position = user.position ?? '';
|
||||
email = user.email ?? '';
|
||||
if (companyId != null) {
|
||||
loadBranches(companyId!);
|
||||
}
|
||||
phoneFields.clear();
|
||||
if (user.phoneNumbers.isNotEmpty) {
|
||||
for (var phone in user.phoneNumbers) {
|
||||
phoneFields.add(
|
||||
UserPhoneField(
|
||||
type: phone['type'] ?? '휴대폰',
|
||||
initialValue: phone['number'] ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
User? user;
|
||||
|
||||
if (_useApi) {
|
||||
user = await _userService.getUser(userId!);
|
||||
} else {
|
||||
addPhoneField();
|
||||
user = dataService.getUserById(userId!);
|
||||
}
|
||||
|
||||
if (user != null) {
|
||||
name = user.name;
|
||||
username = user.username ?? '';
|
||||
companyId = user.companyId;
|
||||
branchId = user.branchId;
|
||||
role = user.role;
|
||||
position = user.position ?? '';
|
||||
email = user.email ?? '';
|
||||
if (companyId != null) {
|
||||
loadBranches(companyId!);
|
||||
}
|
||||
phoneFields.clear();
|
||||
if (user.phoneNumbers.isNotEmpty) {
|
||||
for (var phone in user.phoneNumbers) {
|
||||
phoneFields.add(
|
||||
UserPhoneField(
|
||||
type: phone['type'] ?? '휴대폰',
|
||||
initialValue: phone['number'] ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
addPhoneField();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 전화번호 필드 추가
|
||||
void addPhoneField() {
|
||||
phoneFields.add(UserPhoneField(type: '휴대폰'));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 전화번호 필드 삭제
|
||||
@@ -83,64 +138,185 @@ class UserFormController {
|
||||
if (phoneFields.length > 1) {
|
||||
phoneFields[index].dispose();
|
||||
phoneFields.removeAt(index);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Username 중복 확인
|
||||
void checkUsernameAvailability(String value) {
|
||||
if (value.isEmpty || value == _lastCheckedUsername) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 디바운싱
|
||||
_usernameCheckTimer?.cancel();
|
||||
_usernameCheckTimer = Timer(const Duration(milliseconds: 500), () async {
|
||||
_isCheckingUsername = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
final isDuplicate = await _userService.checkDuplicateUsername(value);
|
||||
_isUsernameAvailable = !isDuplicate;
|
||||
} else {
|
||||
// Mock 데이터에서 중복 확인
|
||||
final users = dataService.getAllUsers();
|
||||
final exists = users.any((u) => u.username == value && u.id != userId);
|
||||
_isUsernameAvailable = !exists;
|
||||
}
|
||||
_lastCheckedUsername = value;
|
||||
} catch (e) {
|
||||
_isUsernameAvailable = null;
|
||||
} finally {
|
||||
_isCheckingUsername = false;
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 저장 (UI에서 호출)
|
||||
void saveUser(Function(String? error) onResult) {
|
||||
Future<void> saveUser(Function(String? error) onResult) async {
|
||||
if (formKey.currentState?.validate() != true) {
|
||||
onResult('폼 유효성 검사 실패');
|
||||
return;
|
||||
}
|
||||
formKey.currentState?.save();
|
||||
|
||||
if (companyId == null) {
|
||||
onResult('소속 회사를 선택해주세요');
|
||||
return;
|
||||
}
|
||||
// 전화번호 목록 준비 (UserPhoneField 기반)
|
||||
List<Map<String, String>> phoneNumbersList = [];
|
||||
for (var phoneField in phoneFields) {
|
||||
if (phoneField.number.isNotEmpty) {
|
||||
phoneNumbersList.add({
|
||||
'type': phoneField.type,
|
||||
'number': phoneField.number,
|
||||
});
|
||||
|
||||
// 신규 등록 시 username 중복 확인
|
||||
if (!isEditMode) {
|
||||
if (username.isEmpty) {
|
||||
onResult('사용자명을 입력해주세요');
|
||||
return;
|
||||
}
|
||||
if (_isUsernameAvailable == false) {
|
||||
onResult('이미 사용중인 사용자명입니다');
|
||||
return;
|
||||
}
|
||||
if (password.isEmpty) {
|
||||
onResult('비밀번호를 입력해주세요');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isEditMode && userId != null) {
|
||||
final user = dataService.getUserById(userId!);
|
||||
if (user != null) {
|
||||
final updatedUser = User(
|
||||
id: user.id,
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
name: name,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phoneNumbers: phoneNumbersList,
|
||||
);
|
||||
dataService.updateUser(updatedUser);
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// 전화번호 목록 준비
|
||||
String? phoneNumber;
|
||||
for (var phoneField in phoneFields) {
|
||||
if (phoneField.number.isNotEmpty) {
|
||||
phoneNumber = phoneField.number;
|
||||
break; // API는 단일 전화번호만 지원
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final newUser = User(
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
name: name,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phoneNumbers: phoneNumbersList,
|
||||
);
|
||||
dataService.addUser(newUser);
|
||||
|
||||
if (_useApi) {
|
||||
if (isEditMode && userId != null) {
|
||||
// 사용자 수정
|
||||
await _userService.updateUser(
|
||||
userId!,
|
||||
name: name,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phone: phoneNumber,
|
||||
companyId: companyId,
|
||||
branchId: branchId,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
password: password.isNotEmpty ? password : null,
|
||||
);
|
||||
} else {
|
||||
// 사용자 생성
|
||||
await _userService.createUser(
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
name: name,
|
||||
role: role,
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
phone: phoneNumber,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
List<Map<String, String>> phoneNumbersList = [];
|
||||
for (var phoneField in phoneFields) {
|
||||
if (phoneField.number.isNotEmpty) {
|
||||
phoneNumbersList.add({
|
||||
'type': phoneField.type,
|
||||
'number': phoneField.number,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditMode && userId != null) {
|
||||
final user = dataService.getUserById(userId!);
|
||||
if (user != null) {
|
||||
final updatedUser = User(
|
||||
id: user.id,
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
name: name,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phoneNumbers: phoneNumbersList,
|
||||
username: username.isNotEmpty ? username : null,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
dataService.updateUser(updatedUser);
|
||||
}
|
||||
} else {
|
||||
final newUser = User(
|
||||
companyId: companyId!,
|
||||
branchId: branchId,
|
||||
name: name,
|
||||
role: role,
|
||||
position: position.isNotEmpty ? position : null,
|
||||
email: email.isNotEmpty ? email : null,
|
||||
phoneNumbers: phoneNumbersList,
|
||||
username: username,
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
dataService.addUser(newUser);
|
||||
}
|
||||
}
|
||||
|
||||
onResult(null);
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
onResult(_error);
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
onResult(null);
|
||||
}
|
||||
|
||||
// 컨트롤러 해제
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameCheckTimer?.cancel();
|
||||
for (var phoneField in phoneFields) {
|
||||
phoneField.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// API/Mock 모드 전환
|
||||
void toggleApiMode() {
|
||||
_useApi = !_useApi;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/user_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/services/user_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/utils/user_utils.dart';
|
||||
|
||||
/// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class UserListController extends ChangeNotifier {
|
||||
final MockDataService dataService;
|
||||
List<User> users = [];
|
||||
final UserService _userService = GetIt.instance<UserService>();
|
||||
|
||||
// 상태 변수
|
||||
List<User> _users = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
bool _useApi = true; // Feature flag
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
bool _hasMoreData = true;
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
// 검색/필터
|
||||
String _searchQuery = '';
|
||||
int? _filterCompanyId;
|
||||
String? _filterRole;
|
||||
bool? _filterIsActive;
|
||||
|
||||
// Getters
|
||||
List<User> get users => _users;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingMore => _isLoadingMore;
|
||||
String? get error => _error;
|
||||
bool get hasMoreData => _hasMoreData;
|
||||
String get searchQuery => _searchQuery;
|
||||
int? get filterCompanyId => _filterCompanyId;
|
||||
String? get filterRole => _filterRole;
|
||||
bool? get filterIsActive => _filterIsActive;
|
||||
|
||||
UserListController({required this.dataService});
|
||||
|
||||
/// 사용자 목록 데이터 로드
|
||||
void loadUsers() {
|
||||
users = dataService.getAllUsers();
|
||||
/// 사용자 목록 초기 로드
|
||||
Future<void> loadUsers({bool refresh = false}) async {
|
||||
if (refresh) {
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
_users.clear();
|
||||
}
|
||||
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (_useApi) {
|
||||
final newUsers = await _userService.getUsers(
|
||||
page: _currentPage,
|
||||
perPage: _perPage,
|
||||
isActive: _filterIsActive,
|
||||
companyId: _filterCompanyId,
|
||||
role: _filterRole,
|
||||
);
|
||||
|
||||
if (newUsers.isEmpty || newUsers.length < _perPage) {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
|
||||
if (_currentPage == 1) {
|
||||
_users = newUsers;
|
||||
} else {
|
||||
_users.addAll(newUsers);
|
||||
}
|
||||
|
||||
_currentPage++;
|
||||
} else {
|
||||
// Mock 데이터 사용
|
||||
var allUsers = dataService.getAllUsers();
|
||||
|
||||
// 필터 적용
|
||||
if (_filterCompanyId != null) {
|
||||
allUsers = allUsers.where((u) => u.companyId == _filterCompanyId).toList();
|
||||
}
|
||||
if (_filterRole != null) {
|
||||
allUsers = allUsers.where((u) => u.role == _filterRole).toList();
|
||||
}
|
||||
if (_filterIsActive != null) {
|
||||
allUsers = allUsers.where((u) => u.isActive == _filterIsActive).toList();
|
||||
}
|
||||
|
||||
// 검색 적용
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
allUsers = allUsers.where((u) =>
|
||||
u.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
(u.email?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false) ||
|
||||
(u.username?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false)
|
||||
).toList();
|
||||
}
|
||||
|
||||
_users = allUsers;
|
||||
_hasMoreData = false;
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 다음 페이지 로드 (무한 스크롤용)
|
||||
Future<void> loadMore() async {
|
||||
if (!_hasMoreData || _isLoadingMore || _isLoading) return;
|
||||
|
||||
_isLoadingMore = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await loadUsers();
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색 쿼리 설정
|
||||
void setSearchQuery(String query) {
|
||||
_searchQuery = query;
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
int? companyId,
|
||||
String? role,
|
||||
bool? isActive,
|
||||
}) {
|
||||
_filterCompanyId = companyId;
|
||||
_filterRole = role;
|
||||
_filterIsActive = isActive;
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_filterCompanyId = null;
|
||||
_filterRole = null;
|
||||
_filterIsActive = null;
|
||||
_searchQuery = '';
|
||||
_currentPage = 1;
|
||||
_hasMoreData = true;
|
||||
loadUsers(refresh: true);
|
||||
}
|
||||
|
||||
/// 사용자 삭제
|
||||
void deleteUser(int id, VoidCallback onDeleted) {
|
||||
dataService.deleteUser(id);
|
||||
loadUsers();
|
||||
onDeleted();
|
||||
Future<void> deleteUser(int id, VoidCallback onDeleted, Function(String) onError) async {
|
||||
try {
|
||||
if (_useApi) {
|
||||
await _userService.deleteUser(id);
|
||||
} else {
|
||||
dataService.deleteUser(id);
|
||||
}
|
||||
|
||||
// 목록에서 삭제된 사용자 제거
|
||||
_users.removeWhere((user) => user.id == id);
|
||||
notifyListeners();
|
||||
|
||||
onDeleted();
|
||||
} catch (e) {
|
||||
onError('사용자 삭제 실패: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자 상태 변경 (활성/비활성)
|
||||
Future<void> changeUserStatus(int id, bool isActive, Function(String) onError) async {
|
||||
try {
|
||||
if (_useApi) {
|
||||
final updatedUser = await _userService.changeUserStatus(id, isActive);
|
||||
// 목록에서 해당 사용자 업데이트
|
||||
final index = _users.indexWhere((u) => u.id == id);
|
||||
if (index != -1) {
|
||||
_users[index] = updatedUser;
|
||||
notifyListeners();
|
||||
}
|
||||
} else {
|
||||
// Mock 데이터에서는 상태 변경 지원 안함
|
||||
onError('Mock 데이터에서는 상태 변경을 지원하지 않습니다');
|
||||
}
|
||||
} catch (e) {
|
||||
onError('상태 변경 실패: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 권한명 반환 함수는 user_utils.dart의 getRoleName을 사용
|
||||
@@ -39,4 +214,10 @@ class UserListController extends ChangeNotifier {
|
||||
);
|
||||
return branch.name;
|
||||
}
|
||||
|
||||
/// API/Mock 모드 전환
|
||||
void toggleApiMode() {
|
||||
_useApi = !_useApi;
|
||||
loadUsers(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user