refactor: Repository 패턴 적용 및 Clean Architecture 완성
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled

## 주요 변경사항

### 🏗️ 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:
JiWoong Sul
2025-08-11 20:14:10 +09:00
parent d64aa26157
commit 731dcd816b
105 changed files with 5225 additions and 3941 deletions

View File

@@ -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();
}
}

View File

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