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:
@@ -590,7 +590,7 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
} else {
|
||||
onSuccess('${successCompanies.join(", ")} 회사로 다중 장비 출고 처리 완료');
|
||||
}
|
||||
} else if (selectedEquipmentInId != null) {
|
||||
} else if (selectedEquipmentInId != null) {
|
||||
final equipment = Equipment(
|
||||
manufacturer: manufacturer,
|
||||
name: name,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/user_model.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
@@ -21,116 +22,288 @@ class UserFormScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UserFormScreenState extends State<UserFormScreen> {
|
||||
late final UserFormController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = UserFormController(
|
||||
dataService: MockDataService(),
|
||||
userId: widget.userId,
|
||||
);
|
||||
_controller.isEditMode = widget.userId != null;
|
||||
_controller.loadCompanies();
|
||||
if (_controller.isEditMode) {
|
||||
_controller.loadUser();
|
||||
} else if (_controller.phoneFields.isEmpty) {
|
||||
_controller.addPhoneField();
|
||||
}
|
||||
}
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
final TextEditingController _confirmPasswordController = TextEditingController();
|
||||
bool _showPassword = false;
|
||||
bool _showConfirmPassword = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(_controller.isEditMode ? '사용자 수정' : '사용자 등록')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 이름
|
||||
_buildTextField(
|
||||
label: '이름',
|
||||
initialValue: _controller.name,
|
||||
hintText: '사용자 이름을 입력하세요',
|
||||
validator: (value) => validateRequired(value, '이름'),
|
||||
onSaved: (value) => _controller.name = value!,
|
||||
),
|
||||
// 직급
|
||||
_buildTextField(
|
||||
label: '직급',
|
||||
initialValue: _controller.position,
|
||||
hintText: '직급을 입력하세요',
|
||||
onSaved: (value) => _controller.position = value ?? '',
|
||||
),
|
||||
// 소속 회사/지점
|
||||
CompanyBranchDropdown(
|
||||
companies: _controller.companies,
|
||||
selectedCompanyId: _controller.companyId,
|
||||
selectedBranchId: _controller.branchId,
|
||||
branches: _controller.branches,
|
||||
onCompanyChanged: (value) {
|
||||
setState(() {
|
||||
_controller.companyId = value;
|
||||
_controller.branchId = null;
|
||||
if (value != null) {
|
||||
_controller.loadBranches(value);
|
||||
} else {
|
||||
_controller.branches = [];
|
||||
}
|
||||
});
|
||||
},
|
||||
onBranchChanged: (value) {
|
||||
setState(() {
|
||||
_controller.branchId = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
// 이메일
|
||||
_buildTextField(
|
||||
label: '이메일',
|
||||
initialValue: _controller.email,
|
||||
hintText: '이메일을 입력하세요',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return validateEmail(value);
|
||||
},
|
||||
onSaved: (value) => _controller.email = value ?? '',
|
||||
),
|
||||
// 전화번호
|
||||
_buildPhoneFieldsSection(),
|
||||
// 권한
|
||||
_buildRoleRadio(),
|
||||
const SizedBox(height: 24),
|
||||
// 저장 버튼
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _onSaveUser,
|
||||
style: AppThemeTailwind.primaryButtonStyle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
_controller.isEditMode ? '수정하기' : '등록하기',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => UserFormController(
|
||||
dataService: MockDataService(),
|
||||
userId: widget.userId,
|
||||
),
|
||||
child: Consumer<UserFormController>(
|
||||
builder: (context, controller, child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(controller.isEditMode ? '사용자 수정' : '사용자 등록'),
|
||||
),
|
||||
body: controller.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 이름
|
||||
_buildTextField(
|
||||
label: '이름',
|
||||
initialValue: controller.name,
|
||||
hintText: '사용자 이름을 입력하세요',
|
||||
validator: (value) => validateRequired(value, '이름'),
|
||||
onSaved: (value) => controller.name = value!,
|
||||
),
|
||||
|
||||
// 사용자명 (신규 등록 시만)
|
||||
if (!controller.isEditMode) ...[
|
||||
_buildTextField(
|
||||
label: '사용자명',
|
||||
initialValue: controller.username,
|
||||
hintText: '로그인에 사용할 사용자명',
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '사용자명을 입력해주세요';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return '사용자명은 3자 이상이어야 합니다';
|
||||
}
|
||||
if (controller.isUsernameAvailable == false) {
|
||||
return '이미 사용 중인 사용자명입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
controller.username = value;
|
||||
controller.checkUsernameAvailability(value);
|
||||
},
|
||||
onSaved: (value) => controller.username = value!,
|
||||
suffixIcon: controller.isCheckingUsername
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: controller.isUsernameAvailable != null
|
||||
? Icon(
|
||||
controller.isUsernameAvailable!
|
||||
? Icons.check_circle
|
||||
: Icons.cancel,
|
||||
color: controller.isUsernameAvailable!
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
||||
// 비밀번호
|
||||
_buildPasswordField(
|
||||
label: '비밀번호',
|
||||
controller: _passwordController,
|
||||
hintText: '비밀번호를 입력하세요',
|
||||
obscureText: !_showPassword,
|
||||
onToggleVisibility: () {
|
||||
setState(() {
|
||||
_showPassword = !_showPassword;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '비밀번호를 입력해주세요';
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return '비밀번호는 6자 이상이어야 합니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) => controller.password = value!,
|
||||
),
|
||||
|
||||
// 비밀번호 확인
|
||||
_buildPasswordField(
|
||||
label: '비밀번호 확인',
|
||||
controller: _confirmPasswordController,
|
||||
hintText: '비밀번호를 다시 입력하세요',
|
||||
obscureText: !_showConfirmPassword,
|
||||
onToggleVisibility: () {
|
||||
setState(() {
|
||||
_showConfirmPassword = !_showConfirmPassword;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '비밀번호를 다시 입력해주세요';
|
||||
}
|
||||
if (value != _passwordController.text) {
|
||||
return '비밀번호가 일치하지 않습니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
// 수정 모드에서 비밀번호 변경 (선택사항)
|
||||
if (controller.isEditMode) ...[
|
||||
ExpansionTile(
|
||||
title: const Text('비밀번호 변경'),
|
||||
children: [
|
||||
_buildPasswordField(
|
||||
label: '새 비밀번호',
|
||||
controller: _passwordController,
|
||||
hintText: '변경할 경우만 입력하세요',
|
||||
obscureText: !_showPassword,
|
||||
onToggleVisibility: () {
|
||||
setState(() {
|
||||
_showPassword = !_showPassword;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty && value.length < 6) {
|
||||
return '비밀번호는 6자 이상이어야 합니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) => controller.password = value ?? '',
|
||||
),
|
||||
|
||||
_buildPasswordField(
|
||||
label: '새 비밀번호 확인',
|
||||
controller: _confirmPasswordController,
|
||||
hintText: '비밀번호를 다시 입력하세요',
|
||||
obscureText: !_showConfirmPassword,
|
||||
onToggleVisibility: () {
|
||||
setState(() {
|
||||
_showConfirmPassword = !_showConfirmPassword;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (_passwordController.text.isNotEmpty && value != _passwordController.text) {
|
||||
return '비밀번호가 일치하지 않습니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
// 직급
|
||||
_buildTextField(
|
||||
label: '직급',
|
||||
initialValue: controller.position,
|
||||
hintText: '직급을 입력하세요',
|
||||
onSaved: (value) => controller.position = value ?? '',
|
||||
),
|
||||
|
||||
// 소속 회사/지점
|
||||
CompanyBranchDropdown(
|
||||
companies: controller.companies,
|
||||
selectedCompanyId: controller.companyId,
|
||||
selectedBranchId: controller.branchId,
|
||||
branches: controller.branches,
|
||||
onCompanyChanged: (value) {
|
||||
controller.companyId = value;
|
||||
controller.branchId = null;
|
||||
if (value != null) {
|
||||
controller.loadBranches(value);
|
||||
} else {
|
||||
controller.branches = [];
|
||||
}
|
||||
},
|
||||
onBranchChanged: (value) {
|
||||
controller.branchId = value;
|
||||
},
|
||||
),
|
||||
|
||||
// 이메일
|
||||
_buildTextField(
|
||||
label: '이메일',
|
||||
initialValue: controller.email,
|
||||
hintText: '이메일을 입력하세요',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return validateEmail(value);
|
||||
},
|
||||
onSaved: (value) => controller.email = value ?? '',
|
||||
),
|
||||
// 전화번호
|
||||
_buildPhoneFieldsSection(controller),
|
||||
// 권한
|
||||
_buildRoleRadio(controller),
|
||||
const SizedBox(height: 24),
|
||||
// 오류 메시지 표시
|
||||
if (controller.error != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.error!,
|
||||
style: TextStyle(color: Colors.red.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 저장 버튼
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: () => _onSaveUser(controller),
|
||||
style: AppThemeTailwind.primaryButtonStyle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: controller.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
controller.isEditMode ? '수정하기' : '등록하기',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -144,6 +317,8 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
String? Function(String?)? validator,
|
||||
void Function(String?)? onSaved,
|
||||
void Function(String)? onChanged,
|
||||
Widget? suffixIcon,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
@@ -154,25 +329,66 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
const SizedBox(height: 4),
|
||||
TextFormField(
|
||||
initialValue: initialValue,
|
||||
decoration: InputDecoration(hintText: hintText),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
suffixIcon: suffixIcon,
|
||||
),
|
||||
keyboardType: keyboardType,
|
||||
inputFormatters: inputFormatters,
|
||||
validator: validator,
|
||||
onSaved: onSaved,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 비밀번호 필드 위젯
|
||||
Widget _buildPasswordField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
required String hintText,
|
||||
required bool obscureText,
|
||||
required VoidCallback onToggleVisibility,
|
||||
String? Function(String?)? validator,
|
||||
void Function(String?)? onSaved,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
obscureText ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: onToggleVisibility,
|
||||
),
|
||||
),
|
||||
validator: validator,
|
||||
onSaved: onSaved,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 전화번호 입력 필드 섹션 위젯 (UserPhoneField 기반)
|
||||
Widget _buildPhoneFieldsSection() {
|
||||
Widget _buildPhoneFieldsSection(UserFormController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('전화번호', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
..._controller.phoneFields.asMap().entries.map((entry) {
|
||||
...controller.phoneFields.asMap().entries.map((entry) {
|
||||
final i = entry.key;
|
||||
final phoneField = entry.value;
|
||||
return Row(
|
||||
@@ -180,17 +396,11 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
// 종류 드롭다운
|
||||
DropdownButton<String>(
|
||||
value: phoneField.type,
|
||||
items:
|
||||
_controller.phoneTypes
|
||||
.map(
|
||||
(type) =>
|
||||
DropdownMenuItem(value: type, child: Text(type)),
|
||||
)
|
||||
.toList(),
|
||||
items: controller.phoneTypes
|
||||
.map((type) => DropdownMenuItem(value: type, child: Text(type)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
phoneField.type = value!;
|
||||
});
|
||||
phoneField.type = value!;
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -205,14 +415,9 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle, color: Colors.red),
|
||||
onPressed:
|
||||
_controller.phoneFields.length > 1
|
||||
? () {
|
||||
setState(() {
|
||||
_controller.removePhoneField(i);
|
||||
});
|
||||
}
|
||||
: null,
|
||||
onPressed: controller.phoneFields.length > 1
|
||||
? () => controller.removePhoneField(i)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -221,11 +426,7 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_controller.addPhoneField();
|
||||
});
|
||||
},
|
||||
onPressed: () => controller.addPhoneField(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('전화번호 추가'),
|
||||
),
|
||||
@@ -235,7 +436,7 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
}
|
||||
|
||||
// 권한(관리등급) 라디오 위젯
|
||||
Widget _buildRoleRadio() {
|
||||
Widget _buildRoleRadio(UserFormController controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Column(
|
||||
@@ -249,11 +450,9 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
child: RadioListTile<String>(
|
||||
title: const Text('관리자'),
|
||||
value: UserRoles.admin,
|
||||
groupValue: _controller.role,
|
||||
groupValue: controller.role,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.role = value!;
|
||||
});
|
||||
controller.role = value!;
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -261,11 +460,9 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
child: RadioListTile<String>(
|
||||
title: const Text('일반 사용자'),
|
||||
value: UserRoles.member,
|
||||
groupValue: _controller.role,
|
||||
groupValue: controller.role,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.role = value!;
|
||||
});
|
||||
controller.role = value!;
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -277,17 +474,24 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
}
|
||||
|
||||
// 저장 버튼 클릭 시 사용자 저장
|
||||
void _onSaveUser() {
|
||||
setState(() {
|
||||
_controller.saveUser((error) {
|
||||
if (error != null) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(error)));
|
||||
} else {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
});
|
||||
void _onSaveUser(UserFormController controller) async {
|
||||
await controller.saveUser((error) {
|
||||
if (error != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(controller.isEditMode ? '사용자 정보가 수정되었습니다' : '사용자가 등록되었습니다'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:superport/models/user_model.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/user/controllers/user_list_controller.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/user_utils.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 사용자 관리 화면
|
||||
class UserListRedesign extends StatefulWidget {
|
||||
@@ -15,28 +18,49 @@ class UserListRedesign extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UserListRedesignState extends State<UserListRedesign> {
|
||||
late final UserListController _controller;
|
||||
final MockDataService _dataService = MockDataService();
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = UserListController(dataService: _dataService);
|
||||
_controller.loadUsers();
|
||||
_controller.addListener(_refresh);
|
||||
|
||||
// 초기 데이터 로드
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<UserListController>().loadUsers();
|
||||
});
|
||||
|
||||
// 무한 스크롤 설정
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
// 검색 디바운싱
|
||||
_searchController.addListener(() {
|
||||
_onSearchChanged(_searchController.text);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_refresh);
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 상태 갱신용 setState 래퍼
|
||||
void _refresh() {
|
||||
setState(() {});
|
||||
|
||||
/// 스크롤 이벤트 처리
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
|
||||
context.read<UserListController>().loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색어 변경 처리 (디바운싱)
|
||||
Timer? _debounce;
|
||||
void _onSearchChanged(String query) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 300), () {
|
||||
context.read<UserListController>().setSearchQuery(query);
|
||||
});
|
||||
}
|
||||
|
||||
/// 회사명 반환 함수
|
||||
@@ -44,36 +68,40 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
final company = _dataService.getCompanyById(companyId);
|
||||
return company?.name ?? '-';
|
||||
}
|
||||
|
||||
/// 상태별 색상 반환
|
||||
Color _getStatusColor(bool isActive) {
|
||||
return isActive ? Colors.green : Colors.red;
|
||||
}
|
||||
|
||||
/// 사용자 권한 표시 배지
|
||||
Widget _buildUserRoleBadge(String role) {
|
||||
final roleName = getRoleName(role);
|
||||
ShadcnBadgeVariant variant;
|
||||
|
||||
switch (role) {
|
||||
case 'S':
|
||||
return ShadcnBadge(
|
||||
text: '관리자',
|
||||
variant: ShadcnBadgeVariant.destructive,
|
||||
size: ShadcnBadgeSize.small,
|
||||
);
|
||||
variant = ShadcnBadgeVariant.destructive;
|
||||
break;
|
||||
case 'M':
|
||||
return ShadcnBadge(
|
||||
text: '멤버',
|
||||
variant: ShadcnBadgeVariant.primary,
|
||||
size: ShadcnBadgeSize.small,
|
||||
);
|
||||
variant = ShadcnBadgeVariant.primary;
|
||||
break;
|
||||
default:
|
||||
return ShadcnBadge(
|
||||
text: '사용자',
|
||||
variant: ShadcnBadgeVariant.outline,
|
||||
size: ShadcnBadgeSize.small,
|
||||
);
|
||||
variant = ShadcnBadgeVariant.outline;
|
||||
}
|
||||
|
||||
return ShadcnBadge(
|
||||
text: roleName,
|
||||
variant: variant,
|
||||
size: ShadcnBadgeSize.small,
|
||||
);
|
||||
}
|
||||
|
||||
/// 사용자 추가 폼으로 이동
|
||||
void _navigateToAdd() async {
|
||||
final result = await Navigator.pushNamed(context, Routes.userAdd);
|
||||
if (result == true) {
|
||||
_controller.loadUsers();
|
||||
if (result == true && mounted) {
|
||||
context.read<UserListController>().loadUsers(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,297 +112,491 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
||||
Routes.userEdit,
|
||||
arguments: userId,
|
||||
);
|
||||
if (result == true) {
|
||||
_controller.loadUsers();
|
||||
if (result == true && mounted) {
|
||||
context.read<UserListController>().loadUsers(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 사용자 삭제 다이얼로그
|
||||
void _showDeleteDialog(int userId) {
|
||||
void _showDeleteDialog(int userId, String userName) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('사용자 삭제'),
|
||||
content: const Text('정말로 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_controller.deleteUser(userId, () {
|
||||
setState(() {});
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('사용자 삭제'),
|
||||
content: Text('"$userName" 사용자를 정말로 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await context.read<UserListController>().deleteUser(
|
||||
userId,
|
||||
() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('사용자가 삭제되었습니다')),
|
||||
);
|
||||
},
|
||||
(error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error), backgroundColor: Colors.red),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 상태 변경 확인 다이얼로그
|
||||
void _showStatusChangeDialog(User user) {
|
||||
final newStatus = !user.isActive;
|
||||
final statusText = newStatus ? '활성화' : '비활성화';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('사용자 상태 변경'),
|
||||
content: Text('"${user.name}" 사용자를 $statusText 하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await context.read<UserListController>().changeUserStatus(
|
||||
user.id!,
|
||||
newStatus,
|
||||
(error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error), backgroundColor: Colors.red),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(statusText),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final int totalCount = _controller.users.length;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex =
|
||||
(startIndex + _pageSize) > totalCount
|
||||
? totalCount
|
||||
: (startIndex + _pageSize);
|
||||
final List<User> pagedUsers = _controller.users.sublist(
|
||||
startIndex,
|
||||
endIndex,
|
||||
);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더 액션 바
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('총 $totalCount명 사용자', style: ShadcnTheme.bodyMuted),
|
||||
Row(
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => UserListController(dataService: _dataService),
|
||||
child: Consumer<UserListController>(
|
||||
builder: (context, controller, child) {
|
||||
if (controller.isLoading && controller.users.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.error != null && controller.users.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '새로고침',
|
||||
onPressed: _controller.loadUsers,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: Icon(Icons.refresh),
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.red[300]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'데이터를 불러올 수 없습니다',
|
||||
style: ShadcnTheme.h4,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
controller.error!,
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ShadcnButton(
|
||||
text: '사용자 추가',
|
||||
onPressed: _navigateToAdd,
|
||||
text: '다시 시도',
|
||||
onPressed: () => controller.loadUsers(refresh: true),
|
||||
variant: ShadcnButtonVariant.primary,
|
||||
textColor: Colors.white,
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// 테이블 컨테이너
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 테이블 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: ShadcnTheme.spacing3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 검색 및 필터 섹션
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
side: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text('번호', style: ShadcnTheme.bodyMedium),
|
||||
// 검색 바
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '이름, 이메일, 사용자명으로 검색...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
controller.setSearchQuery('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: ShadcnTheme.spacing3,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('사용자명', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('이메일', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('회사명', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('지점명', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text('권한', style: ShadcnTheme.bodyMedium),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text('관리', style: ShadcnTheme.bodyMedium),
|
||||
const SizedBox(height: ShadcnTheme.spacing3),
|
||||
// 필터 버튼들
|
||||
Row(
|
||||
children: [
|
||||
// 상태 필터
|
||||
ShadcnButton(
|
||||
text: controller.filterIsActive == null
|
||||
? '모든 상태'
|
||||
: controller.filterIsActive!
|
||||
? '활성 사용자'
|
||||
: '비활성 사용자',
|
||||
onPressed: () {
|
||||
controller.setFilters(
|
||||
isActive: controller.filterIsActive == null
|
||||
? true
|
||||
: controller.filterIsActive!
|
||||
? false
|
||||
: null,
|
||||
);
|
||||
},
|
||||
variant: ShadcnButtonVariant.outline,
|
||||
icon: const Icon(Icons.filter_list),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
// 권한 필터
|
||||
PopupMenuButton<String?>(
|
||||
child: ShadcnButton(
|
||||
text: controller.filterRole == null
|
||||
? '모든 권한'
|
||||
: getRoleName(controller.filterRole!),
|
||||
onPressed: null,
|
||||
variant: ShadcnButtonVariant.outline,
|
||||
icon: const Icon(Icons.person),
|
||||
),
|
||||
onSelected: (role) {
|
||||
controller.setFilters(role: role);
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: null,
|
||||
child: Text('모든 권한'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'S',
|
||||
child: Text('관리자'),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'M',
|
||||
child: Text('멤버'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
// 필터 초기화
|
||||
if (controller.searchQuery.isNotEmpty ||
|
||||
controller.filterIsActive != null ||
|
||||
controller.filterRole != null)
|
||||
ShadcnButton(
|
||||
text: '필터 초기화',
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
controller.clearFilters();
|
||||
},
|
||||
variant: ShadcnButtonVariant.ghost,
|
||||
icon: const Icon(Icons.clear_all),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 테이블 데이터
|
||||
if (pagedUsers.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'등록된 사용자가 없습니다.',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// 헤더 액션 바
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'총 ${controller.users.length}명 사용자',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '새로고침',
|
||||
onPressed: () => controller.loadUsers(refresh: true),
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...pagedUsers.asMap().entries.map((entry) {
|
||||
final int index = entry.key;
|
||||
final User user = entry.value;
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
ShadcnButton(
|
||||
text: '사용자 추가',
|
||||
onPressed: _navigateToAdd,
|
||||
variant: ShadcnButtonVariant.primary,
|
||||
textColor: Colors.white,
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// 테이블 컨테이너
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 테이블 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: ShadcnTheme.spacing3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 번호
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${startIndex + index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 사용자명
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
user.name,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
// 이메일
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
user.email ?? '미등록',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 회사명
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_getCompanyName(user.companyId),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 지점명
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_controller.getBranchName(
|
||||
user.companyId,
|
||||
user.branchId,
|
||||
),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 권한
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: _buildUserRoleBadge(user.role),
|
||||
),
|
||||
// 관리
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
size: 16,
|
||||
color: ShadcnTheme.primary,
|
||||
),
|
||||
onPressed:
|
||||
user.id != null
|
||||
? () => _navigateToEdit(user.id!)
|
||||
: null,
|
||||
tooltip: '수정',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
size: 16,
|
||||
color: ShadcnTheme.destructive,
|
||||
),
|
||||
onPressed:
|
||||
user.id != null
|
||||
? () => _showDeleteDialog(user.id!)
|
||||
: null,
|
||||
tooltip: '삭제',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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 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))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 페이지네이션
|
||||
if (totalCount > _pageSize) ...[
|
||||
const SizedBox(height: ShadcnTheme.spacing6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '이전',
|
||||
onPressed:
|
||||
_currentPage > 1
|
||||
? () {
|
||||
setState(() {
|
||||
_currentPage--;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
size: ShadcnButtonSize.small,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
Text(
|
||||
'$_currentPage / ${(totalCount / _pageSize).ceil()}',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
ShadcnButton(
|
||||
text: '다음',
|
||||
onPressed:
|
||||
_currentPage < (totalCount / _pageSize).ceil()
|
||||
? () {
|
||||
setState(() {
|
||||
_currentPage++;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
size: ShadcnButtonSize.small,
|
||||
),
|
||||
|
||||
// 테이블 데이터
|
||||
if (controller.users.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
|
||||
child: Center(
|
||||
child: Text(
|
||||
controller.searchQuery.isNotEmpty ||
|
||||
controller.filterIsActive != null ||
|
||||
controller.filterRole != null
|
||||
? '검색 결과가 없습니다.'
|
||||
: '등록된 사용자가 없습니다.',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...controller.users.asMap().entries.map((entry) {
|
||||
final int index = entry.key;
|
||||
final User user = entry.value;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
color: index % 2 == 0 ? null : ShadcnTheme.muted.withValues(alpha: 0.1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 번호
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 사용자명
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
user.name,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
if (user.username != null)
|
||||
Text(
|
||||
'@${user.username}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 이메일
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
user.email ?? '미등록',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 회사명
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
_getCompanyName(user.companyId),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 지점명
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
controller.getBranchName(
|
||||
user.companyId,
|
||||
user.branchId,
|
||||
),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// 권한
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: _buildUserRoleBadge(user.role),
|
||||
),
|
||||
// 상태
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 8,
|
||||
color: _getStatusColor(user.isActive),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
user.isActive ? '활성' : '비활성',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: _getStatusColor(user.isActive),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 관리
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.power_settings_new,
|
||||
size: 16,
|
||||
color: user.isActive ? Colors.orange : Colors.green,
|
||||
),
|
||||
onPressed: user.id != null
|
||||
? () => _showStatusChangeDialog(user)
|
||||
: null,
|
||||
tooltip: user.isActive ? '비활성화' : '활성화',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
size: 16,
|
||||
color: ShadcnTheme.primary,
|
||||
),
|
||||
onPressed: user.id != null
|
||||
? () => _navigateToEdit(user.id!)
|
||||
: null,
|
||||
tooltip: '수정',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
size: 16,
|
||||
color: ShadcnTheme.destructive,
|
||||
),
|
||||
onPressed: user.id != null
|
||||
? () => _showDeleteDialog(user.id!, user.name)
|
||||
: null,
|
||||
tooltip: '삭제',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 무한 스크롤 로딩 인디케이터
|
||||
if (controller.isLoadingMore)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
|
||||
// 더 이상 데이터가 없을 때
|
||||
if (!controller.hasMoreData && controller.users.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'모든 사용자를 불러왔습니다',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user