refactor: Repository 패턴 적용 및 Clean Architecture 완성
## 주요 변경사항 ### 🏗️ Architecture - Repository 패턴 전면 도입 (인터페이스/구현체 분리) - Domain Layer에 Repository 인터페이스 정의 - Data Layer에 Repository 구현체 배치 - UseCase 의존성을 Service에서 Repository로 전환 ### 📦 Dependency Injection - GetIt 기반 DI Container 재구성 (lib/injection_container.dart) - Repository 인터페이스와 구현체 등록 - Service와 Repository 공존 (마이그레이션 기간) ### 🔄 Migration Status 완료: - License 모듈 (6개 UseCase) - Warehouse Location 모듈 (5개 UseCase) 진행중: - Auth 모듈 (2/5 UseCase) - Company 모듈 (1/6 UseCase) 대기: - User 모듈 (7개 UseCase) - Equipment 모듈 (4개 UseCase) ### 🎯 Controller 통합 - 중복 Controller 제거 (with_usecase 버전) - 단일 Controller로 통합 - UseCase 패턴 직접 적용 ### 🧹 코드 정리 - 임시 파일 제거 (test_*.md, task.md) - Node.js 아티팩트 제거 (package.json) - 불필요한 테스트 파일 정리 ### ✅ 테스트 개선 - Real API 중심 테스트 구조 - Mock 제거, 실제 API 엔드포인트 사용 - 통합 테스트 프레임워크 강화 ## 기술적 영향 - 의존성 역전 원칙 적용 - 레이어 간 결합도 감소 - 테스트 용이성 향상 - 확장성 및 유지보수성 개선 ## 다음 단계 1. User/Equipment 모듈 Repository 마이그레이션 2. Service Layer 점진적 제거 3. 캐싱 전략 구현 4. 성능 최적화
This commit is contained in:
@@ -38,7 +38,7 @@ class _AppLayoutState extends State<AppLayout>
|
||||
late final DashboardService _dashboardService;
|
||||
late final LookupService _lookupService;
|
||||
late Animation<double> _sidebarAnimation;
|
||||
int _expiringLicenseCount = 0; // 30일 내 만료 예정 라이선스 수
|
||||
int _expiringLicenseCount = 0; // 7일 내 만료 예정 라이선스 수
|
||||
|
||||
// 레이아웃 상수 (1920x1080 최적화)
|
||||
static const double _sidebarExpandedWidth = 260.0;
|
||||
@@ -79,6 +79,7 @@ class _AppLayoutState extends State<AppLayout>
|
||||
},
|
||||
(summary) {
|
||||
print('[DEBUG] 라이선스 만료 정보 로드 성공!');
|
||||
print('[DEBUG] 7일 내 만료: ${summary.expiring7Days ?? 0}개');
|
||||
print('[DEBUG] 30일 내 만료: ${summary.within30Days}개');
|
||||
print('[DEBUG] 60일 내 만료: ${summary.within60Days}개');
|
||||
print('[DEBUG] 90일 내 만료: ${summary.within90Days}개');
|
||||
@@ -86,8 +87,10 @@ class _AppLayoutState extends State<AppLayout>
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// 30일 내 만료 수를 표시 (7일 내 만료가 포함됨)
|
||||
// expiring_30_days는 30일 이내의 모든 라이선스를 포함
|
||||
_expiringLicenseCount = summary.within30Days;
|
||||
print('[DEBUG] 상태 업데이트 완료: $_expiringLicenseCount');
|
||||
print('[DEBUG] 상태 업데이트 완료: $_expiringLicenseCount (30일 내 만료)');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ class _CompanyListState extends State<CompanyList> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CompanyListController();
|
||||
_controller.initializeWithPageSize(10); // 페이지 크기 설정
|
||||
_controller.initialize(pageSize: 10); // 통일된 초기화 방식
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -430,18 +430,13 @@ class _CompanyListState extends State<CompanyList> {
|
||||
],
|
||||
),
|
||||
|
||||
// 페이지네이션 (Controller 상태 사용)
|
||||
// 페이지네이션 (BaseListController의 goToPage 사용)
|
||||
pagination: Pagination(
|
||||
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);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -32,15 +32,9 @@ class CompanyListController extends BaseListController<Company> {
|
||||
}
|
||||
}
|
||||
|
||||
// 초기 데이터 로드
|
||||
Future<void> initialize() async {
|
||||
await loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
// 페이지 크기를 지정하여 초기화
|
||||
// 기존 initializeWithPageSize를 사용하는 코드와의 호환성 유지
|
||||
Future<void> initializeWithPageSize(int newPageSize) async {
|
||||
pageSize = newPageSize;
|
||||
await loadData(isRefresh: true);
|
||||
await initialize(pageSize: newPageSize);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -48,8 +42,8 @@ class CompanyListController extends BaseListController<Company> {
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// API 호출 - 회사 목록 조회 (이제 PaginatedResponse 반환)
|
||||
final response = await ErrorHandler.handleApiCall<dynamic>(
|
||||
// API 호출 - 회사 목록 조회 (PaginatedResponse 반환)
|
||||
final response = await ErrorHandler.handleApiCall(
|
||||
() => _companyService.getCompanies(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
@@ -61,6 +55,20 @@ class CompanyListController extends BaseListController<Company> {
|
||||
},
|
||||
);
|
||||
|
||||
if (response == null) {
|
||||
return PagedResult(
|
||||
items: [],
|
||||
meta: PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// PaginatedResponse를 PagedResult로 변환
|
||||
final meta = PaginationMeta(
|
||||
currentPage: response.page,
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/controllers/base_list_controller.dart';
|
||||
import '../../../core/errors/failures.dart';
|
||||
import '../../../domain/usecases/base_usecase.dart';
|
||||
import '../../../domain/usecases/company/company_usecases.dart';
|
||||
import '../../../models/company_model.dart';
|
||||
import '../../../services/company_service.dart';
|
||||
import '../../../di/injection_container.dart';
|
||||
import '../../../data/models/common/pagination_params.dart';
|
||||
|
||||
/// UseCase를 활용한 회사 목록 관리 컨트롤러
|
||||
/// BaseListController를 상속받아 공통 기능 재사용
|
||||
class CompanyListControllerWithUseCase extends BaseListController<Company> {
|
||||
// UseCases
|
||||
late final GetCompaniesUseCase _getCompaniesUseCase;
|
||||
late final CreateCompanyUseCase _createCompanyUseCase;
|
||||
late final UpdateCompanyUseCase _updateCompanyUseCase;
|
||||
late final DeleteCompanyUseCase _deleteCompanyUseCase;
|
||||
late final GetCompanyDetailUseCase _getCompanyDetailUseCase;
|
||||
late final ToggleCompanyStatusUseCase _toggleCompanyStatusUseCase;
|
||||
|
||||
// 필터 상태
|
||||
String? selectedType;
|
||||
bool? isActive;
|
||||
|
||||
// 선택된 회사들
|
||||
final Set<int> _selectedCompanyIds = {};
|
||||
Set<int> get selectedCompanyIds => _selectedCompanyIds;
|
||||
bool get hasSelection => _selectedCompanyIds.isNotEmpty;
|
||||
|
||||
CompanyListControllerWithUseCase() {
|
||||
// UseCase 초기화
|
||||
final companyService = inject<CompanyService>();
|
||||
_getCompaniesUseCase = GetCompaniesUseCase(companyService);
|
||||
_createCompanyUseCase = CreateCompanyUseCase(companyService);
|
||||
_updateCompanyUseCase = UpdateCompanyUseCase(companyService);
|
||||
_deleteCompanyUseCase = DeleteCompanyUseCase(companyService);
|
||||
_getCompanyDetailUseCase = GetCompanyDetailUseCase(companyService);
|
||||
_toggleCompanyStatusUseCase = ToggleCompanyStatusUseCase(companyService);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PagedResult<Company>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
// UseCase를 통한 데이터 조회
|
||||
final usecaseParams = GetCompaniesParams(
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
search: params.search,
|
||||
isActive: isActive,
|
||||
);
|
||||
|
||||
final result = await _getCompaniesUseCase(usecaseParams);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
throw Exception(failure.message);
|
||||
},
|
||||
(companies) {
|
||||
// PagedResult로 래핑하여 반환 (임시로 메타데이터 생성)
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: companies.length, // 실제로는 서버에서 받아와야 함
|
||||
totalPages: (companies.length / params.perPage).ceil(),
|
||||
hasNext: companies.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
return PagedResult(items: companies, meta: meta);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 생성
|
||||
Future<bool> createCompany(Company company) async {
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
final params = CreateCompanyParams(company: company);
|
||||
final result = await _createCompanyUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
|
||||
// ValidationFailure의 경우 상세 에러 표시
|
||||
if (failure is ValidationFailure && failure.errors != null) {
|
||||
final errorMessages = failure.errors!.entries
|
||||
.map((e) => '${e.key}: ${e.value}')
|
||||
.join('\n');
|
||||
errorState = errorMessages;
|
||||
}
|
||||
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(newCompany) {
|
||||
// 로컬 리스트에 추가
|
||||
addItemLocally(newCompany);
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 수정
|
||||
Future<bool> updateCompany(int id, Company company) async {
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
final params = UpdateCompanyParams(id: id, company: company);
|
||||
final result = await _updateCompanyUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
|
||||
// ValidationFailure의 경우 상세 에러 표시
|
||||
if (failure is ValidationFailure && failure.errors != null) {
|
||||
final errorMessages = failure.errors!.entries
|
||||
.map((e) => '${e.key}: ${e.value}')
|
||||
.join('\n');
|
||||
errorState = errorMessages;
|
||||
}
|
||||
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(updatedCompany) {
|
||||
// 로컬 리스트 업데이트
|
||||
updateItemLocally(updatedCompany, (item) => item.id == id);
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 삭제
|
||||
Future<bool> deleteCompany(int id) async {
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
final params = DeleteCompanyParams(id: id);
|
||||
final result = await _deleteCompanyUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
// 로컬 리스트에서 제거
|
||||
removeItemLocally((item) => item.id == id);
|
||||
_selectedCompanyIds.remove(id);
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 상세 조회
|
||||
Future<Company?> getCompanyDetail(int id, {bool includeBranches = false}) async {
|
||||
final params = GetCompanyDetailParams(
|
||||
id: id,
|
||||
includeBranches: includeBranches,
|
||||
);
|
||||
|
||||
final result = await _getCompanyDetailUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
notifyListeners();
|
||||
return null;
|
||||
},
|
||||
(company) => company,
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 상태 토글 (활성화/비활성화)
|
||||
Future<bool> toggleCompanyStatus(int id) async {
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
// 현재 회사 상태를 확인하여 토글 (기본값 true로 가정)
|
||||
final params = ToggleCompanyStatusParams(
|
||||
id: id,
|
||||
isActive: false, // 임시로 false로 설정 (실제로는 현재 상태를 API로 확인해야 함)
|
||||
);
|
||||
final result = await _toggleCompanyStatusUseCase(params);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
errorState = failure.message;
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
// 로컬 리스트에서 상태 업데이트 (실제로는 API에서 업데이트된 Company 객체를 받아와야 함)
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 회사 선택/해제
|
||||
void toggleSelection(int companyId) {
|
||||
if (_selectedCompanyIds.contains(companyId)) {
|
||||
_selectedCompanyIds.remove(companyId);
|
||||
} else {
|
||||
_selectedCompanyIds.add(companyId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 전체 선택/해제
|
||||
void toggleSelectAll() {
|
||||
if (_selectedCompanyIds.length == items.length) {
|
||||
_selectedCompanyIds.clear();
|
||||
} else {
|
||||
_selectedCompanyIds.clear();
|
||||
_selectedCompanyIds.addAll(items.where((c) => c.id != null).map((c) => c.id!));
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택 초기화
|
||||
void clearSelection() {
|
||||
_selectedCompanyIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 필터 적용
|
||||
void applyFilters({String? type, bool? active}) {
|
||||
selectedType = type;
|
||||
isActive = active;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
selectedType = null;
|
||||
isActive = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 선택된 회사들 일괄 삭제
|
||||
Future<bool> deleteSelectedCompanies() async {
|
||||
if (_selectedCompanyIds.isEmpty) return false;
|
||||
|
||||
isLoadingState = true;
|
||||
notifyListeners();
|
||||
|
||||
bool allSuccess = true;
|
||||
final failedIds = <int>[];
|
||||
|
||||
for (final id in _selectedCompanyIds.toList()) {
|
||||
final params = DeleteCompanyParams(id: id);
|
||||
final result = await _deleteCompanyUseCase(params);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
allSuccess = false;
|
||||
failedIds.add(id);
|
||||
debugPrint('회사 $id 삭제 실패: ${failure.message}');
|
||||
},
|
||||
(_) {
|
||||
removeItemLocally((item) => item.id == id);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (failedIds.isNotEmpty) {
|
||||
errorState = '일부 회사 삭제 실패: ${failedIds.join(', ')}';
|
||||
}
|
||||
|
||||
_selectedCompanyIds.clear();
|
||||
isLoadingState = false;
|
||||
notifyListeners();
|
||||
|
||||
return allSuccess;
|
||||
}
|
||||
}
|
||||
@@ -103,16 +103,14 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||
// API에서 반환한 실제 메타데이터 사용
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: items.length < params.perPage ?
|
||||
(params.page - 1) * params.perPage + items.length :
|
||||
(params.page * params.perPage) + 1,
|
||||
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||
hasNext: items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
currentPage: apiEquipmentDtos.page,
|
||||
perPage: apiEquipmentDtos.size,
|
||||
total: apiEquipmentDtos.totalElements,
|
||||
totalPages: apiEquipmentDtos.totalPages,
|
||||
hasNext: !apiEquipmentDtos.last,
|
||||
hasPrevious: !apiEquipmentDtos.first,
|
||||
);
|
||||
|
||||
return PagedResult(items: items, meta: meta);
|
||||
|
||||
@@ -168,10 +168,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
/// 필터링된 장비 목록 반환
|
||||
List<UnifiedEquipment> _getFilteredEquipments() {
|
||||
// 서버에서 이미 페이지네이션된 데이터를 사용
|
||||
var equipments = _controller.equipments;
|
||||
print('DEBUG: Total equipments from controller: ${equipments.length}'); // 디버그 정보
|
||||
|
||||
// 검색 키워드 적용 (확장된 검색 필드)
|
||||
// 로컬 검색 키워드 적용 (서버 검색과 병행)
|
||||
// 서버에서 검색된 결과에 추가 로컬 필터링
|
||||
if (_appliedSearchKeyword.isNotEmpty) {
|
||||
equipments = equipments.where((e) {
|
||||
final keyword = _appliedSearchKeyword.toLowerCase();
|
||||
@@ -190,8 +191,6 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
print('DEBUG: Filtered equipments count: ${equipments.length}'); // 디버그 정보
|
||||
print('DEBUG: Selected status filter: $_selectedStatus'); // 디버그 정보
|
||||
return equipments;
|
||||
}
|
||||
|
||||
@@ -392,7 +391,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
final int selectedRentCount = controller.getSelectedEquipmentCountByStatus(EquipmentStatus.rent);
|
||||
|
||||
final filteredEquipments = _getFilteredEquipments();
|
||||
final totalCount = filteredEquipments.length;
|
||||
// 백엔드 API에서 제공하는 실제 전체 아이템 수 사용
|
||||
final totalCount = controller.total;
|
||||
|
||||
return BaseListScreen(
|
||||
isLoading: controller.isLoading && controller.equipments.isEmpty,
|
||||
@@ -414,8 +414,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
dataTable: _buildDataTable(filteredEquipments),
|
||||
|
||||
// 페이지네이션
|
||||
pagination: totalCount > controller.pageSize ? Pagination(
|
||||
totalCount: totalCount,
|
||||
pagination: controller.totalPages > 1 ? Pagination(
|
||||
totalCount: controller.total,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
@@ -944,17 +944,12 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
/// 데이터 테이블
|
||||
Widget _buildDataTable(List<UnifiedEquipment> filteredEquipments) {
|
||||
final int startIndex = (_controller.currentPage - 1) * _controller.pageSize;
|
||||
final int endIndex =
|
||||
(startIndex + _controller.pageSize) > filteredEquipments.length
|
||||
? filteredEquipments.length
|
||||
: (startIndex + _controller.pageSize);
|
||||
final List<UnifiedEquipment> pagedEquipments = filteredEquipments.sublist(
|
||||
startIndex,
|
||||
endIndex,
|
||||
);
|
||||
// 백엔드에서 이미 페이지네이션된 데이터를 받으므로
|
||||
// 프론트엔드에서 추가 페이징 불필요
|
||||
final List<UnifiedEquipment> pagedEquipments = filteredEquipments;
|
||||
|
||||
if (pagedEquipments.isEmpty) {
|
||||
// 전체 데이터가 없는지 확인 (API의 total 사용)
|
||||
if (_controller.total == 0 && pagedEquipments.isEmpty) {
|
||||
return StandardEmptyState(
|
||||
title:
|
||||
_appliedSearchKeyword.isNotEmpty
|
||||
@@ -1173,19 +1168,9 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
/// 페이지 데이터 가져오기
|
||||
List<UnifiedEquipment> _getPagedEquipments() {
|
||||
final filteredEquipments = _getFilteredEquipments();
|
||||
final int startIndex = (_controller.currentPage - 1) * _controller.pageSize;
|
||||
final int endIndex = startIndex + _controller.pageSize;
|
||||
|
||||
if (startIndex >= filteredEquipments.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final actualEndIndex = endIndex > filteredEquipments.length
|
||||
? filteredEquipments.length
|
||||
: endIndex;
|
||||
|
||||
return filteredEquipments.sublist(startIndex, actualEndIndex);
|
||||
// 서버 페이지네이션 사용: 컨트롤러의 items가 이미 페이지네이션된 데이터
|
||||
// 로컬 필터링만 적용
|
||||
return _getFilteredEquipments();
|
||||
}
|
||||
|
||||
/// 카테고리 축약 표기 함수
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/controllers/base_list_controller.dart';
|
||||
import '../../../core/utils/error_handler.dart';
|
||||
import '../../../data/models/common/pagination_params.dart';
|
||||
import '../../../data/models/license/license_dto.dart';
|
||||
import '../../../domain/usecases/license/license_usecases.dart';
|
||||
|
||||
/// UseCase 패턴을 적용한 라이선스 목록 컨트롤러
|
||||
class LicenseListControllerWithUseCase extends BaseListController<LicenseDto> {
|
||||
final GetLicensesUseCase getLicensesUseCase;
|
||||
final CreateLicenseUseCase createLicenseUseCase;
|
||||
final UpdateLicenseUseCase updateLicenseUseCase;
|
||||
final DeleteLicenseUseCase deleteLicenseUseCase;
|
||||
final CheckLicenseExpiryUseCase checkLicenseExpiryUseCase;
|
||||
|
||||
// 선택된 항목들
|
||||
final Set<int> _selectedLicenseIds = {};
|
||||
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||
|
||||
// 필터 옵션
|
||||
String? _filterByCompany;
|
||||
String? _filterByExpiry;
|
||||
DateTime? _filterStartDate;
|
||||
DateTime? _filterEndDate;
|
||||
|
||||
String? get filterByCompany => _filterByCompany;
|
||||
String? get filterByExpiry => _filterByExpiry;
|
||||
DateTime? get filterStartDate => _filterStartDate;
|
||||
DateTime? get filterEndDate => _filterEndDate;
|
||||
|
||||
// 만료 임박 라이선스 정보
|
||||
LicenseExpiryResult? _expiryResult;
|
||||
LicenseExpiryResult? get expiryResult => _expiryResult;
|
||||
|
||||
LicenseListControllerWithUseCase({
|
||||
required this.getLicensesUseCase,
|
||||
required this.createLicenseUseCase,
|
||||
required this.updateLicenseUseCase,
|
||||
required this.deleteLicenseUseCase,
|
||||
required this.checkLicenseExpiryUseCase,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PagedResult<LicenseDto>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
try {
|
||||
// 필터 파라미터 구성
|
||||
final filters = <String, dynamic>{};
|
||||
if (_filterByCompany != null) filters['company_id'] = _filterByCompany;
|
||||
if (_filterByExpiry != null) filters['expiry'] = _filterByExpiry;
|
||||
if (_filterStartDate != null) filters['start_date'] = _filterStartDate!.toIso8601String();
|
||||
if (_filterEndDate != null) filters['end_date'] = _filterEndDate!.toIso8601String();
|
||||
|
||||
final updatedParams = params.copyWith(filters: filters);
|
||||
final getParams = GetLicensesParams.fromPaginationParams(updatedParams);
|
||||
|
||||
final result = await getLicensesUseCase(getParams);
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(licenseResponse) {
|
||||
// PagedResult로 래핑하여 반환
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: licenseResponse.items.length, // 실제로는 서버에서 받아와야 함
|
||||
totalPages: (licenseResponse.items.length / params.perPage).ceil(),
|
||||
hasNext: licenseResponse.items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
return PagedResult(items: licenseResponse.items, meta: meta);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('데이터 로드 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 만료 임박 라이선스 체크
|
||||
Future<void> checkExpiringLicenses() async {
|
||||
try {
|
||||
final params = CheckLicenseExpiryParams(
|
||||
companyId: _filterByCompany != null ? int.tryParse(_filterByCompany!) : null,
|
||||
);
|
||||
|
||||
final result = await checkLicenseExpiryUseCase(params);
|
||||
|
||||
result.fold(
|
||||
(failure) => errorState = failure.message,
|
||||
(expiryResult) {
|
||||
_expiryResult = expiryResult;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '라이선스 만료 체크 실패: $e';
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 생성
|
||||
Future<void> createLicense({
|
||||
required int equipmentId,
|
||||
required int companyId,
|
||||
required String licenseType,
|
||||
required DateTime startDate,
|
||||
required DateTime expiryDate,
|
||||
String? description,
|
||||
double? cost,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = CreateLicenseParams(
|
||||
equipmentId: equipmentId,
|
||||
companyId: companyId,
|
||||
licenseType: licenseType,
|
||||
startDate: startDate,
|
||||
expiryDate: expiryDate,
|
||||
description: description,
|
||||
cost: cost,
|
||||
);
|
||||
|
||||
final result = await createLicenseUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(license) async {
|
||||
await refresh();
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 수정
|
||||
Future<void> updateLicense({
|
||||
required int id,
|
||||
int? equipmentId,
|
||||
int? companyId,
|
||||
String? licenseType,
|
||||
DateTime? startDate,
|
||||
DateTime? expiryDate,
|
||||
String? description,
|
||||
double? cost,
|
||||
String? status,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = UpdateLicenseParams(
|
||||
id: id,
|
||||
equipmentId: equipmentId,
|
||||
companyId: companyId,
|
||||
licenseType: licenseType,
|
||||
startDate: startDate,
|
||||
expiryDate: expiryDate,
|
||||
description: description,
|
||||
cost: cost,
|
||||
status: status,
|
||||
);
|
||||
|
||||
final result = await updateLicenseUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(license) async {
|
||||
updateItemLocally(license, (item) => item.id == license.id);
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 라이선스 삭제
|
||||
Future<void> deleteLicense(int id) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final result = await deleteLicenseUseCase(id);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(_) async {
|
||||
removeItemLocally((item) => item.id == id);
|
||||
_selectedLicenseIds.remove(id);
|
||||
await checkExpiringLicenses();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
String? company,
|
||||
String? expiry,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
}) {
|
||||
_filterByCompany = company;
|
||||
_filterByExpiry = expiry;
|
||||
_filterStartDate = startDate;
|
||||
_filterEndDate = endDate;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_filterByCompany = null;
|
||||
_filterByExpiry = null;
|
||||
_filterStartDate = null;
|
||||
_filterEndDate = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 라이선스 선택 토글
|
||||
void toggleLicenseSelection(int id) {
|
||||
if (_selectedLicenseIds.contains(id)) {
|
||||
_selectedLicenseIds.remove(id);
|
||||
} else {
|
||||
_selectedLicenseIds.add(id);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 모든 라이선스 선택
|
||||
void selectAll() {
|
||||
_selectedLicenseIds.clear();
|
||||
_selectedLicenseIds.addAll(items.map((e) => e.id));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택 해제
|
||||
void clearSelection() {
|
||||
_selectedLicenseIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택된 라이선스 일괄 삭제
|
||||
Future<void> deleteSelectedLicenses() async {
|
||||
if (_selectedLicenseIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLicenseIds.toList()) {
|
||||
final result = await deleteLicenseUseCase(id);
|
||||
result.fold(
|
||||
(failure) => print('Failed to delete license $id: ${failure.message}'),
|
||||
(_) => removeItemLocally((item) => item.id == id),
|
||||
);
|
||||
}
|
||||
|
||||
_selectedLicenseIds.clear();
|
||||
await checkExpiringLicenses();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 생성: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectedLicenseIds.clear();
|
||||
_expiryResult = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ class _LicenseListState extends State<LicenseList> {
|
||||
// 실제 API 사용 여부에 따라 컨트롤러 초기화
|
||||
final useApi = env.Environment.useApi;
|
||||
_controller = LicenseListController();
|
||||
_controller.pageSize = 10; // 페이지 크기를 10으로 설정
|
||||
|
||||
debugPrint('📌 Controller 모드: ${useApi ? "Real API" : "Mock Data"}');
|
||||
debugPrint('==========================================\n');
|
||||
@@ -239,16 +240,17 @@ class _LicenseListState extends State<LicenseList> {
|
||||
child: Consumer<LicenseListController>(
|
||||
builder: (context, controller, child) {
|
||||
final licenses = controller.licenses;
|
||||
final totalCount = licenses.length;
|
||||
// 백엔드 API에서 제공하는 실제 전체 아이템 수 사용
|
||||
final totalCount = controller.total;
|
||||
|
||||
return BaseListScreen(
|
||||
headerSection: _buildStatisticsCards(),
|
||||
searchBar: _buildSearchBar(),
|
||||
actionBar: _buildActionBar(),
|
||||
dataTable: _buildDataTable(),
|
||||
pagination: totalCount > controller.pageSize
|
||||
pagination: controller.total > 0
|
||||
? Pagination(
|
||||
totalCount: totalCount,
|
||||
totalCount: controller.total,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
@@ -597,6 +599,7 @@ class _LicenseListState extends State<LicenseList> {
|
||||
...pagedLicenses.asMap().entries.map((entry) {
|
||||
final displayIndex = entry.key;
|
||||
final license = entry.value;
|
||||
// 백엔드에서 이미 페이지네이션된 데이터를 받으므로 추가 계산 불필요
|
||||
final index = (_controller.currentPage - 1) * _controller.pageSize + displayIndex;
|
||||
final daysRemaining = _controller.getDaysUntilExpiry(license.expiryDate);
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/data/models/auth/login_request.dart';
|
||||
import 'package:superport/di/injection_container.dart';
|
||||
import 'package:superport/injection_container.dart';
|
||||
import 'package:superport/services/auth_service.dart';
|
||||
import 'package:superport/services/health_test_service.dart';
|
||||
import 'package:superport/services/health_check_service.dart';
|
||||
|
||||
/// 로그인 화면의 상태 및 비즈니스 로직을 담당하는 ChangeNotifier 기반 컨트롤러
|
||||
class LoginController extends ChangeNotifier {
|
||||
final AuthService _authService = inject<AuthService>();
|
||||
final AuthService _authService = sl<AuthService>();
|
||||
final HealthCheckService _healthCheckService = HealthCheckService();
|
||||
/// 아이디 입력 컨트롤러
|
||||
final TextEditingController idController = TextEditingController();
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../core/errors/failures.dart';
|
||||
import '../../../domain/usecases/base_usecase.dart';
|
||||
import '../../../domain/usecases/auth/login_usecase.dart';
|
||||
import '../../../domain/usecases/auth/check_auth_status_usecase.dart';
|
||||
import '../../../services/auth_service.dart';
|
||||
import '../../../services/health_check_service.dart';
|
||||
import '../../../di/injection_container.dart';
|
||||
|
||||
/// UseCase를 활용한 로그인 화면 컨트롤러
|
||||
/// 비즈니스 로직을 UseCase로 분리하여 테스트 용이성과 재사용성 향상
|
||||
class LoginControllerWithUseCase extends ChangeNotifier {
|
||||
// UseCases
|
||||
late final LoginUseCase _loginUseCase;
|
||||
late final CheckAuthStatusUseCase _checkAuthStatusUseCase;
|
||||
|
||||
// Services
|
||||
final HealthCheckService _healthCheckService = HealthCheckService();
|
||||
|
||||
// UI Controllers
|
||||
final TextEditingController idController = TextEditingController();
|
||||
final TextEditingController pwController = TextEditingController();
|
||||
|
||||
// Focus Nodes
|
||||
final FocusNode idFocus = FocusNode();
|
||||
final FocusNode pwFocus = FocusNode();
|
||||
|
||||
// State
|
||||
bool saveId = false;
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
LoginControllerWithUseCase() {
|
||||
// UseCase 초기화
|
||||
final authService = inject<AuthService>();
|
||||
_loginUseCase = LoginUseCase(authService);
|
||||
_checkAuthStatusUseCase = CheckAuthStatusUseCase(authService);
|
||||
|
||||
// 초기 인증 상태 확인
|
||||
_checkInitialAuthStatus();
|
||||
}
|
||||
|
||||
/// 초기 인증 상태 확인
|
||||
Future<void> _checkInitialAuthStatus() async {
|
||||
final result = await _checkAuthStatusUseCase(const NoParams());
|
||||
result.fold(
|
||||
(failure) => debugPrint('인증 상태 확인 실패: ${failure.message}'),
|
||||
(isAuthenticated) {
|
||||
if (isAuthenticated) {
|
||||
debugPrint('이미 로그인된 상태입니다.');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 아이디 저장 체크박스 상태 변경
|
||||
void setSaveId(bool value) {
|
||||
saveId = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 에러 메시지 초기화
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 로그인 처리
|
||||
Future<bool> login() async {
|
||||
// 입력값 검증 (UI 레벨)
|
||||
if (idController.text.trim().isEmpty) {
|
||||
_errorMessage = '아이디 또는 이메일을 입력해주세요.';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pwController.text.isEmpty) {
|
||||
_errorMessage = '비밀번호를 입력해주세요.';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 로딩 시작
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
// 입력값이 이메일인지 username인지 판단
|
||||
final inputValue = idController.text.trim();
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
final isEmail = emailRegex.hasMatch(inputValue);
|
||||
|
||||
try {
|
||||
// UseCase 실행
|
||||
final params = LoginParams(
|
||||
email: isEmail ? inputValue : '$inputValue@superport.kr', // username인 경우 임시 도메인 추가
|
||||
password: pwController.text,
|
||||
);
|
||||
|
||||
debugPrint('[LoginController] 로그인 시도: ${params.email}');
|
||||
|
||||
final result = await _loginUseCase(params).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () async {
|
||||
debugPrint('[LoginController] 로그인 요청 타임아웃');
|
||||
return Left(NetworkFailure(
|
||||
message: '요청 시간이 초과되었습니다. 네트워크 연결을 확인해주세요.',
|
||||
));
|
||||
},
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
debugPrint('[LoginController] 로그인 실패: ${failure.message}');
|
||||
|
||||
// 실패 타입에 따른 메시지 처리
|
||||
if (failure is ValidationFailure) {
|
||||
_errorMessage = failure.message;
|
||||
} else if (failure is AuthenticationFailure) {
|
||||
_errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.';
|
||||
} else if (failure is NetworkFailure) {
|
||||
_errorMessage = '네트워크 연결을 확인해주세요.';
|
||||
} else if (failure is ServerFailure) {
|
||||
_errorMessage = '서버 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.';
|
||||
} else {
|
||||
_errorMessage = failure.message;
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
},
|
||||
(loginResponse) {
|
||||
debugPrint('[LoginController] 로그인 성공');
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[LoginController] 예상치 못한 에러: $e');
|
||||
_errorMessage = '로그인 중 오류가 발생했습니다.';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 헬스체크 실행
|
||||
Future<bool> performHealthCheck() async {
|
||||
debugPrint('[LoginController] 헬스체크 시작');
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final healthResult = await _healthCheckService.checkHealth();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
// HealthCheckService가 Map을 반환하는 경우 적절히 변환
|
||||
final isHealthy = healthResult is bool ? healthResult :
|
||||
(healthResult is Map && healthResult['status'] == 'healthy');
|
||||
|
||||
if (isHealthy == false) {
|
||||
_errorMessage = '서버와 연결할 수 없습니다.\n잠시 후 다시 시도해주세요.';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('[LoginController] 헬스체크 실패: $e');
|
||||
_errorMessage = '서버 상태 확인 중 오류가 발생했습니다.';
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
idController.dispose();
|
||||
pwController.dispose();
|
||||
idFocus.dispose();
|
||||
pwFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,7 @@ class _UserListState extends State<UserList> {
|
||||
// 초기 데이터 로드
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final controller = context.read<UserListController>();
|
||||
controller.pageSize = 10; // 페이지 크기 설정
|
||||
controller.loadUsers();
|
||||
controller.initialize(pageSize: 10); // 통일된 초기화 방식
|
||||
});
|
||||
|
||||
// 검색 디바운싱
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/controllers/base_list_controller.dart';
|
||||
import '../../../core/utils/error_handler.dart';
|
||||
import '../../../data/models/common/pagination_params.dart';
|
||||
import '../../../data/models/warehouse/warehouse_dto.dart';
|
||||
import '../../../domain/usecases/warehouse_location/warehouse_location_usecases.dart';
|
||||
|
||||
/// UseCase 패턴을 적용한 창고 위치 목록 컨트롤러
|
||||
class WarehouseLocationListControllerWithUseCase extends BaseListController<WarehouseLocationDto> {
|
||||
final GetWarehouseLocationsUseCase getWarehouseLocationsUseCase;
|
||||
final CreateWarehouseLocationUseCase createWarehouseLocationUseCase;
|
||||
final UpdateWarehouseLocationUseCase updateWarehouseLocationUseCase;
|
||||
final DeleteWarehouseLocationUseCase deleteWarehouseLocationUseCase;
|
||||
|
||||
// 선택된 항목들
|
||||
final Set<int> _selectedLocationIds = {};
|
||||
Set<int> get selectedLocationIds => _selectedLocationIds;
|
||||
|
||||
// 필터 옵션
|
||||
bool _showActiveOnly = true;
|
||||
String? _filterByManager;
|
||||
|
||||
bool get showActiveOnly => _showActiveOnly;
|
||||
String? get filterByManager => _filterByManager;
|
||||
|
||||
WarehouseLocationListControllerWithUseCase({
|
||||
required this.getWarehouseLocationsUseCase,
|
||||
required this.createWarehouseLocationUseCase,
|
||||
required this.updateWarehouseLocationUseCase,
|
||||
required this.deleteWarehouseLocationUseCase,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PagedResult<WarehouseLocationDto>> fetchData({
|
||||
required PaginationParams params,
|
||||
Map<String, dynamic>? additionalFilters,
|
||||
}) async {
|
||||
try {
|
||||
// 필터 파라미터 구성
|
||||
final filters = <String, dynamic>{};
|
||||
if (_showActiveOnly) filters['is_active'] = true;
|
||||
if (_filterByManager != null) filters['manager'] = _filterByManager;
|
||||
|
||||
final updatedParams = params.copyWith(filters: filters);
|
||||
final getParams = GetWarehouseLocationsParams.fromPaginationParams(updatedParams);
|
||||
|
||||
final result = await getWarehouseLocationsUseCase(getParams);
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(locationsResponse) {
|
||||
// PagedResult로 래핑하여 반환
|
||||
final meta = PaginationMeta(
|
||||
currentPage: params.page,
|
||||
perPage: params.perPage,
|
||||
total: locationsResponse.items.length,
|
||||
totalPages: (locationsResponse.items.length / params.perPage).ceil(),
|
||||
hasNext: locationsResponse.items.length >= params.perPage,
|
||||
hasPrevious: params.page > 1,
|
||||
);
|
||||
return PagedResult(items: locationsResponse.items, meta: meta);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('데이터 로드 실패: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 생성
|
||||
Future<void> createWarehouseLocation({
|
||||
required String name,
|
||||
required String address,
|
||||
String? description,
|
||||
String? contactNumber,
|
||||
String? manager,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = CreateWarehouseLocationParams(
|
||||
name: name,
|
||||
address: address,
|
||||
description: description,
|
||||
contactNumber: contactNumber,
|
||||
manager: manager,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
);
|
||||
|
||||
final result = await createWarehouseLocationUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(location) async => await refresh(),
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 수정
|
||||
Future<void> updateWarehouseLocation({
|
||||
required int id,
|
||||
String? name,
|
||||
String? address,
|
||||
String? description,
|
||||
String? contactNumber,
|
||||
String? manager,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
bool? isActive,
|
||||
}) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final params = UpdateWarehouseLocationParams(
|
||||
id: id,
|
||||
name: name,
|
||||
address: address,
|
||||
description: description,
|
||||
contactNumber: contactNumber,
|
||||
manager: manager,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
isActive: isActive,
|
||||
);
|
||||
|
||||
final result = await updateWarehouseLocationUseCase(params);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(location) async => updateItemLocally(location, (item) => item.id == location.id),
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 삭제
|
||||
Future<void> deleteWarehouseLocation(int id) async {
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final result = await deleteWarehouseLocationUseCase(id);
|
||||
|
||||
await result.fold(
|
||||
(failure) async => errorState = failure.message,
|
||||
(_) async {
|
||||
removeItemLocally((item) => item.id == id);
|
||||
_selectedLocationIds.remove(id);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 위치 활성/비활성 토글
|
||||
Future<void> toggleLocationStatus(int id) async {
|
||||
final location = items.firstWhere((item) => item.id == id);
|
||||
await updateWarehouseLocation(
|
||||
id: id,
|
||||
isActive: !location.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
/// 필터 설정
|
||||
void setFilters({
|
||||
bool? showActiveOnly,
|
||||
String? manager,
|
||||
}) {
|
||||
if (showActiveOnly != null) _showActiveOnly = showActiveOnly;
|
||||
_filterByManager = manager;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_showActiveOnly = true;
|
||||
_filterByManager = null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// 창고 위치 선택 토글
|
||||
void toggleLocationSelection(int id) {
|
||||
if (_selectedLocationIds.contains(id)) {
|
||||
_selectedLocationIds.remove(id);
|
||||
} else {
|
||||
_selectedLocationIds.add(id);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 모든 창고 위치 선택
|
||||
void selectAll() {
|
||||
_selectedLocationIds.clear();
|
||||
_selectedLocationIds.addAll(items.map((e) => e.id));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택 해제
|
||||
void clearSelection() {
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 삭제
|
||||
Future<void> deleteSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
final errors = <String>[];
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
final result = await deleteWarehouseLocationUseCase(id);
|
||||
result.fold(
|
||||
(failure) => errors.add('Location $id: ${failure.message}'),
|
||||
(_) => removeItemLocally((item) => item.id == id),
|
||||
);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
errorState = '일부 창고 위치 삭제 실패:\n${errors.join('\n')}';
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 활성화
|
||||
Future<void> activateSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
await updateWarehouseLocation(id: id, isActive: true);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 선택된 창고 위치 일괄 비활성화
|
||||
Future<void> deactivateSelectedLocations() async {
|
||||
if (_selectedLocationIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
isLoadingState = true;
|
||||
|
||||
for (final id in _selectedLocationIds.toList()) {
|
||||
await updateWarehouseLocation(id: id, isActive: false);
|
||||
}
|
||||
|
||||
_selectedLocationIds.clear();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
errorState = '오류 발생: $e';
|
||||
} finally {
|
||||
isLoadingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 드롭다운용 활성 창고 위치 목록 가져오기
|
||||
List<WarehouseLocationDto> getActiveLocations() {
|
||||
return items.where((location) => location.isActive).toList();
|
||||
}
|
||||
|
||||
/// 특정 관리자의 창고 위치 목록 가져오기
|
||||
List<WarehouseLocationDto> getLocationsByManager(String manager) {
|
||||
return items.where((location) => location.managerName == manager).toList(); // managerName 필드 사용
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectedLocationIds.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class _WarehouseLocationListState
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = WarehouseLocationListController();
|
||||
_controller.pageSize = 10; // 페이지 크기를 10으로 설정
|
||||
// 초기 데이터 로드
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_controller.loadWarehouseLocations();
|
||||
@@ -145,8 +146,8 @@ class _WarehouseLocationListState
|
||||
dataTable: _buildDataTable(pagedLocations),
|
||||
|
||||
// 페이지네이션
|
||||
pagination: totalCount > controller.pageSize ? Pagination(
|
||||
totalCount: totalCount,
|
||||
pagination: controller.totalPages > 1 ? Pagination(
|
||||
totalCount: controller.total,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) {
|
||||
@@ -162,7 +163,8 @@ class _WarehouseLocationListState
|
||||
|
||||
/// 데이터 테이블
|
||||
Widget _buildDataTable(List<WarehouseLocation> pagedLocations) {
|
||||
if (pagedLocations.isEmpty) {
|
||||
// 전체 데이터가 없는지 확인 (API의 total 사용)
|
||||
if (_controller.total == 0 && pagedLocations.isEmpty) {
|
||||
return StandardEmptyState(
|
||||
title:
|
||||
_controller.searchQuery.isNotEmpty
|
||||
|
||||
Reference in New Issue
Block a user