feat: 소프트 딜리트 기능 전면 구현 완료
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

## 주요 변경사항
- Company, Equipment, License, Warehouse Location 모든 화면에 소프트 딜리트 구현
- 관리자 권한으로 삭제된 데이터 조회 가능 (includeInactive 파라미터)
- 데이터 무결성 보장을 위한 논리 삭제 시스템 완성

## 기능 개선
- 각 리스트 컨트롤러에 toggleIncludeInactive() 메서드 추가
- UI에 "비활성 포함" 체크박스 추가 (관리자 전용)
- API 데이터소스에 includeInactive 파라미터 지원

## 문서 정리
- 불필요한 문서 파일 제거 및 재구성
- CLAUDE.md 프로젝트 상태 업데이트 (진행률 80%)
- 테스트 결과 문서화 (test20250812v01.md)

## UI 컴포넌트
- Equipment 화면 위젯 모듈화 (custom_dropdown_field, equipment_basic_info_section)
- 폼 유효성 검증 강화

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-08-12 20:02:54 +09:00
parent 1645182b38
commit e7860ae028
48 changed files with 2096 additions and 1242 deletions

View File

@@ -1,176 +0,0 @@
# 백엔드 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

@@ -1,120 +0,0 @@
# 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. 테스트 실행 및 검증

340
docs/superportPRD.md Normal file
View File

@@ -0,0 +1,340 @@
**아래 내용 전체를 복사해서** `superportPRD.md` **파일로 저장하시면 됩니다.**
---
# supERPort ERP - 초기 프론트엔드 PRD (Flutter)
본 문서는 Flutter로 제작할 ERP 웹/앱 프론트엔드의 최소 기능 요구사항과 디렉터리 구조, 스타일 가이드, 그리고 AI 코드 생성에 활용 가능한 프롬프트를 포함하고 있습니다. 현재 계획은 확장 가능하며, 추후 대화와 요구사항 변경에 따라 자동으로 업데이트할 수 있습니다.
## 1. 개요
- **이름**: supERPort
- **기술 스택**: Flutter
- **주요 화면**:
1. 장비 입고
2. 장비 출고
3. 회사 등록
4. 사용자 등록
5. 라이센스 등록
## 2. 공통 스타일 가이드
- **디자인 레퍼런스**: [Metronic Admin Template](https://themeforest.net/item/metronic-responsive-admin-dashboard-template/4021469)
- **디자인 레퍼런스를 철저히 검토 및 적용**: Metronic의 Layout, 컬러 팔레트, 컴포넌트, 인터랙션 가이드라인을 꼼꼼하게 분석하여, Flutter 및 Material Icons와 조화롭게 적용해야 합니다.
- 하나의 화면에 중복되는 정보를 제공하지 않아야 합니다.
- **아이콘**: Material Icons
- **UI 기조**: 모던하고 직관적인 플랫 디자인, 적절한 그림자(Elevation)와 여백 사용
- **메인 컬러 예시**:
- Primary: #5867dd (Metronic 기본색)
- Secondary: #34bfa3
- Background: #f7f8fa
- **타이포그래피**: 가독성 높은 산세리프 폰트(예: Roboto, Noto Sans)
- **반응형 고려**: Web & Mobile 겸용, Responsive Layout 구성
## 3. 기능 요구사항
### 3.1 장비 입고 화면
- **등록 후 리스트로 관리**
- 새로운 장비 입고 기록을 추가 (Form)
- 등록된 목록을 리스트(테이블)로 보여주고, 항목별 Editing 기능 제공
- **필수 데이터**
1. **제조사명** (varchar 필수)
2. **장비명** (varchar 필수)
3. **대분류 / 중분류 / 소분류** (varchar, Email 선택 UI 유사 방식)
4. **시리얼 넘버** (varchar)
5. **바코드 입력** (varchar, 차후에 바코드 스캔 기능과 연동 예정)
6. **물품 수** (int 필수)
- 시리얼 번호가 존재할 경우 1 고정 및 수정 불가
- 시리얼 번호가 없으면 직접 입력
7. **입고일** (datetime)
- 당일 날짜를 기본값으로 설정
- 캘린더에서 과거 날짜만 선택 가능
8. (DB 연계 시) **장비(장비이력) 테이블**과 매핑
- **입출고**는 “I”(입고)로 기록
- **발생시간**: 입고 시점(= 입고일)
### 3.2 장비 출고 화면
- **등록 후 리스트로 관리**
- 새 출고 정보 등록 (Form)
- 출고 기록 목록 표시 및 편집 기능
- **필수 데이터**
1. **장비명** (varchar 필수)
2. **대분류 / 중분류 / 소분류** (출고 시점에 불러오고 수정 가능 여부는 정책 결정)
3. **시리얼 넘버** (varchar)
4. **바코드** (varchar, 바코드 스캔 기능 연동 예정)
5. **출고 수량** (int 필수)
6. **출고일** (datetime)
7. (DB 연계 시) **장비(장비이력) 테이블**과 매핑
- **입출고**는 “O”(출고)로 기록
- **발생시간**: 출고 시점(= 출고일)
### 3.3 회사 등록 화면
- **등록 후 리스트로 관리**
- 새 회사 정보 등록 (Form)
- 회사 목록 조회 및 편집
- **필수 데이터**
1. **회사명** (varchar 필수)
2. **주소** (varchar)
3. (확장) **지점 정보** (Optional / 별도 화면 구성 가능)
- 지점명 (varchar)
- 대표전화번호 (varchar)
- 주소 (varchar)
4. (DB 연계 시) **고객(회사) / 고객(지점)** 테이블 매핑
- 회사 ID, 지점 ID 등 Primary Key, FK 관계
### 3.4 사용자 등록 화면
- **등록 후 리스트로 관리**
- 사용자(직원) 가입/등록 Form
- 사용자 목록 조회 및 편집
- **필수 데이터**
1. **이름** (varchar 필수)
2. **소속 회사ID** (int)
3. **관리등급** (char) 예) S(관리자), M(멤버)
4. (DB 연계 시) **서비스(직원)** 테이블 매핑
### 3.5 라이센스 등록 화면
- **등록 후 리스트로 관리**
- 유지보수 라이센스 정보 등록 (Form)
- 등록된 라이센스 목록 표시 및 편집
- **필수 데이터**
1. **서비스(회사)ID** (int) 라이센스가 적용될 회사/서비스 정보
2. **라이센스명** (varchar, 예: “1년 유지보수”)
3. **라이센스기간** (int, 월 단위)
4. **방문주기** (int, 유지보수 방문 주기)
5. (DB 연계 시) **유지(라이센스)** 테이블과 매핑
## 4. 추천 디렉터리 구조
동일한 기능을 담은 모듈(파일)이 300라인을 초과하게 될 경우, **하위 파일로 분리**해 관리하는 것을 권장합니다.
장비(Equipment) 모델과 장비 입고(In) / 출고(Out) 모델을 **분리**하여, 공통 필드와 입·출고 전용 필드를 구분해 유지보수를 용이하게 합니다.
lib/
┣ models/
┃ ┣ equipment_model.dart (장비(Equipment) 공통 모델)
┃ ┣ equipment_in_model.dart (장비 입고 관련 모델, 장비 + 입고 필드)
┃ ┣ equipment_out_model.dart (장비 출고 관련 모델, 장비 + 출고 필드)
┃ ┣ company_model.dart (회사 정보 모델)
┃ ┣ user_model.dart (사용자 정보 모델)
┃ ┗ license_model.dart (유지보수 라이센스 정보 모델)
┣ screens/
┃ ┣ equipment_in/
┃ ┃ ┣ equipment_in_list.dart (장비 입고 리스트 화면)
┃ ┃ ┗ equipment_in_form.dart (장비 입고 등록/수정 폼)
┃ ┣ equipment_out/
┃ ┃ ┣ equipment_out_list.dart (장비 출고 리스트 화면)
┃ ┃ ┗ equipment_out_form.dart (장비 출고 등록/수정 폼)
┃ ┣ company/
┃ ┃ ┣ company_list.dart (회사 목록 화면)
┃ ┃ ┗ company_form.dart (회사 등록/수정 폼)
┃ ┣ user/
┃ ┃ ┣ user_list.dart (사용자 목록 화면)
┃ ┃ ┗ user_form.dart (사용자 등록/수정 폼)
┃ ┣ license/
┃ ┃ ┣ license_list.dart (라이센스 목록 화면)
┃ ┃ ┗ license_form.dart (라이센스 등록/수정 폼)
┃ ┗ common/
┃ ┣ custom_widgets.dart (공통 위젯)
┃ ┗ theme.dart (스타일, 테마 정보)
┣ services/
┃ ┗ mock_data_service.dart (서버 없는 샘플 데이터 관리)
┣ utils/
┃ ┣ validators.dart (입력값 검증 함수)
┃ ┗ constants.dart (상수 관리, 예: 라우트명, 컬러코드)
┗ main.dart
## 5. 데이터베이스 설계
아래는 장비, 회사(고객), 서비스(직원/결제), 유지보수 라이센스 등에 대한 **예시** 테이블입니다. 실제 구현 시에는 필요에 따라 테이블을 통합하거나 컬럼명을 조정할 수 있습니다.
### 5.1 고객 관련 테이블
|테이블명|필드명|타입|예시|비고|
|---|---|---|---|---|
|고객(회사)|ID|Integer|1|Primary Key|
||회사명|Varchar|LG전자|고객 회사명|
||주소|Varchar|서울시 종로구|고객 회사 주소|
|고객(지점)|ID|Integer|10|Primary Key|
||회사ID|Integer|1|FK - 고객(회사)|
||지점명|Varchar|본사|고객 지점명|
||주소|Varchar|서울시 종로구|지점 주소|
||대표전화번호|Varchar|02-3403-2222|지점 연락처|
### 5.2 서비스 관련 테이블
|테이블명|필드명|타입|예시|비고|
|---|---|---|---|---|
|서비스(직원)|ID|Integer|20|Primary Key|
||회사ID|Integer|1|FK - 서비스(회사) 또는 고객(회사)|
||이름|Varchar|홍길동|직원 이름|
||관리등급|Char|M|S(관리자)/M(멤버)|
|서비스(결제)|ID|Integer|30|Primary Key|
||서비스(직원)ID|Integer|20|FK - 서비스(직원)|
||결제여부|Char|A|승인(A)/반려(D)|
||결제일|Datetime|2025-03-04 11:00|결제 일시|
### 5.3 장비 관련 테이블
|테이블명|필드명|타입|예시|비고|
|---|---|---|---|---|
|장비(장비정보)|ID|Integer|50|Primary Key|
||장비(회사명)ID|Integer|1|FK - 장비(회사명) or 고객(회사)|
||장비명|Varchar|라우터 123|장비 모델명|
|장비(장비이력)|ID|Integer|60|Primary Key|
||장비(장비바코드)ID|Integer|50|FK - 장비(장비바코드)|
||입출고|Varchar|I|입고(I)/출고(O)/임대(R) 등 구분|
||발생시간|Datetime|2025-03-04 11:00|이력 발생 시간|
### 5.4 유지보수 라이센스 관련 테이블
|테이블명|필드명|타입|예시|비고|
|---|---|---|---|---|
|유지(라이센스)|ID|Integer|70|Primary Key|
||서비스(회사)ID|Integer|1|FK - 서비스(회사)|
||라이센스명|Varchar|1년 유지보수|유지보수 라이센스명|
||라이센스기간|Integer|12|월 단위|
||방문주기|Integer|1|유지보수 방문 주기|
> 위 테이블들은 예시이며, 실제 구현 시에는 **API 요청/응답 형식**과 **화면 요구사항**에 따라 컬럼을 추가/수정/제거할 수 있습니다.
## 6. 코드 생성용 AI 프롬프트 예시
(아래 텍스트는 코드 자동생성용 프롬프트 작성 예시일 뿐, 실제 환경에 맞춰 수정해서 사용하세요.)
[System]
You are Claude, a large language model trained by Anthropic.
[User]
read document "doc/doc name" at first.
- 앱 이름: supERPort
- 화면 개요: 장비 입고, 장비 출고, 회사 등록, 사용자 등록
- 참조 스타일: Metronic Admin + Material Icons
- 필수 폴더 구조와 기능
1. equipment_in (리스트 & 폼)
2. equipment_out (리스트 & 폼)
3. company (리스트 & 폼)
4. user (리스트 & 폼)
Generate Flutter code with the above requirements.
- Use a consistent coding style.
- Provide minimal working code example for each screen.
- Utilize Material Icons.
- Implement basic validation in the forms.
## 7. 추후 업데이트 사항
- 장비 출고 시 필수 데이터 상세
- 회사 등록/사용자 등록 시 필수 데이터 구조 정의 (지점 정보, 연락처 등 확장)
- Form 유효성 검사 규칙 세부화
- 권한(관리자/사용자)별 접근 제어
- 바코드 스캔 기능 연동
- API 연동 및 서버 연결
- **유지보수 라이센스** 관련 화면 및 기능 확대 (결제/계약 기간 연동 등)
---
본 문서는 ERP 플랫폼 “supERPort”의 Flutter 프론트엔드 개발에 필요한 **최소 요구사항**, **디렉터리 구조**, **스타일 가이드**, 그리고 **데이터베이스 설계** 정보를 담고 있습니다. 여기서 정의되지 않은 사항은 추후 대화에서 확정된 후 문서에 자동 반영될 예정입니다. 필요에 따라 Markdown 형식으로 다운로드해, 버전 관리 시스템에 추가하거나 직접 열람할 수 있습니다.

View File

@@ -1,300 +0,0 @@
# 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