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

## 주요 변경사항

### 아키텍처 개선
- Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리)
- Use Case 패턴 도입으로 비즈니스 로직 캡슐화
- Repository 패턴으로 데이터 접근 추상화
- 의존성 주입 구조 개선

### 상태 관리 최적화
- 모든 Controller에서 불필요한 상태 관리 로직 제거
- 페이지네이션 로직 통일 및 간소화
- 에러 처리 로직 개선 (에러 메시지 한글화)
- 로딩 상태 관리 최적화

### Mock 서비스 제거
- MockDataService 완전 제거
- 모든 화면을 실제 API 전용으로 전환
- 불필요한 Mock 관련 코드 정리

### UI/UX 개선
- Overview 화면 대시보드 기능 강화
- 라이선스 만료 알림 위젯 추가
- 사이드바 네비게이션 개선
- 일관된 UI 컴포넌트 사용

### 코드 품질
- 중복 코드 제거 및 함수 추출
- 파일별 책임 분리 명확화
- 테스트 코드 업데이트

## 영향 범위
- 모든 화면의 Controller 리팩토링
- API 통신 레이어 구조 개선
- 에러 처리 및 로깅 시스템 개선

## 향후 계획
- 단위 테스트 커버리지 확대
- 통합 테스트 시나리오 추가
- 성능 모니터링 도구 통합
This commit is contained in:
JiWoong Sul
2025-08-11 00:04:28 +09:00
parent 6b5d126990
commit 162fe08618
113 changed files with 11072 additions and 3319 deletions

View File

@@ -0,0 +1,85 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/license/license_dto.dart';
import '../../../data/repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 만료일 체크 UseCase
@injectable
class CheckLicenseExpiryUseCase implements UseCase<LicenseExpiryResult, CheckLicenseExpiryParams> {
final LicenseRepository repository;
CheckLicenseExpiryUseCase(this.repository);
@override
Future<Either<Failure, LicenseExpiryResult>> call(CheckLicenseExpiryParams params) async {
try {
// 모든 라이선스 조회
final allLicenses = await repository.getLicenses(
page: 1,
perPage: 10000, // 모든 라이선스 조회
);
final now = DateTime.now();
final expiring30Days = <LicenseDto>[];
final expiring60Days = <LicenseDto>[];
final expiring90Days = <LicenseDto>[];
final expired = <LicenseDto>[];
for (final license in allLicenses.items) {
if (license.expiryDate == null) continue;
final daysUntilExpiry = license.expiryDate!.difference(now).inDays;
if (daysUntilExpiry < 0) {
expired.add(license);
} else if (daysUntilExpiry <= 30) {
expiring30Days.add(license);
} else if (daysUntilExpiry <= 60) {
expiring60Days.add(license);
} else if (daysUntilExpiry <= 90) {
expiring90Days.add(license);
}
}
return Right(LicenseExpiryResult(
expiring30Days: expiring30Days,
expiring60Days: expiring60Days,
expiring90Days: expiring90Days,
expired: expired,
));
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 라이선스 만료일 체크 파라미터
class CheckLicenseExpiryParams {
final int? companyId;
final String? equipmentType;
CheckLicenseExpiryParams({
this.companyId,
this.equipmentType,
});
}
/// 라이선스 만료일 체크 결과
class LicenseExpiryResult {
final List<LicenseDto> expiring30Days;
final List<LicenseDto> expiring60Days;
final List<LicenseDto> expiring90Days;
final List<LicenseDto> expired;
LicenseExpiryResult({
required this.expiring30Days,
required this.expiring60Days,
required this.expiring90Days,
required this.expired,
});
int get totalExpiring => expiring30Days.length + expiring60Days.length + expiring90Days.length;
int get totalExpired => expired.length;
}

View File

@@ -0,0 +1,68 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/license/license_dto.dart';
import '../../../data/repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 생성 UseCase
@injectable
class CreateLicenseUseCase implements UseCase<LicenseDto, CreateLicenseParams> {
final LicenseRepository repository;
CreateLicenseUseCase(this.repository);
@override
Future<Either<Failure, LicenseDto>> call(CreateLicenseParams params) async {
try {
// 비즈니스 로직: 만료일 검증
if (params.expiryDate.isBefore(params.startDate)) {
return Left(ValidationFailure(message: '만료일은 시작일 이후여야 합니다'));
}
// 비즈니스 로직: 최소 라이선스 기간 검증 (30일)
final duration = params.expiryDate.difference(params.startDate).inDays;
if (duration < 30) {
return Left(ValidationFailure(message: '라이선스 기간은 최소 30일 이상이어야 합니다'));
}
final license = await repository.createLicense(params.toMap());
return Right(license);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 라이선스 생성 파라미터
class CreateLicenseParams {
final int equipmentId;
final int companyId;
final String licenseType;
final DateTime startDate;
final DateTime expiryDate;
final String? description;
final double? cost;
CreateLicenseParams({
required this.equipmentId,
required this.companyId,
required this.licenseType,
required this.startDate,
required this.expiryDate,
this.description,
this.cost,
});
Map<String, dynamic> toMap() {
return {
'equipment_id': equipmentId,
'company_id': companyId,
'license_type': licenseType,
'start_date': startDate.toIso8601String(),
'expiry_date': expiryDate.toIso8601String(),
'description': description,
'cost': cost,
};
}
}

View File

@@ -0,0 +1,29 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 삭제 UseCase
@injectable
class DeleteLicenseUseCase implements UseCase<bool, int> {
final LicenseRepository repository;
DeleteLicenseUseCase(this.repository);
@override
Future<Either<Failure, bool>> call(int id) async {
try {
// 비즈니스 로직: 활성 라이선스는 삭제 불가
final license = await repository.getLicenseDetail(id);
if (license.isActive) {
return Left(ValidationFailure(message: '활성 라이선스는 삭제할 수 없습니다'));
}
await repository.deleteLicense(id);
return const Right(true);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,24 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/license/license_dto.dart';
import '../../../data/repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 상세 조회 UseCase
@injectable
class GetLicenseDetailUseCase implements UseCase<LicenseDto, int> {
final LicenseRepository repository;
GetLicenseDetailUseCase(this.repository);
@override
Future<Either<Failure, LicenseDto>> call(int id) async {
try {
final license = await repository.getLicenseDetail(id);
return Right(license);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,55 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/common/pagination_params.dart';
import '../../../data/models/license/license_dto.dart';
import '../../../data/repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 목록 조회 UseCase
@injectable
class GetLicensesUseCase implements UseCase<LicenseListResponseDto, GetLicensesParams> {
final LicenseRepository repository;
GetLicensesUseCase(this.repository);
@override
Future<Either<Failure, LicenseListResponseDto>> call(GetLicensesParams params) async {
try {
final licenses = await repository.getLicenses(
page: params.page,
perPage: params.perPage,
search: params.search,
filters: params.filters,
);
return Right(licenses);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 라이선스 목록 조회 파라미터
class GetLicensesParams {
final int page;
final int perPage;
final String? search;
final Map<String, dynamic>? filters;
GetLicensesParams({
this.page = 1,
this.perPage = 20,
this.search,
this.filters,
});
/// PaginationParams로부터 변환
factory GetLicensesParams.fromPaginationParams(PaginationParams params) {
return GetLicensesParams(
page: params.page,
perPage: params.perPage,
search: params.search,
filters: params.filters,
);
}
}

View File

@@ -0,0 +1,7 @@
// License UseCase barrel file
export 'get_licenses_usecase.dart';
export 'get_license_detail_usecase.dart';
export 'create_license_usecase.dart';
export 'update_license_usecase.dart';
export 'delete_license_usecase.dart';
export 'check_license_expiry_usecase.dart';

View File

@@ -0,0 +1,75 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/license/license_dto.dart';
import '../../../data/repositories/license_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 라이선스 수정 UseCase
@injectable
class UpdateLicenseUseCase implements UseCase<LicenseDto, UpdateLicenseParams> {
final LicenseRepository repository;
UpdateLicenseUseCase(this.repository);
@override
Future<Either<Failure, LicenseDto>> call(UpdateLicenseParams params) async {
try {
// 비즈니스 로직: 만료일 검증
if (params.expiryDate != null && params.startDate != null) {
if (params.expiryDate!.isBefore(params.startDate!)) {
return Left(ValidationFailure(message: '만료일은 시작일 이후여야 합니다'));
}
// 비즈니스 로직: 최소 라이선스 기간 검증 (30일)
final duration = params.expiryDate!.difference(params.startDate!).inDays;
if (duration < 30) {
return Left(ValidationFailure(message: '라이선스 기간은 최소 30일 이상이어야 합니다'));
}
}
final license = await repository.updateLicense(params.id, params.toMap());
return Right(license);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 라이선스 수정 파라미터
class UpdateLicenseParams {
final int id;
final int? equipmentId;
final int? companyId;
final String? licenseType;
final DateTime? startDate;
final DateTime? expiryDate;
final String? description;
final double? cost;
final String? status;
UpdateLicenseParams({
required this.id,
this.equipmentId,
this.companyId,
this.licenseType,
this.startDate,
this.expiryDate,
this.description,
this.cost,
this.status,
});
Map<String, dynamic> toMap() {
final Map<String, dynamic> data = {};
if (equipmentId != null) data['equipment_id'] = equipmentId;
if (companyId != null) data['company_id'] = companyId;
if (licenseType != null) data['license_type'] = licenseType;
if (startDate != null) data['start_date'] = startDate!.toIso8601String();
if (expiryDate != null) data['expiry_date'] = expiryDate!.toIso8601String();
if (description != null) data['description'] = description;
if (cost != null) data['cost'] = cost;
if (status != null) data['status'] = status;
return data;
}
}