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

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

View File

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

View File

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