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,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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user