## 주요 변경사항 ### 아키텍처 개선 - Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리) - Use Case 패턴 도입으로 비즈니스 로직 캡슐화 - Repository 패턴으로 데이터 접근 추상화 - 의존성 주입 구조 개선 ### 상태 관리 최적화 - 모든 Controller에서 불필요한 상태 관리 로직 제거 - 페이지네이션 로직 통일 및 간소화 - 에러 처리 로직 개선 (에러 메시지 한글화) - 로딩 상태 관리 최적화 ### Mock 서비스 제거 - MockDataService 완전 제거 - 모든 화면을 실제 API 전용으로 전환 - 불필요한 Mock 관련 코드 정리 ### UI/UX 개선 - Overview 화면 대시보드 기능 강화 - 라이선스 만료 알림 위젯 추가 - 사이드바 네비게이션 개선 - 일관된 UI 컴포넌트 사용 ### 코드 품질 - 중복 코드 제거 및 함수 추출 - 파일별 책임 분리 명확화 - 테스트 코드 업데이트 ## 영향 범위 - 모든 화면의 Controller 리팩토링 - API 통신 레이어 구조 개선 - 에러 처리 및 로깅 시스템 개선 ## 향후 계획 - 단위 테스트 커버리지 확대 - 통합 테스트 시나리오 추가 - 성능 모니터링 도구 통합
272 lines
7.2 KiB
Dart
272 lines
7.2 KiB
Dart
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/user_service.dart';
|
|
import 'package:superport/services/company_service.dart';
|
|
import 'package:superport/utils/constants.dart';
|
|
import 'package:superport/models/user_phone_field.dart';
|
|
|
|
// 사용자 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
|
class UserFormController extends ChangeNotifier {
|
|
final UserService _userService = GetIt.instance<UserService>();
|
|
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
|
|
|
// 상태 변수
|
|
bool _isLoading = false;
|
|
String? _error;
|
|
// API만 사용
|
|
|
|
// 폼 필드
|
|
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 = [];
|
|
final List<String> phoneTypes = ['휴대폰', '사무실', '팩스', '기타'];
|
|
|
|
List<Company> companies = [];
|
|
List<Branch> branches = [];
|
|
|
|
// Getters
|
|
bool get isLoading => _isLoading;
|
|
String? get error => _error;
|
|
bool get isCheckingUsername => _isCheckingUsername;
|
|
bool? get isUsernameAvailable => _isUsernameAvailable;
|
|
|
|
UserFormController({this.userId}) {
|
|
isEditMode = userId != null;
|
|
if (isEditMode) {
|
|
loadUser();
|
|
} else {
|
|
addPhoneField();
|
|
}
|
|
loadCompanies();
|
|
}
|
|
|
|
// 회사 목록 로드
|
|
Future<void> loadCompanies() async {
|
|
try {
|
|
final result = await _companyService.getCompanies();
|
|
companies = result;
|
|
notifyListeners();
|
|
} catch (e) {
|
|
debugPrint('회사 목록 로드 실패: $e');
|
|
companies = [];
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
// 회사 ID에 따라 지점 목록 로드
|
|
void loadBranches(int companyId) {
|
|
final company = companies.firstWhere(
|
|
(c) => c.id == companyId,
|
|
orElse: () => Company(
|
|
id: companyId,
|
|
name: '알 수 없는 회사',
|
|
branches: [],
|
|
),
|
|
);
|
|
branches = company.branches ?? [];
|
|
// 지점 변경 시 이전 선택 지점이 새 회사에 없으면 초기화
|
|
if (branchId != null && !branches.any((b) => b.id == branchId)) {
|
|
branchId = null;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
// 사용자 정보 로드 (수정 모드)
|
|
Future<void> loadUser() async {
|
|
if (userId == null) return;
|
|
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final user = await _userService.getUser(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();
|
|
}
|
|
|
|
// 전화번호 필드 삭제
|
|
void removePhoneField(int index) {
|
|
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 {
|
|
final isDuplicate = await _userService.checkDuplicateUsername(value);
|
|
_isUsernameAvailable = !isDuplicate;
|
|
_lastCheckedUsername = value;
|
|
} catch (e) {
|
|
_isUsernameAvailable = null;
|
|
} finally {
|
|
_isCheckingUsername = false;
|
|
notifyListeners();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 사용자 저장 (UI에서 호출)
|
|
Future<void> saveUser(Function(String? error) onResult) async {
|
|
if (formKey.currentState?.validate() != true) {
|
|
onResult('폼 유효성 검사 실패');
|
|
return;
|
|
}
|
|
formKey.currentState?.save();
|
|
|
|
if (companyId == null) {
|
|
onResult('소속 회사를 선택해주세요');
|
|
return;
|
|
}
|
|
|
|
// 신규 등록 시 username 중복 확인
|
|
if (!isEditMode) {
|
|
if (username.isEmpty) {
|
|
onResult('사용자명을 입력해주세요');
|
|
return;
|
|
}
|
|
if (_isUsernameAvailable == false) {
|
|
onResult('이미 사용중인 사용자명입니다');
|
|
return;
|
|
}
|
|
if (password.isEmpty) {
|
|
onResult('비밀번호를 입력해주세요');
|
|
return;
|
|
}
|
|
}
|
|
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
// 전화번호 목록 준비
|
|
String? phoneNumber;
|
|
for (var phoneField in phoneFields) {
|
|
if (phoneField.number.isNotEmpty) {
|
|
phoneNumber = phoneField.number;
|
|
break; // API는 단일 전화번호만 지원
|
|
}
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
onResult(null);
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
onResult(_error);
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
// 컨트롤러 해제
|
|
@override
|
|
void dispose() {
|
|
_usernameCheckTimer?.cancel();
|
|
for (var phoneField in phoneFields) {
|
|
phoneField.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
// API 모드만 사용 (Mock 데이터 제거됨)
|
|
// void toggleApiMode() 메서드 제거
|
|
}
|