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,176 @@
# 백엔드 API 구현 요청 사항
## 1. 시리얼 번호 중복 체크 API
### 요청 사항
장비 입고 시 시리얼 번호 중복을 방지하기 위한 API가 필요합니다.
### API 스펙
#### Endpoint
```
POST /api/v1/equipment/check-serial
```
#### Request Body
```json
{
"serialNumber": "SN123456789"
}
```
#### Response
**성공 (200 OK) - 사용 가능한 시리얼 번호**
```json
{
"available": true,
"message": "사용 가능한 시리얼 번호입니다."
}
```
**성공 (200 OK) - 중복된 시리얼 번호**
```json
{
"available": false,
"message": "이미 등록된 시리얼 번호입니다.",
"existingEquipment": {
"id": 123,
"name": "장비명",
"companyName": "회사명",
"warehouseLocation": "창고 위치"
}
}
```
**실패 (400 Bad Request) - 잘못된 요청**
```json
{
"error": "시리얼 번호를 입력해주세요."
}
```
### 구현 요구사항
1. **중복 체크 로직**
- equipment 테이블의 serial_number 컬럼에서 중복 확인
- 대소문자 구분 없이 체크 (case-insensitive)
- 공백 제거 후 비교 (trim)
2. **성능 고려사항**
- serial_number 컬럼에 인덱스 필요
- 빠른 응답을 위한 최적화
3. **보안 고려사항**
- SQL Injection 방지
- Rate limiting 적용 (분당 60회 제한)
### 프론트엔드 통합 코드
```dart
// services/equipment_service.dart
Future<bool> checkSerialNumberAvailability(String serialNumber) async {
final response = await dio.post(
'/equipment/check-serial',
data: {'serialNumber': serialNumber},
);
if (response.statusCode == 200) {
return response.data['available'] ?? false;
}
throw Exception('시리얼 번호 확인 실패');
}
// controllers/equipment_form_controller.dart
Future<String?> validateSerialNumber(String? value) async {
if (value == null || value.isEmpty) {
return '시리얼 번호를 입력해주세요.';
}
// 실시간 중복 체크
final isAvailable = await _equipmentService.checkSerialNumberAvailability(value);
if (!isAvailable) {
return '이미 등록된 시리얼 번호입니다.';
}
return null;
}
```
## 2. 벌크 시리얼 번호 체크 API (추가 제안)
### 요청 사항
여러 장비를 한 번에 등록할 때 시리얼 번호들을 일괄 체크하는 API
### API 스펙
#### Endpoint
```
POST /api/v1/equipment/check-serial-bulk
```
#### Request Body
```json
{
"serialNumbers": ["SN001", "SN002", "SN003"]
}
```
#### Response
```json
{
"results": [
{
"serialNumber": "SN001",
"available": true
},
{
"serialNumber": "SN002",
"available": false,
"existingEquipmentId": 456
},
{
"serialNumber": "SN003",
"available": true
}
],
"summary": {
"total": 3,
"available": 2,
"duplicates": 1
}
}
```
## 3. 시리얼 번호 유니크 제약 조건
### 데이터베이스 스키마 변경 요청
```sql
-- equipment 테이블에 유니크 제약 조건 추가
ALTER TABLE equipment
ADD CONSTRAINT unique_serial_number
UNIQUE (serial_number);
-- 성능을 위한 인덱스 추가 (이미 유니크 제약에 포함되지만 명시적으로)
CREATE INDEX idx_equipment_serial_number
ON equipment(LOWER(TRIM(serial_number)));
```
## 구현 우선순위
1. **Phase 1 (즉시)**: 단일 시리얼 번호 체크 API
2. **Phase 2 (선택)**: 벌크 시리얼 번호 체크 API
3. **Phase 3 (필수)**: DB 유니크 제약 조건
## 예상 일정
- API 구현: 2-3시간
- 테스트: 1시간
- 배포: 30분
---
작성일: 2025-01-09
작성자: Frontend Team
상태: 백엔드 팀 검토 대기중

View File

@@ -0,0 +1,120 @@
# Mock Service 제거 계획
> 작성일: 2025-01-09
> 목적: Real API 전용으로 전환 (2025-01-07 결정사항)
## 📋 제거 대상 (25개 파일)
### Controllers - List (8개)
- [x] `user_list_controller_refactored.dart`
- [x] `company_list_controller_refactored.dart`
- [x] `warehouse_location_list_controller_refactored.dart`
- [ ] `warehouse_location_list_controller.dart` (구버전)
- [ ] `user_list_controller.dart` (구버전)
- [ ] `company_list_controller.dart` (구버전)
- [x] `equipment_list_controller_refactored.dart` (새로 생성) ✅
- [ ] `license_list_controller.dart`
### Controllers - Form (5개)
- [ ] `equipment_in_form_controller.dart`
- [ ] `equipment_out_form_controller.dart`
- [ ] `warehouse_location_form_controller.dart`
- [ ] `user_form_controller.dart`
- [ ] `license_form_controller.dart`
### Views (9개)
- [ ] `company_list_redesign.dart`
- [ ] `equipment_list_redesign.dart`
- [ ] `license_list_redesign.dart`
- [ ] `user_list_redesign.dart`
- [ ] `equipment_in_form.dart`
- [ ] `equipment_out_form.dart`
- [ ] `company_form.dart`
- [ ] `license_form.dart`
- [ ] `user_form.dart`
### Core (4개)
- [ ] `main.dart`
- [x] `base_list_controller.dart`
- [ ] `auth_service.dart`
- [ ] `mock_data_service.dart` (파일 삭제)
## 🔄 제거 패턴
### 1. Controller 수정 패턴
```dart
// BEFORE
class SomeController extends ChangeNotifier {
final MockDataService? dataService;
bool _useApi = true;
Future<void> loadData() async {
if (_useApi && _service != null) {
// API 호출
} else {
// Mock 데이터 사용
}
}
}
// AFTER
class SomeController extends ChangeNotifier {
// MockDataService 완전 제거
// useApi 플래그 제거
Future<void> loadData() async {
// API 호출만 유지
}
}
```
### 2. View 수정 패턴
```dart
// BEFORE
ChangeNotifierProvider(
create: (_) => SomeController(
dataService: GetIt.instance<MockDataService>(),
),
)
// AFTER
ChangeNotifierProvider(
create: (_) => SomeController(),
)
```
### 3. BaseListController 수정
```dart
// useApi 파라미터 제거
// Mock 관련 로직 제거
```
## ⚠️ 주의사항
1. **GetIt 의존성**: MockDataService가 DI에 등록되지 않았으므로 직접 전달 부분만 제거
2. **테스트 코드**: 테스트에서 Mock을 사용하는 경우 별도 처리 필요
3. **Environment.useApi**: 환경변수 자체는 유지 (향후 완전 제거)
4. **백업**: 중요 변경사항이므로 커밋 전 백업 필수
## 📊 진행 상황
- **시작**: 2025-01-09
- **예상 완료**: 2025-01-09
- **진행률**: 5/26 파일 (19%)
- **완료 항목**:
- BaseListController (useApi 파라미터 제거)
- WarehouseLocationListControllerRefactored (Mock 코드 제거)
- CompanyListControllerRefactored (Mock 코드 제거)
- UserListControllerRefactored (Mock 코드 제거)
- EquipmentListControllerRefactored (새로 생성, Mock 없이 구현)
## 🔧 실행 순서
1. BaseListController에서 useApi 관련 로직 제거
2. Refactored Controllers 수정 (3개)
3. 기존 Controllers 수정 (나머지)
4. Form Controllers 수정
5. Views 수정
6. Core 파일 정리
7. MockDataService 파일 삭제
8. 테스트 실행 및 검증

300
docs/usecase_guide.md Normal file
View File

@@ -0,0 +1,300 @@
# UseCase 패턴 가이드
## 📌 개요
UseCase 패턴은 Clean Architecture의 핵심 개념으로, 비즈니스 로직을 캡슐화하여 재사용성과 테스트 용이성을 높입니다.
## 🏗️ 구조
```
lib/
├── domain/
│ └── usecases/
│ ├── base_usecase.dart # 기본 UseCase 인터페이스
│ ├── auth/ # 인증 관련 UseCase
│ │ ├── login_usecase.dart
│ │ ├── logout_usecase.dart
│ │ └── ...
│ ├── company/ # 회사 관리 UseCase
│ │ ├── get_companies_usecase.dart
│ │ ├── create_company_usecase.dart
│ │ └── ...
│ └── ...
```
## 🔑 핵심 개념
### 1. UseCase 추상 클래스
```dart
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
```
- **Type**: 성공 시 반환할 데이터 타입
- **Params**: UseCase 실행에 필요한 파라미터
- **Either**: 실패(Left) 또는 성공(Right) 결과를 담는 컨테이너
### 2. Failure 클래스
```dart
abstract class Failure {
final String message;
final String? code;
final dynamic originalError;
}
```
다양한 실패 타입:
- **ServerFailure**: 서버 에러
- **NetworkFailure**: 네트워크 에러
- **AuthFailure**: 인증 에러
- **ValidationFailure**: 유효성 검증 에러
- **PermissionFailure**: 권한 에러
## 📝 UseCase 구현 예시
### 1. 로그인 UseCase
```dart
class LoginUseCase extends UseCase<LoginResponse, LoginParams> {
final AuthService _authService;
LoginUseCase(this._authService);
@override
Future<Either<Failure, LoginResponse>> call(LoginParams params) async {
try {
// 1. 유효성 검증
if (!_isValidEmail(params.email)) {
return Left(ValidationFailure(
message: '올바른 이메일 형식이 아닙니다.',
));
}
// 2. 비즈니스 로직 실행
final response = await _authService.login(
LoginRequest(
email: params.email,
password: params.password,
),
);
// 3. 성공 결과 반환
return Right(response);
} on DioException catch (e) {
// 4. 에러 처리
return Left(_handleDioError(e));
}
}
}
```
### 2. 파라미터가 없는 UseCase
```dart
class LogoutUseCase extends UseCase<void, NoParams> {
final AuthService _authService;
LogoutUseCase(this._authService);
@override
Future<Either<Failure, void>> call(NoParams params) async {
try {
await _authService.logout();
return const Right(null);
} catch (e) {
return Left(UnknownFailure(
message: '로그아웃 중 오류가 발생했습니다.',
));
}
}
}
```
## 🎯 Controller에서 UseCase 사용
### 1. UseCase 초기화
```dart
class LoginControllerWithUseCase extends ChangeNotifier {
late final LoginUseCase _loginUseCase;
LoginControllerWithUseCase() {
final authService = inject<AuthService>();
_loginUseCase = LoginUseCase(authService);
}
}
```
### 2. UseCase 실행
```dart
Future<bool> login() async {
final params = LoginParams(
email: emailController.text,
password: passwordController.text,
);
final result = await _loginUseCase(params);
return result.fold(
(failure) {
// 실패 처리
_errorMessage = failure.message;
notifyListeners();
return false;
},
(loginResponse) {
// 성공 처리
return true;
},
);
}
```
## 🧪 테스트 작성
### 1. UseCase 단위 테스트
```dart
void main() {
late LoginUseCase loginUseCase;
late MockAuthService mockAuthService;
setUp(() {
mockAuthService = MockAuthService();
loginUseCase = LoginUseCase(mockAuthService);
});
test('로그인 성공 테스트', () async {
// Given
const params = LoginParams(
email: 'test@example.com',
password: 'password123',
);
final expectedResponse = LoginResponse(...);
when(mockAuthService.login(any))
.thenAnswer((_) async => expectedResponse);
// When
final result = await loginUseCase(params);
// Then
expect(result.isRight(), true);
result.fold(
(failure) => fail('Should not fail'),
(response) => expect(response, expectedResponse),
);
});
test('잘못된 이메일 형식 테스트', () async {
// Given
const params = LoginParams(
email: 'invalid-email',
password: 'password123',
);
// When
final result = await loginUseCase(params);
// Then
expect(result.isLeft(), true);
result.fold(
(failure) => expect(failure, isA<ValidationFailure>()),
(response) => fail('Should not succeed'),
);
});
}
```
### 2. Controller 테스트
```dart
void main() {
late LoginControllerWithUseCase controller;
late MockLoginUseCase mockLoginUseCase;
setUp(() {
mockLoginUseCase = MockLoginUseCase();
controller = LoginControllerWithUseCase();
controller._loginUseCase = mockLoginUseCase;
});
test('로그인 버튼 클릭 시 UseCase 호출', () async {
// Given
controller.emailController.text = 'test@example.com';
controller.passwordController.text = 'password123';
when(mockLoginUseCase(any))
.thenAnswer((_) async => Right(LoginResponse()));
// When
final result = await controller.login();
// Then
expect(result, true);
verify(mockLoginUseCase(any)).called(1);
});
}
```
## 💡 장점
1. **단일 책임 원칙**: 각 UseCase는 하나의 비즈니스 로직만 담당
2. **테스트 용이성**: 비즈니스 로직을 독립적으로 테스트 가능
3. **재사용성**: 여러 Controller에서 동일한 UseCase 재사용
4. **의존성 역전**: Controller가 구체적인 Service가 아닌 UseCase에 의존
5. **에러 처리 표준화**: Either 패턴으로 일관된 에러 처리
## 📋 구현 체크리스트
### UseCase 생성 시
- [ ] UseCase 클래스 생성 (base_usecase 상속)
- [ ] 파라미터 클래스 정의 (필요한 경우)
- [ ] 유효성 검증 로직 구현
- [ ] 에러 처리 구현
- [ ] 성공/실패 케이스 모두 처리
### Controller 리팩토링 시
- [ ] UseCase 의존성 주입
- [ ] 비즈니스 로직을 UseCase 호출로 대체
- [ ] Either 패턴으로 결과 처리
- [ ] 에러 메시지 사용자 친화적으로 변환
### 테스트 작성 시
- [ ] UseCase 단위 테스트
- [ ] 성공 케이스 테스트
- [ ] 실패 케이스 테스트
- [ ] 경계값 테스트
- [ ] Controller 통합 테스트
## 🔄 마이그레이션 전략
### Phase 1: 핵심 기능부터 시작
1. 인증 관련 기능 (로그인, 로그아웃)
2. CRUD 기본 기능
3. 복잡한 비즈니스 로직
### Phase 2: 점진적 확산
1. 새로운 기능은 UseCase 패턴으로 구현
2. 기존 코드는 리팩토링 시 UseCase 적용
3. 테스트 커버리지 확보
### Phase 3: 완전 마이그레이션
1. 모든 비즈니스 로직 UseCase화
2. Service 레이어는 데이터 액세스만 담당
3. Controller는 UI 로직만 담당
## 📚 참고 자료
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Either Pattern in Dart](https://pub.dev/packages/dartz)
- [Flutter Clean Architecture](https://resocoder.com/flutter-clean-architecture-tdd/)
---
**작성일**: 2025-01-09
**버전**: 1.0