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:
@@ -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