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,75 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/warehouse/warehouse_dto.dart';
import '../../../data/repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 창고 위치 생성 UseCase
@injectable
class CreateWarehouseLocationUseCase implements UseCase<WarehouseLocationDto, CreateWarehouseLocationParams> {
final WarehouseLocationRepository repository;
CreateWarehouseLocationUseCase(this.repository);
@override
Future<Either<Failure, WarehouseLocationDto>> call(CreateWarehouseLocationParams params) async {
try {
// 비즈니스 로직: 이름 중복 체크
if (params.name.isEmpty) {
return Left(ValidationFailure(message: '창고 위치 이름은 필수입니다'));
}
// 비즈니스 로직: 주소 유효성 검증
if (params.address.isEmpty) {
return Left(ValidationFailure(message: '창고 주소는 필수입니다'));
}
// 비즈니스 로직: 연락처 형식 검증
if (params.contactNumber != null && params.contactNumber!.isNotEmpty) {
final phoneRegex = RegExp(r'^[\d\-\+\(\)\s]+$');
if (!phoneRegex.hasMatch(params.contactNumber!)) {
return Left(ValidationFailure(message: '올바른 연락처 형식이 아닙니다'));
}
}
final location = await repository.createWarehouseLocation(params.toMap());
return Right(location);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 창고 위치 생성 파라미터
class CreateWarehouseLocationParams {
final String name;
final String address;
final String? description;
final String? contactNumber;
final String? manager;
final double? latitude;
final double? longitude;
CreateWarehouseLocationParams({
required this.name,
required this.address,
this.description,
this.contactNumber,
this.manager,
this.latitude,
this.longitude,
});
Map<String, dynamic> toMap() {
return {
'name': name,
'address': address,
'description': description,
'contact_number': contactNumber,
'manager': manager,
'latitude': latitude,
'longitude': longitude,
};
}
}

View File

@@ -0,0 +1,30 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 창고 위치 삭제 UseCase
@injectable
class DeleteWarehouseLocationUseCase implements UseCase<bool, int> {
final WarehouseLocationRepository repository;
DeleteWarehouseLocationUseCase(this.repository);
@override
Future<Either<Failure, bool>> call(int id) async {
try {
// 비즈니스 로직: 사용 중인 창고는 삭제 불가
// TODO: 창고에 장비가 있는지 확인하는 로직 추가 필요
// final hasEquipment = await repository.checkWarehouseHasEquipment(id);
// if (hasEquipment) {
// return Left(ValidationFailure(message: '장비가 있는 창고는 삭제할 수 없습니다'));
// }
await repository.deleteWarehouseLocation(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/warehouse/warehouse_dto.dart';
import '../../../data/repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 창고 위치 상세 조회 UseCase
@injectable
class GetWarehouseLocationDetailUseCase implements UseCase<WarehouseLocationDto, int> {
final WarehouseLocationRepository repository;
GetWarehouseLocationDetailUseCase(this.repository);
@override
Future<Either<Failure, WarehouseLocationDto>> call(int id) async {
try {
final location = await repository.getWarehouseLocationDetail(id);
return Right(location);
} 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/warehouse/warehouse_dto.dart';
import '../../../data/repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 창고 위치 목록 조회 UseCase
@injectable
class GetWarehouseLocationsUseCase implements UseCase<WarehouseLocationListDto, GetWarehouseLocationsParams> {
final WarehouseLocationRepository repository;
GetWarehouseLocationsUseCase(this.repository);
@override
Future<Either<Failure, WarehouseLocationListDto>> call(GetWarehouseLocationsParams params) async {
try {
final locations = await repository.getWarehouseLocations(
page: params.page,
perPage: params.perPage,
search: params.search,
filters: params.filters,
);
return Right(locations);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 창고 위치 목록 조회 파라미터
class GetWarehouseLocationsParams {
final int page;
final int perPage;
final String? search;
final Map<String, dynamic>? filters;
GetWarehouseLocationsParams({
this.page = 1,
this.perPage = 20,
this.search,
this.filters,
});
/// PaginationParams로부터 변환
factory GetWarehouseLocationsParams.fromPaginationParams(PaginationParams params) {
return GetWarehouseLocationsParams(
page: params.page,
perPage: params.perPage,
search: params.search,
filters: params.filters,
);
}
}

View File

@@ -0,0 +1,88 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../data/models/warehouse/warehouse_dto.dart';
import '../../../data/repositories/warehouse_location_repository.dart';
import '../../../core/errors/failures.dart';
import '../base_usecase.dart';
/// 창고 위치 수정 UseCase
@injectable
class UpdateWarehouseLocationUseCase implements UseCase<WarehouseLocationDto, UpdateWarehouseLocationParams> {
final WarehouseLocationRepository repository;
UpdateWarehouseLocationUseCase(this.repository);
@override
Future<Either<Failure, WarehouseLocationDto>> call(UpdateWarehouseLocationParams params) async {
try {
// 비즈니스 로직: 이름 유효성 검증
if (params.name != null && params.name!.isEmpty) {
return Left(ValidationFailure(message: '창고 위치 이름은 비어있을 수 없습니다'));
}
// 비즈니스 로직: 주소 유효성 검증
if (params.address != null && params.address!.isEmpty) {
return Left(ValidationFailure(message: '창고 주소는 비어있을 수 없습니다'));
}
// 비즈니스 로직: 연락처 형식 검증
if (params.contactNumber != null && params.contactNumber!.isNotEmpty) {
final phoneRegex = RegExp(r'^[\d\-\+\(\)\s]+$');
if (!phoneRegex.hasMatch(params.contactNumber!)) {
return Left(ValidationFailure(message: '올바른 연락처 형식이 아닙니다'));
}
}
// 비즈니스 로직: 좌표 유효성 검증
if (params.latitude != null && (params.latitude! < -90 || params.latitude! > 90)) {
return Left(ValidationFailure(message: '유효하지 않은 위도값입니다'));
}
if (params.longitude != null && (params.longitude! < -180 || params.longitude! > 180)) {
return Left(ValidationFailure(message: '유효하지 않은 경도값입니다'));
}
final location = await repository.updateWarehouseLocation(params.id, params.toMap());
return Right(location);
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
}
/// 창고 위치 수정 파라미터
class UpdateWarehouseLocationParams {
final int id;
final String? name;
final String? address;
final String? description;
final String? contactNumber;
final String? manager;
final double? latitude;
final double? longitude;
final bool? isActive;
UpdateWarehouseLocationParams({
required this.id,
this.name,
this.address,
this.description,
this.contactNumber,
this.manager,
this.latitude,
this.longitude,
this.isActive,
});
Map<String, dynamic> toMap() {
final Map<String, dynamic> data = {};
if (name != null) data['name'] = name;
if (address != null) data['address'] = address;
if (description != null) data['description'] = description;
if (contactNumber != null) data['contact_number'] = contactNumber;
if (manager != null) data['manager'] = manager;
if (latitude != null) data['latitude'] = latitude;
if (longitude != null) data['longitude'] = longitude;
if (isActive != null) data['is_active'] = isActive;
return data;
}
}

View File

@@ -0,0 +1,6 @@
// WarehouseLocation UseCase barrel file
export 'get_warehouse_locations_usecase.dart';
export 'get_warehouse_location_detail_usecase.dart';
export 'create_warehouse_location_usecase.dart';
export 'update_warehouse_location_usecase.dart';
export 'delete_warehouse_location_usecase.dart';