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:
456
lib/data/repositories/company_repository_impl.dart
Normal file
456
lib/data/repositories/company_repository_impl.dart
Normal file
@@ -0,0 +1,456 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../core/errors/failures.dart';
|
||||
import '../../domain/repositories/company_repository.dart';
|
||||
import '../../models/company_model.dart';
|
||||
import '../../models/address_model.dart';
|
||||
import '../datasources/remote/company_remote_datasource.dart';
|
||||
import '../models/common/paginated_response.dart';
|
||||
import '../models/company/company_dto.dart';
|
||||
import '../models/company/branch_dto.dart';
|
||||
import '../models/company/company_list_dto.dart';
|
||||
|
||||
/// 회사 관리 Repository 구현체
|
||||
/// 회사 및 지점 정보 CRUD 작업을 처리하며 도메인 모델과 API DTO 간 변환을 담당
|
||||
@Injectable(as: CompanyRepository)
|
||||
class CompanyRepositoryImpl implements CompanyRepository {
|
||||
final CompanyRemoteDataSource remoteDataSource;
|
||||
|
||||
CompanyRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, PaginatedResponse<Company>>> getCompanies({
|
||||
int? page,
|
||||
int? limit,
|
||||
String? search,
|
||||
CompanyType? companyType,
|
||||
String? sortBy,
|
||||
String? sortOrder,
|
||||
}) async {
|
||||
try {
|
||||
final result = await remoteDataSource.getCompanies(
|
||||
page: page ?? 1,
|
||||
perPage: limit ?? 20,
|
||||
search: search,
|
||||
isActive: null, // companyType에 따른 필터링 로직 필요 시 추가
|
||||
);
|
||||
|
||||
// DTO를 도메인 모델로 변환
|
||||
final companies = result.items.map((dto) => _mapDtoToDomain(dto)).toList();
|
||||
|
||||
final paginatedResult = PaginatedResponse<Company>(
|
||||
items: companies,
|
||||
page: result.page,
|
||||
size: result.size,
|
||||
totalElements: result.totalElements,
|
||||
totalPages: result.totalPages,
|
||||
first: result.first,
|
||||
last: result.last,
|
||||
);
|
||||
|
||||
return Right(paginatedResult);
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(
|
||||
message: '회사 목록 조회 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Company>> getCompanyById(int id) async {
|
||||
try {
|
||||
final result = await remoteDataSource.getCompanyWithBranches(id);
|
||||
final company = _mapDetailDtoToDomain(result);
|
||||
return Right(company);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('404')) {
|
||||
return Left(NotFoundFailure(
|
||||
message: '해당 회사를 찾을 수 없습니다.',
|
||||
resourceType: 'Company',
|
||||
resourceId: id.toString(),
|
||||
));
|
||||
}
|
||||
return Left(ServerFailure(
|
||||
message: '회사 상세 정보 조회 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Company>> createCompany(Company company) async {
|
||||
try {
|
||||
final request = _mapDomainToCreateRequest(company);
|
||||
final result = await remoteDataSource.createCompany(request);
|
||||
final createdCompany = _mapResponseToDomain(result);
|
||||
return Right(createdCompany);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('중복')) {
|
||||
return Left(DuplicateFailure(
|
||||
message: '이미 존재하는 회사명입니다.',
|
||||
field: 'name',
|
||||
value: company.name,
|
||||
));
|
||||
}
|
||||
if (e.toString().contains('유효성')) {
|
||||
return Left(ValidationFailure(
|
||||
message: '입력 데이터가 올바르지 않습니다.',
|
||||
));
|
||||
}
|
||||
return Left(ServerFailure(
|
||||
message: '회사 생성 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Company>> updateCompany(int id, Company company) async {
|
||||
try {
|
||||
final request = _mapDomainToUpdateRequest(company);
|
||||
final result = await remoteDataSource.updateCompany(id, request);
|
||||
final updatedCompany = _mapResponseToDomain(result);
|
||||
return Right(updatedCompany);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('404')) {
|
||||
return Left(NotFoundFailure(
|
||||
message: '수정할 회사를 찾을 수 없습니다.',
|
||||
resourceType: 'Company',
|
||||
resourceId: id.toString(),
|
||||
));
|
||||
}
|
||||
if (e.toString().contains('중복')) {
|
||||
return Left(DuplicateFailure(
|
||||
message: '이미 존재하는 회사명입니다.',
|
||||
field: 'name',
|
||||
value: company.name,
|
||||
));
|
||||
}
|
||||
return Left(ServerFailure(
|
||||
message: '회사 정보 수정 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> deleteCompany(int id) async {
|
||||
try {
|
||||
await remoteDataSource.deleteCompany(id);
|
||||
return const Right(null);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('404')) {
|
||||
return Left(NotFoundFailure(
|
||||
message: '삭제할 회사를 찾을 수 없습니다.',
|
||||
resourceType: 'Company',
|
||||
resourceId: id.toString(),
|
||||
));
|
||||
}
|
||||
if (e.toString().contains('참조')) {
|
||||
return Left(BusinessFailure(
|
||||
message: '해당 회사에 연결된 데이터가 있어 삭제할 수 없습니다.',
|
||||
));
|
||||
}
|
||||
return Left(ServerFailure(
|
||||
message: '회사 삭제 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Company>> toggleCompanyStatus(int id) async {
|
||||
try {
|
||||
// 현재 회사 정보 조회
|
||||
final currentCompany = await remoteDataSource.getCompanyDetail(id);
|
||||
final newStatus = !currentCompany.isActive;
|
||||
|
||||
// 상태 업데이트
|
||||
await remoteDataSource.updateCompanyStatus(id, newStatus);
|
||||
|
||||
// 업데이트된 회사 정보 재조회
|
||||
final updatedCompany = await remoteDataSource.getCompanyDetail(id);
|
||||
final company = _mapResponseToDomain(updatedCompany);
|
||||
|
||||
return Right(company);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('404')) {
|
||||
return Left(NotFoundFailure(
|
||||
message: '상태를 변경할 회사를 찾을 수 없습니다.',
|
||||
resourceType: 'Company',
|
||||
resourceId: id.toString(),
|
||||
));
|
||||
}
|
||||
return Left(ServerFailure(
|
||||
message: '회사 상태 변경 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Branch>> createBranch(int companyId, Branch branch) async {
|
||||
try {
|
||||
final request = _mapBranchToCreateRequest(branch);
|
||||
final result = await remoteDataSource.createBranch(companyId, request);
|
||||
final createdBranch = _mapBranchResponseToDomain(result);
|
||||
return Right(createdBranch);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('404')) {
|
||||
return Left(NotFoundFailure(
|
||||
message: '해당 회사를 찾을 수 없습니다.',
|
||||
resourceType: 'Company',
|
||||
resourceId: companyId.toString(),
|
||||
));
|
||||
}
|
||||
return Left(ServerFailure(
|
||||
message: '지점 생성 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Branch>> updateBranch(int companyId, int branchId, Branch branch) async {
|
||||
try {
|
||||
final request = _mapBranchToUpdateRequest(branch);
|
||||
final result = await remoteDataSource.updateBranch(companyId, branchId, request);
|
||||
final updatedBranch = _mapBranchResponseToDomain(result);
|
||||
return Right(updatedBranch);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('404')) {
|
||||
return Left(NotFoundFailure(
|
||||
message: '수정할 지점을 찾을 수 없습니다.',
|
||||
resourceType: 'Branch',
|
||||
resourceId: branchId.toString(),
|
||||
));
|
||||
}
|
||||
return Left(ServerFailure(
|
||||
message: '지점 정보 수정 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> deleteBranch(int companyId, int branchId) async {
|
||||
try {
|
||||
await remoteDataSource.deleteBranch(companyId, branchId);
|
||||
return const Right(null);
|
||||
} catch (e) {
|
||||
if (e.toString().contains('404')) {
|
||||
return Left(NotFoundFailure(
|
||||
message: '삭제할 지점을 찾을 수 없습니다.',
|
||||
resourceType: 'Branch',
|
||||
resourceId: branchId.toString(),
|
||||
));
|
||||
}
|
||||
return Left(ServerFailure(
|
||||
message: '지점 삭제 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<String>>> searchCompanyNames(String query, {int? limit}) async {
|
||||
try {
|
||||
final companies = await remoteDataSource.searchCompanies(query);
|
||||
final names = companies.map((company) => company.name).take(limit ?? 10).toList();
|
||||
return Right(names);
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(
|
||||
message: '회사명 검색 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<CompanyType, int>>> getCompanyCountByType() async {
|
||||
// TODO: API에서 회사 유형별 통계 기능이 구현되면 추가
|
||||
return const Left(ServerFailure(
|
||||
message: '회사 유형별 통계 기능이 아직 구현되지 않았습니다.',
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> hasLinkedUsers(int companyId) async {
|
||||
// TODO: 회사에 연결된 사용자 존재 여부 확인 API 구현 필요
|
||||
try {
|
||||
// 임시로 false 반환 - API 구현 후 수정 필요
|
||||
return const Right(false);
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(
|
||||
message: '연결된 사용자 확인 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> hasLinkedEquipment(int companyId) async {
|
||||
// TODO: 회사에 연결된 장비 존재 여부 확인 API 구현 필요
|
||||
try {
|
||||
// 임시로 false 반환 - API 구현 후 수정 필요
|
||||
return const Right(false);
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(
|
||||
message: '연결된 장비 확인 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> isDuplicateCompanyName(String name, {int? excludeId}) async {
|
||||
try {
|
||||
final isDuplicate = await remoteDataSource.checkDuplicateCompany(name);
|
||||
// excludeId가 있는 경우 해당 ID 제외 로직 추가 필요
|
||||
return Right(isDuplicate);
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(
|
||||
message: '중복 회사명 확인 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Private 매퍼 메서드들
|
||||
|
||||
Company _mapDtoToDomain(CompanyListDto dto) {
|
||||
return Company(
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
address: Address.fromFullAddress(dto.address ?? ''),
|
||||
contactName: dto.contactName,
|
||||
contactPosition: null, // CompanyListDto에 없음
|
||||
contactPhone: dto.contactPhone,
|
||||
contactEmail: dto.contactEmail,
|
||||
companyTypes: _parseCompanyTypes(dto.companyTypes),
|
||||
remark: null, // CompanyListDto에 없음
|
||||
branches: [], // 목록에서는 지점 정보 비어있음
|
||||
);
|
||||
}
|
||||
|
||||
Company _mapDetailDtoToDomain(CompanyWithBranches dto) {
|
||||
return Company(
|
||||
id: dto.company.id,
|
||||
name: dto.company.name,
|
||||
address: Address.fromFullAddress(dto.company.address ?? ''),
|
||||
contactName: dto.company.contactName,
|
||||
contactPosition: dto.company.contactPosition,
|
||||
contactPhone: dto.company.contactPhone,
|
||||
contactEmail: dto.company.contactEmail,
|
||||
companyTypes: _parseCompanyTypes(dto.company.companyTypes),
|
||||
remark: dto.company.remark,
|
||||
branches: dto.branches.map((branch) => _mapBranchDtoToDomain(branch)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Company _mapResponseToDomain(CompanyResponse response) {
|
||||
return Company(
|
||||
id: response.id,
|
||||
name: response.name,
|
||||
address: Address.fromFullAddress(response.address ?? ''),
|
||||
contactName: response.contactName,
|
||||
contactPosition: response.contactPosition,
|
||||
contactPhone: response.contactPhone,
|
||||
contactEmail: response.contactEmail,
|
||||
companyTypes: _parseCompanyTypes(response.companyTypes),
|
||||
remark: response.remark,
|
||||
branches: [], // CompanyResponse에서는 지점 정보 따로 조회
|
||||
);
|
||||
}
|
||||
|
||||
Branch _mapBranchDtoToDomain(BranchListDto dto) {
|
||||
return Branch(
|
||||
id: dto.id,
|
||||
companyId: dto.companyId,
|
||||
name: dto.branchName,
|
||||
address: Address.fromFullAddress(dto.address ?? ''),
|
||||
contactName: dto.managerName,
|
||||
contactPosition: null, // BranchListDto에 없음
|
||||
contactPhone: dto.phone,
|
||||
contactEmail: null, // BranchListDto에 없음
|
||||
remark: null, // BranchListDto에 없음
|
||||
);
|
||||
}
|
||||
|
||||
Branch _mapBranchResponseToDomain(BranchResponse response) {
|
||||
return Branch(
|
||||
id: response.id,
|
||||
companyId: response.companyId,
|
||||
name: response.branchName,
|
||||
address: Address.fromFullAddress(response.address ?? ''),
|
||||
contactName: response.managerName,
|
||||
contactPosition: null,
|
||||
contactPhone: response.phone,
|
||||
contactEmail: null,
|
||||
remark: response.remark,
|
||||
);
|
||||
}
|
||||
|
||||
/// API에서 받은 문자열 리스트를 CompanyType enum 리스트로 변환
|
||||
/// 지원하는 형식: ['customer', 'partner'] 또는 ['고객사', '파트너사']
|
||||
List<CompanyType> _parseCompanyTypes(List<String>? types) {
|
||||
if (types == null || types.isEmpty) return [CompanyType.customer];
|
||||
|
||||
return types.map((type) {
|
||||
final lowerType = type.toLowerCase().trim();
|
||||
// API 문자열 형식 매칭
|
||||
if (lowerType == 'partner' || lowerType.contains('partner') || lowerType == '파트너사') {
|
||||
return CompanyType.partner;
|
||||
}
|
||||
// 기본값은 customer
|
||||
return CompanyType.customer;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// CompanyType enum을 API 문자열로 변환
|
||||
String _mapCompanyTypeToApiString(CompanyType type) {
|
||||
switch (type) {
|
||||
case CompanyType.partner:
|
||||
return 'partner';
|
||||
case CompanyType.customer:
|
||||
return 'customer';
|
||||
}
|
||||
}
|
||||
|
||||
CreateCompanyRequest _mapDomainToCreateRequest(Company company) {
|
||||
return CreateCompanyRequest(
|
||||
name: company.name,
|
||||
address: company.address.toString(),
|
||||
contactName: company.contactName ?? '',
|
||||
contactPosition: company.contactPosition ?? '',
|
||||
contactPhone: company.contactPhone ?? '',
|
||||
contactEmail: company.contactEmail ?? '',
|
||||
companyTypes: company.companyTypes.map((type) => _mapCompanyTypeToApiString(type)).toList(),
|
||||
remark: company.remark,
|
||||
);
|
||||
}
|
||||
|
||||
UpdateCompanyRequest _mapDomainToUpdateRequest(Company company) {
|
||||
return UpdateCompanyRequest(
|
||||
name: company.name,
|
||||
address: company.address.toString(),
|
||||
contactName: company.contactName,
|
||||
contactPosition: company.contactPosition,
|
||||
contactPhone: company.contactPhone,
|
||||
contactEmail: company.contactEmail,
|
||||
companyTypes: company.companyTypes.map((type) => _mapCompanyTypeToApiString(type)).toList(),
|
||||
remark: company.remark,
|
||||
isActive: null, // UpdateCompanyRequest에서 필요한 경우 추가
|
||||
);
|
||||
}
|
||||
|
||||
CreateBranchRequest _mapBranchToCreateRequest(Branch branch) {
|
||||
return CreateBranchRequest(
|
||||
branchName: branch.name,
|
||||
address: branch.address.toString(),
|
||||
phone: branch.contactPhone ?? '',
|
||||
managerName: branch.contactName,
|
||||
managerPhone: null, // Branch에 없음
|
||||
remark: branch.remark,
|
||||
);
|
||||
}
|
||||
|
||||
UpdateBranchRequest _mapBranchToUpdateRequest(Branch branch) {
|
||||
return UpdateBranchRequest(
|
||||
branchName: branch.name,
|
||||
address: branch.address.toString(),
|
||||
phone: branch.contactPhone,
|
||||
managerName: branch.contactName,
|
||||
managerPhone: null, // Branch에 없음
|
||||
remark: branch.remark,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user