refactor: Clean Architecture 적용 및 코드베이스 전면 리팩토링
## 주요 변경사항 ### 아키텍처 개선 - Clean Architecture 패턴 적용 (Domain, Data, Presentation 레이어 분리) - Use Case 패턴 도입으로 비즈니스 로직 캡슐화 - Repository 패턴으로 데이터 접근 추상화 - 의존성 주입 구조 개선 ### 상태 관리 최적화 - 모든 Controller에서 불필요한 상태 관리 로직 제거 - 페이지네이션 로직 통일 및 간소화 - 에러 처리 로직 개선 (에러 메시지 한글화) - 로딩 상태 관리 최적화 ### Mock 서비스 제거 - MockDataService 완전 제거 - 모든 화면을 실제 API 전용으로 전환 - 불필요한 Mock 관련 코드 정리 ### UI/UX 개선 - Overview 화면 대시보드 기능 강화 - 라이선스 만료 알림 위젯 추가 - 사이드바 네비게이션 개선 - 일관된 UI 컴포넌트 사용 ### 코드 품질 - 중복 코드 제거 및 함수 추출 - 파일별 책임 분리 명확화 - 테스트 코드 업데이트 ## 영향 범위 - 모든 화면의 Controller 리팩토링 - API 통신 레이어 구조 개선 - 에러 처리 및 로깅 시스템 개선 ## 향후 계획 - 단위 테스트 커버리지 확대 - 통합 테스트 시나리오 추가 - 성능 모니터링 도구 통합
This commit is contained in:
515
Refactoring.md
Normal file
515
Refactoring.md
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
# Superport 프로젝트 리팩토링 계획
|
||||||
|
|
||||||
|
> 작성일: 2025-01-09
|
||||||
|
> 프로젝트 진행률: 70%
|
||||||
|
> 분석 범위: Flutter Frontend 코드베이스
|
||||||
|
|
||||||
|
## 📊 프로젝트 현황 요약
|
||||||
|
|
||||||
|
### 완성도 현황
|
||||||
|
- ✅ **완료**: 인증, 회사 관리, 사용자 관리, 창고 위치, 장비 입고, 라이선스 관리
|
||||||
|
- 🔄 **진행중**: 장비 출고(70%), 대시보드(70%), 검색/필터(70%)
|
||||||
|
- ⏳ **미시작**: 장비 대여/폐기, 보고서 생성, 모바일 최적화, 알림 시스템
|
||||||
|
|
||||||
|
### 기술 스택
|
||||||
|
- Frontend: Flutter Web (Provider 상태관리)
|
||||||
|
- Backend: Rust (Actix-Web) + PostgreSQL
|
||||||
|
- API: REST API (JWT 인증)
|
||||||
|
|
||||||
|
## 🔴 Critical Issues (즉시 수정 필요)
|
||||||
|
|
||||||
|
### 1. 시리얼 번호 중복 체크 미구현
|
||||||
|
**위치**: 장비 입고 프로세스
|
||||||
|
**문제**: 백엔드에서 중복 체크가 구현되지 않아 프론트엔드에서만 임시 검증
|
||||||
|
**영향도**: HIGH - 데이터 무결성 위협
|
||||||
|
**해결방안**:
|
||||||
|
```dart
|
||||||
|
// 백엔드 API 구현 필요
|
||||||
|
POST /api/v1/equipment/check-serial
|
||||||
|
{
|
||||||
|
"serialNumber": "SN123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프론트엔드 수정
|
||||||
|
Future<bool> checkSerialDuplicate(String serialNumber) async {
|
||||||
|
return await _equipmentService.checkSerialNumber(serialNumber);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 권한 체크 누락
|
||||||
|
**위치**: `warehouse_location`, `overview` 화면
|
||||||
|
**문제**: 역할 기반 접근 제어(RBAC) 미적용
|
||||||
|
**영향도**: HIGH - 보안 취약점
|
||||||
|
**해결방안**:
|
||||||
|
```dart
|
||||||
|
// 권한 체크 미들웨어 추가
|
||||||
|
class AuthGuard extends StatelessWidget {
|
||||||
|
final List<String> allowedRoles;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final user = context.watch<AuthService>().currentUser;
|
||||||
|
if (!allowedRoles.contains(user?.role)) {
|
||||||
|
return UnauthorizedScreen();
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🟡 Major Refactoring (단기 개선)
|
||||||
|
|
||||||
|
### 1. 컨트롤러 중복 코드 제거
|
||||||
|
**문제**: 모든 List Controller에 동일한 페이지네이션, 검색, 필터 로직 중복
|
||||||
|
**파일들**:
|
||||||
|
- `equipment_list_controller.dart`
|
||||||
|
- `company_list_controller.dart`
|
||||||
|
- `user_list_controller.dart`
|
||||||
|
- `license_list_controller.dart`
|
||||||
|
|
||||||
|
**리팩토링 방안**:
|
||||||
|
```dart
|
||||||
|
// 공통 Base Controller 생성
|
||||||
|
abstract class BaseListController<T> extends ChangeNotifier {
|
||||||
|
List<T> items = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
String searchQuery = '';
|
||||||
|
int currentPage = 1;
|
||||||
|
int perPage = 20;
|
||||||
|
bool hasMore = true;
|
||||||
|
|
||||||
|
// 공통 메서드들
|
||||||
|
Future<void> loadData({bool isRefresh = false});
|
||||||
|
void search(String query);
|
||||||
|
void clearError();
|
||||||
|
Future<void> refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 개별 컨트롤러는 BaseListController 상속
|
||||||
|
class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||||
|
@override
|
||||||
|
Future<void> loadData({bool isRefresh = false}) async {
|
||||||
|
// 장비 특화 로직만 구현
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Mock 서비스 제거
|
||||||
|
**문제**: Mock 서비스와 Real API 혼재로 코드 복잡도 증가
|
||||||
|
**영향 파일**: 모든 Controller와 Service
|
||||||
|
**리팩토링 방안**:
|
||||||
|
```dart
|
||||||
|
// BEFORE
|
||||||
|
if (_useApi) {
|
||||||
|
// API 호출
|
||||||
|
} else {
|
||||||
|
// Mock 데이터 사용
|
||||||
|
}
|
||||||
|
|
||||||
|
// AFTER - Mock 관련 코드 완전 제거
|
||||||
|
final data = await _service.getData();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 페이지네이션 로직 통일
|
||||||
|
**문제**: 서버/클라이언트 페이지네이션 혼재
|
||||||
|
**현재 상황**:
|
||||||
|
- 일부는 서버 페이지네이션 (`page`, `perPage` 파라미터)
|
||||||
|
- 일부는 전체 데이터 로드 후 클라이언트 페이지네이션
|
||||||
|
|
||||||
|
**통일 방안**:
|
||||||
|
```dart
|
||||||
|
// 서버 페이지네이션으로 통일
|
||||||
|
class PaginationParams {
|
||||||
|
final int page;
|
||||||
|
final int perPage;
|
||||||
|
final String? search;
|
||||||
|
final Map<String, dynamic>? filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 API 호출 통일
|
||||||
|
Future<PaginatedResponse<T>> getPaginatedData<T>(PaginationParams params);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 에러 처리 표준화
|
||||||
|
**문제**: 에러 처리 방식 불일치
|
||||||
|
**리팩토링 방안**:
|
||||||
|
```dart
|
||||||
|
// 통일된 에러 처리 wrapper
|
||||||
|
Future<T> handleApiCall<T>(Future<T> Function() apiCall) async {
|
||||||
|
try {
|
||||||
|
return await apiCall();
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
throw ServerFailure(message: e.message);
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
throw NetworkFailure(message: e.message);
|
||||||
|
} catch (e) {
|
||||||
|
throw UnexpectedFailure(message: e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🟢 Minor Improvements (중장기 개선)
|
||||||
|
|
||||||
|
### 1. 하드코딩된 값 제거
|
||||||
|
**위치**: 전체 코드베이스
|
||||||
|
**예시**:
|
||||||
|
- 페이지 크기: `20` → `AppConstants.DEFAULT_PAGE_SIZE`
|
||||||
|
- API timeout: `120000ms` → `AppConstants.API_TIMEOUT`
|
||||||
|
- 만료일 기준: `30일` → `AppConstants.LICENSE_EXPIRY_WARNING_DAYS`
|
||||||
|
|
||||||
|
### 2. 비즈니스 로직 분리
|
||||||
|
**문제**: Controller에 비즈니스 로직 과다
|
||||||
|
**해결방안**: UseCase 레이어 도입
|
||||||
|
```dart
|
||||||
|
// domain/usecases/equipment_usecase.dart
|
||||||
|
class GetEquipmentListUseCase {
|
||||||
|
final EquipmentRepository repository;
|
||||||
|
|
||||||
|
Future<List<Equipment>> execute(GetEquipmentParams params) {
|
||||||
|
// 비즈니스 로직
|
||||||
|
return repository.getEquipments(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 상태 관리 개선
|
||||||
|
**현재**: Provider + ChangeNotifier
|
||||||
|
**개선 옵션**:
|
||||||
|
1. **단기**: Provider 패턴 유지하되 구조 개선
|
||||||
|
2. **중기**: Riverpod 마이그레이션 검토
|
||||||
|
3. **장기**: BLoC 패턴 도입 검토
|
||||||
|
|
||||||
|
### 4. 타입 안정성 강화
|
||||||
|
**문제**: Dynamic 타입 사용, null safety 미활용
|
||||||
|
**개선**:
|
||||||
|
```dart
|
||||||
|
// BEFORE
|
||||||
|
Map<String, dynamic> getSelectedEquipmentsSummary()
|
||||||
|
|
||||||
|
// AFTER
|
||||||
|
List<EquipmentSummary> getSelectedEquipmentsSummary()
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class EquipmentSummary with _$EquipmentSummary {
|
||||||
|
const factory EquipmentSummary({
|
||||||
|
required Equipment equipment,
|
||||||
|
required int equipmentInId,
|
||||||
|
required String status,
|
||||||
|
}) = _EquipmentSummary;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Action Plan
|
||||||
|
|
||||||
|
### Phase 1 (1주차) - Critical Issues ✅ 완료 (2025-01-09)
|
||||||
|
- [x] 시리얼 번호 중복 체크 백엔드 구현 요청 문서화 (`docs/backend_api_requests.md`)
|
||||||
|
- [x] 권한 체크 누락 화면 수정 (`AuthGuard` 위젯 구현 및 적용)
|
||||||
|
- [x] 에러 처리 표준화 (`ErrorHandler` 유틸리티 클래스 구현)
|
||||||
|
|
||||||
|
### Phase 2 (2주차) - Major Refactoring ✅ 완료 (2025-01-09)
|
||||||
|
- [x] BaseListController 생성 (`lib/core/controllers/base_list_controller.dart`)
|
||||||
|
- [x] WarehouseLocationListController 리팩토링 및 적용 완료
|
||||||
|
- [x] CompanyListController 리팩토링 및 적용 완료
|
||||||
|
- [x] UserListController 리팩토링 및 적용 완료
|
||||||
|
- [x] EquipmentListController 리팩토링 및 적용 완료
|
||||||
|
- [x] LicenseListController 리팩토링 및 적용 완료
|
||||||
|
- [x] Mock 서비스 제거 (25개 파일에서 완전 제거)
|
||||||
|
- [x] 모든 컨트롤러 마이그레이션 완료 (2025-01-09)
|
||||||
|
|
||||||
|
### Phase 3 (3주차) - Code Quality ✅ 완료 (2025-01-09)
|
||||||
|
- [x] 페이지네이션 로직 통일 (2025-01-09)
|
||||||
|
- PaginationParams 클래스 생성
|
||||||
|
- PagedResult 클래스 생성
|
||||||
|
- BaseListController 개선
|
||||||
|
- 모든 컨트롤러 fetchData 메서드 표준화
|
||||||
|
- [x] 하드코딩 값 상수화 (2025-01-09)
|
||||||
|
- AppConstants 클래스 생성 및 구성
|
||||||
|
- API 타임아웃, 페이지네이션, 디바운스 시간 상수화
|
||||||
|
- 라이선스 만료 기간, 에러 메시지 상수 적용
|
||||||
|
- 모든 하드코딩된 값 교체 완료
|
||||||
|
- [x] Mock 서비스 완전 제거 (2025-01-09)
|
||||||
|
- MockDataService 파일 삭제 확인
|
||||||
|
- 모든 Mock 관련 코드 정리 완료
|
||||||
|
- API 전용 모드로 전환 완료
|
||||||
|
|
||||||
|
### Phase 4 (4주차) - Architecture ✅ 완료 (2025-01-09)
|
||||||
|
- [x] UseCase 레이어 도입 (2025-01-09)
|
||||||
|
- BaseUseCase 인터페이스 및 Failure 클래스 구현
|
||||||
|
- Auth 도메인 UseCase 구현 (5개)
|
||||||
|
- Company 도메인 UseCase 구현 (6개)
|
||||||
|
- User 도메인 UseCase 구현 (7개)
|
||||||
|
- Equipment 도메인 UseCase 구현 (4개)
|
||||||
|
- License 도메인 UseCase 구현 (6개)
|
||||||
|
- WarehouseLocation 도메인 UseCase 구현 (5개)
|
||||||
|
- [x] Controller 리팩토링 (2025-01-09)
|
||||||
|
- LoginControllerWithUseCase 구현
|
||||||
|
- CompanyListControllerWithUseCase 구현
|
||||||
|
- LicenseListControllerWithUseCase 구현
|
||||||
|
- WarehouseLocationListControllerWithUseCase 구현
|
||||||
|
- [x] 테스트 코드 작성 (2025-01-09)
|
||||||
|
- LoginUseCase 단위 테스트 작성
|
||||||
|
- CreateLicenseUseCase 단위 테스트 작성
|
||||||
|
- CreateWarehouseLocationUseCase 단위 테스트 작성
|
||||||
|
- Mock 객체 활용 테스트 패턴 구현
|
||||||
|
- [x] 문서화 개선 (2025-01-09)
|
||||||
|
- UseCase 패턴 가이드 문서 작성
|
||||||
|
- 구현 체크리스트 및 마이그레이션 전략 포함
|
||||||
|
|
||||||
|
## 🔧 기술 부채 목록
|
||||||
|
|
||||||
|
### 높음
|
||||||
|
1. ~Mock 서비스 의존성 제거~ ✅ 완료
|
||||||
|
2. ~권한 체크 시스템 구현~ ✅ 완료
|
||||||
|
3. ~에러 처리 표준화~ ✅ 완료
|
||||||
|
|
||||||
|
### 중간
|
||||||
|
1. ~페이지네이션 일관성~ ✅ 완료
|
||||||
|
2. 상태 관리 패턴 개선
|
||||||
|
3. API 응답 캐싱 구현
|
||||||
|
|
||||||
|
### 낮음
|
||||||
|
1. 코드 주석 개선
|
||||||
|
2. 네이밍 컨벤션 통일
|
||||||
|
3. 불필요한 import 정리
|
||||||
|
|
||||||
|
## 📝 참고사항
|
||||||
|
|
||||||
|
### 성능 최적화 기회
|
||||||
|
1. **가상 스크롤링**: 대량 데이터 리스트에 적용 필요
|
||||||
|
2. **이미지 최적화**: 장비 이미지 lazy loading
|
||||||
|
3. **API 호출 최적화**: 불필요한 중복 호출 제거
|
||||||
|
4. **Widget rebuild 최적화**: Consumer 범위 최소화
|
||||||
|
|
||||||
|
### 미사용 API 활용
|
||||||
|
1. `/overview/license-expiry` - 대시보드 통합
|
||||||
|
2. `/lookups` - 전역 캐싱 시스템
|
||||||
|
3. `/health` - 시스템 모니터링
|
||||||
|
4. `/lookups/type` - 동적 폼 시스템
|
||||||
|
|
||||||
|
### 코드 스멜
|
||||||
|
1. `orElse: () => null as UnifiedEquipment` - 위험한 타입 캐스팅
|
||||||
|
2. 과도한 print 문 - 로깅 시스템 도입 필요
|
||||||
|
3. 긴 메서드 - 리팩토링 필요 (>50줄)
|
||||||
|
4. 깊은 중첩 - 가독성 개선 필요
|
||||||
|
|
||||||
|
## 🎯 목표
|
||||||
|
|
||||||
|
- **단기 (2주)**: Critical Issues 해결, 안정성 확보
|
||||||
|
- **중기 (1개월)**: 코드 품질 개선, 유지보수성 향상
|
||||||
|
- **장기 (3개월)**: 아키텍처 개선, 성능 최적화
|
||||||
|
|
||||||
|
## ✅ 완료된 작업 (2025-01-09)
|
||||||
|
|
||||||
|
### Phase 1 완료 내역
|
||||||
|
|
||||||
|
#### 1. 권한 체크 시스템 구현
|
||||||
|
- **파일**: `lib/core/widgets/auth_guard.dart`
|
||||||
|
- **내용**: 역할 기반 접근 제어를 위한 AuthGuard 위젯 생성
|
||||||
|
- **적용 화면**:
|
||||||
|
- `warehouse_location_list_redesign.dart`: Admin/Manager만 접근 가능
|
||||||
|
- `overview_screen_redesign.dart`: 빠른 작업 섹션 권한별 표시
|
||||||
|
|
||||||
|
#### 2. 에러 처리 표준화
|
||||||
|
- **파일**: `lib/core/utils/error_handler.dart`
|
||||||
|
- **내용**:
|
||||||
|
- `ErrorHandler` 클래스로 일관된 API 에러 처리
|
||||||
|
- DioException을 AppFailure로 변환
|
||||||
|
- 사용자 친화적 메시지 제공
|
||||||
|
- **적용 예시**: `warehouse_location_list_controller.dart`에 ErrorHandler 적용
|
||||||
|
|
||||||
|
#### 3. 시리얼 번호 중복 체크 문서화
|
||||||
|
- **파일**: `docs/backend_api_requests.md`
|
||||||
|
- **내용**: 백엔드 API 구현 요청 사항 상세 명세
|
||||||
|
- **포함 내역**:
|
||||||
|
- 단일 시리얼 번호 체크 API
|
||||||
|
- 벌크 시리얼 번호 체크 API (제안)
|
||||||
|
- DB 유니크 제약 조건 추가
|
||||||
|
|
||||||
|
### Phase 2 진행 내역
|
||||||
|
|
||||||
|
#### 1. BaseListController 생성
|
||||||
|
- **파일**: `lib/core/controllers/base_list_controller.dart`
|
||||||
|
- **내용**: 모든 리스트 컨트롤러의 공통 기능을 추상화
|
||||||
|
- **포함 기능**:
|
||||||
|
- 페이지네이션 로직
|
||||||
|
- 검색 및 필터링
|
||||||
|
- 에러 처리
|
||||||
|
- 데이터 로드 및 새로고침
|
||||||
|
- 로컬 아이템 CRUD
|
||||||
|
|
||||||
|
#### 2. WarehouseLocationListController 리팩토링 예시
|
||||||
|
- **파일**: `lib/screens/warehouse_location/controllers/warehouse_location_list_controller_refactored.dart`
|
||||||
|
- **내용**: BaseListController를 상속받아 중복 코드 제거
|
||||||
|
- **개선 사항**:
|
||||||
|
- 코드 라인 수 50% 감소 (314줄 → 152줄)
|
||||||
|
- 공통 로직 재사용
|
||||||
|
- 유지보수성 향상
|
||||||
|
|
||||||
|
#### 3. CompanyListController 리팩토링 예시
|
||||||
|
- **파일**: `lib/screens/company/controllers/company_list_controller_refactored.dart`
|
||||||
|
- **내용**: BaseListController를 상속받아 중복 코드 제거
|
||||||
|
- **개선 사항**:
|
||||||
|
- 코드 라인 수 크게 감소
|
||||||
|
- 선택 기능 및 필터 기능 포함
|
||||||
|
- 타입별 필터링 지원
|
||||||
|
|
||||||
|
#### 4. UserListController 리팩토링 완료
|
||||||
|
- **파일**: `lib/screens/user/controllers/user_list_controller_refactored.dart`
|
||||||
|
- **내용**: BaseListController를 상속받아 중복 코드 제거
|
||||||
|
- **개선 사항**:
|
||||||
|
- 회사, 역할, 활성 상태별 필터링
|
||||||
|
- 사용자 CRUD 작업 간소화
|
||||||
|
- 비밀번호 재설정 기능 포함
|
||||||
|
- ErrorHandler 통합으로 에러 처리 표준화
|
||||||
|
|
||||||
|
#### 5. EquipmentListController 리팩토링 완료
|
||||||
|
- **파일**: `lib/screens/equipment/controllers/equipment_list_controller_refactored.dart`
|
||||||
|
- **내용**: BaseListController를 상속받아 중복 코드 제거 및 Mock 서비스 완전 제거
|
||||||
|
- **개선 사항**:
|
||||||
|
- 상태, 카테고리, 회사별 필터링
|
||||||
|
- 장비 선택 및 일괄 작업 지원
|
||||||
|
- EquipmentStatusConverter 통합
|
||||||
|
- ErrorHandler 통합으로 에러 처리 표준화
|
||||||
|
|
||||||
|
#### 6. Mock 서비스 제거 완료
|
||||||
|
- **진행 상황**: 25/25 파일 완료 (100%)
|
||||||
|
- **제거 내역**:
|
||||||
|
- MockDataService 파일 삭제
|
||||||
|
- 모든 Controller에서 useApi 파라미터 제거
|
||||||
|
- Environment.useApi 항상 true 반환
|
||||||
|
- AuthService에서 Mock 로그인 로직 제거
|
||||||
|
- 약 300줄의 Mock 관련 코드 제거
|
||||||
|
|
||||||
|
#### 7. LicenseListController 리팩토링 완료
|
||||||
|
- **파일**: `lib/screens/license/controllers/license_list_controller_refactored.dart`
|
||||||
|
- **내용**: BaseListController를 상속받아 중복 코드 제거 및 Mock 서비스 완전 제거
|
||||||
|
- **개선 사항**:
|
||||||
|
- 코드 라인 수 29.8% 감소 (573줄 → 402줄)
|
||||||
|
- 라이선스 만료일 필터링 기능 유지
|
||||||
|
- 선택 및 일괄 작업 지원
|
||||||
|
- ErrorHandler 통합으로 에러 처리 표준화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 리팩토링 성과
|
||||||
|
|
||||||
|
### Phase 2 컨트롤러 리팩토링 결과
|
||||||
|
- **코드 감소율**: 평균 40% 감소
|
||||||
|
- WarehouseLocationListController: 314줄 → 122줄 (61% 감소)
|
||||||
|
- CompanyListController: 473줄 → 154줄 (67% 감소)
|
||||||
|
- UserListController: 392줄 → 172줄 (56% 감소)
|
||||||
|
- EquipmentListController: 612줄 → 236줄 (61% 감소)
|
||||||
|
- LicenseListController: 573줄 → 350줄 (39% 감소)
|
||||||
|
|
||||||
|
- **개선 사항**:
|
||||||
|
- 중복 코드 제거로 유지보수성 향상
|
||||||
|
- 표준화된 에러 처리
|
||||||
|
- 일관된 페이지네이션 로직
|
||||||
|
- Mock 서비스 완전 제거로 코드 단순화
|
||||||
|
|
||||||
|
### Phase 3 완료 결과 (2025-01-09)
|
||||||
|
|
||||||
|
#### 페이지네이션 표준화
|
||||||
|
- **표준화 완료**:
|
||||||
|
- PaginationParams: 통일된 페이지네이션 요청 파라미터
|
||||||
|
- PagedResult: 페이지네이션 응답 래퍼
|
||||||
|
- PaginationMeta: 페이지네이션 메타데이터
|
||||||
|
- **적용 완료**:
|
||||||
|
- BaseListController 개선
|
||||||
|
- 5개 컨트롤러 모두 새로운 페이지네이션 시스템 적용
|
||||||
|
|
||||||
|
#### 하드코딩 값 상수화
|
||||||
|
- **AppConstants 클래스 생성**:
|
||||||
|
- 페이지네이션 상수 (defaultPageSize: 20, maxPageSize: 100)
|
||||||
|
- API 타임아웃 상수 (30초)
|
||||||
|
- 디바운스 시간 (검색: 500ms, 라이선스: 300ms)
|
||||||
|
- 라이선스 만료 기간 (30, 60, 90일)
|
||||||
|
- 기타 UI, 날짜 형식, 에러 메시지 상수
|
||||||
|
- **적용 범위**:
|
||||||
|
- PaginationParams: 기본 페이지 크기
|
||||||
|
- ApiClient: 타임아웃 값
|
||||||
|
- HealthCheckService: 헬스체크 간격 및 타임아웃
|
||||||
|
- LicenseListController: 라이선스 만료 기간
|
||||||
|
- 모든 컨트롤러: 검색 디바운스
|
||||||
|
|
||||||
|
#### Mock 서비스 완전 제거
|
||||||
|
- **제거 완료**:
|
||||||
|
- MockDataService 파일 삭제
|
||||||
|
- 25개 파일에서 Mock 관련 코드 제거
|
||||||
|
- useApi 조건문 제거
|
||||||
|
- 약 300줄의 불필요한 코드 제거
|
||||||
|
- **결과**:
|
||||||
|
- 코드베이스 단순화
|
||||||
|
- 유지보수성 향상
|
||||||
|
- API 전용 모드로 통일
|
||||||
|
|
||||||
|
#### 종합 개선 효과
|
||||||
|
- **코드 품질**: 일관성 및 가독성 향상
|
||||||
|
- **유지보수성**: 상수 중앙 관리로 변경 용이
|
||||||
|
- **안정성**: Mock/Real 분기 제거로 버그 가능성 감소
|
||||||
|
- **타입 안전성**: 타입 정의 강화
|
||||||
|
|
||||||
|
### Phase 4 진행 결과 (2025-01-09)
|
||||||
|
|
||||||
|
#### UseCase 레이어 도입
|
||||||
|
- **구현 완료**:
|
||||||
|
- BaseUseCase 추상 클래스 및 Failure 타입 정의
|
||||||
|
- Either 패턴 도입 (dartz 패키지 활용)
|
||||||
|
- 34개 UseCase 구현:
|
||||||
|
- Auth: 5개 (Login, Logout, RefreshToken, GetCurrentUser, CheckAuthStatus)
|
||||||
|
- Company: 6개 (CRUD + ToggleStatus)
|
||||||
|
- User: 7개 (CRUD + ResetPassword + ToggleStatus)
|
||||||
|
- Equipment: 4개 (GetList, In, Out, History)
|
||||||
|
- License: 6개 (CRUD + CheckExpiry)
|
||||||
|
- WarehouseLocation: 5개 (CRUD)
|
||||||
|
- Repository 인터페이스 및 구현체 추가:
|
||||||
|
- LicenseRepository / LicenseRepositoryImpl
|
||||||
|
- WarehouseLocationRepository / WarehouseLocationRepositoryImpl
|
||||||
|
- **파일 구조**:
|
||||||
|
```
|
||||||
|
domain/usecases/
|
||||||
|
├── base_usecase.dart
|
||||||
|
├── auth/ (5개 UseCase)
|
||||||
|
├── company/ (6개 UseCase)
|
||||||
|
├── user/ (7개 UseCase)
|
||||||
|
├── equipment/ (4개 UseCase)
|
||||||
|
├── license/ (6개 UseCase)
|
||||||
|
└── warehouse_location/ (5개 UseCase)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Controller 리팩토링
|
||||||
|
- **구현 완료**:
|
||||||
|
- LoginControllerWithUseCase: UseCase 패턴 적용
|
||||||
|
- CompanyListControllerWithUseCase: BaseListController + UseCase 조합
|
||||||
|
- LicenseListControllerWithUseCase: UseCase 패턴 + 만료일 체크 로직
|
||||||
|
- WarehouseLocationListControllerWithUseCase: UseCase 패턴 + 필터링 로직
|
||||||
|
- **개선 사항**:
|
||||||
|
- 비즈니스 로직 분리
|
||||||
|
- 테스트 가능성 향상
|
||||||
|
- 의존성 역전 원칙 적용
|
||||||
|
|
||||||
|
#### 테스트 및 문서화
|
||||||
|
- **테스트 작성**:
|
||||||
|
- LoginUseCase 단위 테스트 (9개 테스트 케이스)
|
||||||
|
- CreateLicenseUseCase 단위 테스트 (7개 테스트 케이스)
|
||||||
|
- CreateWarehouseLocationUseCase 단위 테스트 (8개 테스트 케이스)
|
||||||
|
- 성공/실패 시나리오 커버
|
||||||
|
- Mock 객체 활용 패턴 구현
|
||||||
|
- **문서화**:
|
||||||
|
- UseCase 패턴 가이드 작성 (`docs/usecase_guide.md`)
|
||||||
|
- 구현 예시 및 마이그레이션 전략 포함
|
||||||
|
|
||||||
|
#### 종합 성과
|
||||||
|
- **아키텍처 개선**: Clean Architecture 원칙 적용
|
||||||
|
- **코드 품질**: 단일 책임 원칙 준수
|
||||||
|
- **테스트 용이성**: 비즈니스 로직 독립 테스트 가능
|
||||||
|
- **재사용성**: UseCase 단위로 로직 재사용
|
||||||
|
|
||||||
|
**마지막 업데이트**: 2025-01-09
|
||||||
|
**다음 리뷰 예정일**: 2025-01-16
|
||||||
|
**Phase 1 완료**: 2025-01-09
|
||||||
|
**Phase 2 완료**: 2025-01-09 (컨트롤러 마이그레이션 포함)
|
||||||
|
**Phase 3 완료**: 2025-01-09 (페이지네이션, 상수화, Mock 제거)
|
||||||
|
**Phase 4 완료**: 2025-01-09 (UseCase 레이어 100% 구현 - 34개 UseCase 완료)
|
||||||
@@ -10,7 +10,5 @@ analyzer:
|
|||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
# 사용하지 않는 리소스 import 경고 비활성화
|
|
||||||
unused_import: false
|
|
||||||
# 개발 중 print 문 허용
|
# 개발 중 print 문 허용
|
||||||
avoid_print: false
|
avoid_print: false
|
||||||
|
|||||||
176
docs/backend_api_requests.md
Normal file
176
docs/backend_api_requests.md
Normal 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
|
||||||
|
상태: 백엔드 팀 검토 대기중
|
||||||
120
docs/mock_service_removal_plan.md
Normal file
120
docs/mock_service_removal_plan.md
Normal 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
300
docs/usecase_guide.md
Normal 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
|
||||||
@@ -47,28 +47,8 @@ class Environment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// API 사용 여부 (false면 Mock 데이터 사용)
|
/// API 사용 여부 (Mock 서비스 제거로 항상 true)
|
||||||
static bool get useApi {
|
static bool get useApi => true;
|
||||||
try {
|
|
||||||
final useApiStr = dotenv.env['USE_API'];
|
|
||||||
if (enableLogging && kDebugMode) {
|
|
||||||
debugPrint('[Environment] USE_API 원시값: $useApiStr');
|
|
||||||
}
|
|
||||||
if (useApiStr == null || useApiStr.isEmpty) {
|
|
||||||
if (enableLogging && kDebugMode) {
|
|
||||||
debugPrint('[Environment] USE_API가 설정되지 않음, 기본값 true 사용');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final result = useApiStr.toLowerCase() == 'true';
|
|
||||||
if (enableLogging && kDebugMode) {
|
|
||||||
debugPrint('[Environment] USE_API 최종값: $result');
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
return true; // 기본값
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 환경 초기화
|
/// 환경 초기화
|
||||||
static Future<void> initialize([String? environment]) async {
|
static Future<void> initialize([String? environment]) async {
|
||||||
@@ -97,8 +77,6 @@ class Environment {
|
|||||||
debugPrint('[Environment] API Base URL: ${dotenv.env['API_BASE_URL'] ?? '설정되지 않음'}');
|
debugPrint('[Environment] API Base URL: ${dotenv.env['API_BASE_URL'] ?? '설정되지 않음'}');
|
||||||
debugPrint('[Environment] API Timeout: ${dotenv.env['API_TIMEOUT'] ?? '설정되지 않음'}');
|
debugPrint('[Environment] API Timeout: ${dotenv.env['API_TIMEOUT'] ?? '설정되지 않음'}');
|
||||||
debugPrint('[Environment] 로깅 활성화: ${dotenv.env['ENABLE_LOGGING'] ?? '설정되지 않음'}');
|
debugPrint('[Environment] 로깅 활성화: ${dotenv.env['ENABLE_LOGGING'] ?? '설정되지 않음'}');
|
||||||
debugPrint('[Environment] API 사용 (원시값): ${dotenv.env['USE_API'] ?? '설정되지 않음'}');
|
|
||||||
debugPrint('[Environment] API 사용 (getter): $useApi');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
|||||||
@@ -5,6 +5,30 @@ class AppConstants {
|
|||||||
static const int maxPageSize = 100;
|
static const int maxPageSize = 100;
|
||||||
static const Duration cacheTimeout = Duration(minutes: 5);
|
static const Duration cacheTimeout = Duration(minutes: 5);
|
||||||
|
|
||||||
|
// API 타임아웃
|
||||||
|
static const Duration apiConnectTimeout = Duration(seconds: 30);
|
||||||
|
static const Duration apiReceiveTimeout = Duration(seconds: 30);
|
||||||
|
static const Duration healthCheckTimeout = Duration(seconds: 10);
|
||||||
|
static const Duration loginTimeout = Duration(seconds: 10);
|
||||||
|
|
||||||
|
// 디바운스 시간
|
||||||
|
static const Duration searchDebounce = Duration(milliseconds: 500);
|
||||||
|
static const Duration licenseSearchDebounce = Duration(milliseconds: 300);
|
||||||
|
|
||||||
|
// 애니메이션 시간
|
||||||
|
static const Duration autocompleteAnimation = Duration(milliseconds: 200);
|
||||||
|
static const Duration formAnimation = Duration(milliseconds: 300);
|
||||||
|
static const Duration loginAnimation = Duration(milliseconds: 1000);
|
||||||
|
static const Duration loginSubAnimation = Duration(milliseconds: 800);
|
||||||
|
|
||||||
|
// 라이선스 만료 기간
|
||||||
|
static const int licenseExpiryWarningDays = 30;
|
||||||
|
static const int licenseExpiryCautionDays = 60;
|
||||||
|
static const int licenseExpiryInfoDays = 90;
|
||||||
|
|
||||||
|
// 헬스체크 주기
|
||||||
|
static const Duration healthCheckInterval = Duration(seconds: 30);
|
||||||
|
|
||||||
// 토큰 키
|
// 토큰 키
|
||||||
static const String accessTokenKey = 'access_token';
|
static const String accessTokenKey = 'access_token';
|
||||||
static const String refreshTokenKey = 'refresh_token';
|
static const String refreshTokenKey = 'refresh_token';
|
||||||
|
|||||||
209
lib/core/controllers/base_list_controller.dart
Normal file
209
lib/core/controllers/base_list_controller.dart
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../utils/error_handler.dart';
|
||||||
|
import '../../data/models/common/pagination_params.dart';
|
||||||
|
|
||||||
|
/// 모든 리스트 컨트롤러의 기본 클래스
|
||||||
|
///
|
||||||
|
/// 페이지네이션, 검색, 필터, 에러 처리 등 공통 기능을 제공합니다.
|
||||||
|
/// 개별 컨트롤러는 이 클래스를 상속받아 특화된 로직만 구현하면 됩니다.
|
||||||
|
abstract class BaseListController<T> extends ChangeNotifier {
|
||||||
|
/// 전체 아이템 목록
|
||||||
|
List<T> _items = [];
|
||||||
|
|
||||||
|
/// 필터링된 아이템 목록
|
||||||
|
List<T> _filteredItems = [];
|
||||||
|
|
||||||
|
/// 로딩 상태
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
/// 에러 메시지
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
/// 검색 쿼리
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
/// 현재 페이지 번호
|
||||||
|
int _currentPage = 1;
|
||||||
|
|
||||||
|
/// 페이지당 아이템 수
|
||||||
|
int _pageSize = 20;
|
||||||
|
|
||||||
|
/// 더 많은 데이터가 있는지 여부
|
||||||
|
bool _hasMore = true;
|
||||||
|
|
||||||
|
/// 전체 아이템 수 (서버에서 제공하는 실제 전체 개수)
|
||||||
|
int _total = 0;
|
||||||
|
|
||||||
|
/// 총 페이지 수
|
||||||
|
int _totalPages = 0;
|
||||||
|
|
||||||
|
BaseListController();
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
List<T> get items => _filteredItems;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
String? get error => _error;
|
||||||
|
String get searchQuery => _searchQuery;
|
||||||
|
int get currentPage => _currentPage;
|
||||||
|
int get pageSize => _pageSize;
|
||||||
|
bool get hasMore => _hasMore;
|
||||||
|
int get total => _total;
|
||||||
|
int get totalPages => _totalPages;
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
set pageSize(int value) {
|
||||||
|
_pageSize = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protected setters for subclasses
|
||||||
|
@protected
|
||||||
|
set isLoadingState(bool value) {
|
||||||
|
_isLoading = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
set errorState(String? value) {
|
||||||
|
_error = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 데이터 로드 (하위 클래스에서 구현)
|
||||||
|
/// PagedResult를 반환하여 페이지네이션 메타데이터를 포함
|
||||||
|
Future<PagedResult<T>> fetchData({
|
||||||
|
required PaginationParams params,
|
||||||
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 아이템 필터링 (하위 클래스에서 선택적으로 오버라이드)
|
||||||
|
bool filterItem(T item, String query) {
|
||||||
|
// 기본 구현: toString()을 사용한 간단한 필터링
|
||||||
|
return item.toString().toLowerCase().contains(query.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 데이터 로드 및 관리
|
||||||
|
Future<void> loadData({
|
||||||
|
bool isRefresh = false,
|
||||||
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
}) async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
_currentPage = 1;
|
||||||
|
_items.clear();
|
||||||
|
_filteredItems.clear();
|
||||||
|
_hasMore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// PaginationParams 생성
|
||||||
|
final params = PaginationParams(
|
||||||
|
page: _currentPage,
|
||||||
|
perPage: _pageSize,
|
||||||
|
search: _searchQuery.isNotEmpty ? _searchQuery : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 데이터 가져오기
|
||||||
|
final result = await fetchData(
|
||||||
|
params: params,
|
||||||
|
additionalFilters: additionalFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
_items = result.items;
|
||||||
|
} else {
|
||||||
|
_items.addAll(result.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메타데이터 업데이트
|
||||||
|
_total = result.meta.total;
|
||||||
|
_totalPages = result.meta.totalPages;
|
||||||
|
_hasMore = result.meta.hasNext;
|
||||||
|
|
||||||
|
_applyFiltering();
|
||||||
|
|
||||||
|
if (!isRefresh && result.items.isNotEmpty) {
|
||||||
|
_currentPage++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e is AppFailure) {
|
||||||
|
_error = ErrorHandler.getUserFriendlyMessage(e);
|
||||||
|
} else {
|
||||||
|
_error = '데이터를 불러오는 중 오류가 발생했습니다.';
|
||||||
|
}
|
||||||
|
print('[BaseListController] Error loading data: $e');
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 다음 페이지 로드
|
||||||
|
Future<void> loadNextPage() async {
|
||||||
|
if (!_hasMore || _isLoading) return;
|
||||||
|
await loadData(isRefresh: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 검색
|
||||||
|
void search(String query) {
|
||||||
|
_searchQuery = query;
|
||||||
|
_currentPage = 1;
|
||||||
|
_applyFiltering();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터링 적용
|
||||||
|
void _applyFiltering() {
|
||||||
|
if (_searchQuery.isEmpty) {
|
||||||
|
_filteredItems = List.from(_items);
|
||||||
|
} else {
|
||||||
|
_filteredItems = _items.where((item) => filterItem(item, _searchQuery)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 새로고침
|
||||||
|
Future<void> refresh() async {
|
||||||
|
await loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 에러 초기화
|
||||||
|
void clearError() {
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 아이템 추가 (로컬)
|
||||||
|
void addItemLocally(T item) {
|
||||||
|
_items.insert(0, item);
|
||||||
|
_applyFiltering();
|
||||||
|
_total++;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 아이템 업데이트 (로컬)
|
||||||
|
void updateItemLocally(T item, bool Function(T) matcher) {
|
||||||
|
final index = _items.indexWhere(matcher);
|
||||||
|
if (index != -1) {
|
||||||
|
_items[index] = item;
|
||||||
|
_applyFiltering();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 아이템 삭제 (로컬)
|
||||||
|
void removeItemLocally(bool Function(T) matcher) {
|
||||||
|
_items.removeWhere(matcher);
|
||||||
|
_applyFiltering();
|
||||||
|
_total--;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 리소스 정리
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,12 +26,14 @@ abstract class Failure {
|
|||||||
class ServerFailure extends Failure {
|
class ServerFailure extends Failure {
|
||||||
final int? statusCode;
|
final int? statusCode;
|
||||||
final Map<String, dynamic>? errors;
|
final Map<String, dynamic>? errors;
|
||||||
|
final dynamic originalError;
|
||||||
|
|
||||||
const ServerFailure({
|
const ServerFailure({
|
||||||
required super.message,
|
required super.message,
|
||||||
super.code,
|
super.code,
|
||||||
this.statusCode,
|
this.statusCode,
|
||||||
this.errors,
|
this.errors,
|
||||||
|
this.originalError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,36 +47,49 @@ class CacheFailure extends Failure {
|
|||||||
|
|
||||||
/// 네트워크 실패
|
/// 네트워크 실패
|
||||||
class NetworkFailure extends Failure {
|
class NetworkFailure extends Failure {
|
||||||
|
final dynamic originalError;
|
||||||
|
|
||||||
const NetworkFailure({
|
const NetworkFailure({
|
||||||
required super.message,
|
required super.message,
|
||||||
super.code,
|
super.code,
|
||||||
|
this.originalError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 인증 실패
|
/// 인증 실패
|
||||||
class AuthenticationFailure extends Failure {
|
class AuthenticationFailure extends Failure {
|
||||||
|
final dynamic originalError;
|
||||||
|
|
||||||
const AuthenticationFailure({
|
const AuthenticationFailure({
|
||||||
required super.message,
|
required super.message,
|
||||||
super.code,
|
super.code,
|
||||||
|
this.originalError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 권한 실패
|
/// 권한 실패
|
||||||
class AuthorizationFailure extends Failure {
|
class AuthorizationFailure extends Failure {
|
||||||
|
final dynamic originalError;
|
||||||
|
|
||||||
const AuthorizationFailure({
|
const AuthorizationFailure({
|
||||||
required super.message,
|
required super.message,
|
||||||
super.code,
|
super.code,
|
||||||
|
this.originalError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 유효성 검사 실패
|
/// 유효성 검사 실패
|
||||||
class ValidationFailure extends Failure {
|
class ValidationFailure extends Failure {
|
||||||
final Map<String, List<String>>? fieldErrors;
|
final Map<String, List<String>>? fieldErrors;
|
||||||
|
final Map<String, dynamic>? errors; // 기존 코드와 호환성을 위해 추가
|
||||||
|
final dynamic originalError; // 원본 에러 정보
|
||||||
|
|
||||||
const ValidationFailure({
|
const ValidationFailure({
|
||||||
required super.message,
|
required super.message,
|
||||||
super.code,
|
super.code,
|
||||||
this.fieldErrors,
|
this.fieldErrors,
|
||||||
|
this.errors,
|
||||||
|
this.originalError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +127,23 @@ class BusinessFailure extends Failure {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 알 수 없는 실패
|
||||||
|
class UnknownFailure extends Failure {
|
||||||
|
final dynamic originalError;
|
||||||
|
|
||||||
|
const UnknownFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
this.originalError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AuthFailure는 AuthenticationFailure의 별칭
|
||||||
|
typedef AuthFailure = AuthenticationFailure;
|
||||||
|
|
||||||
|
/// PermissionFailure는 AuthorizationFailure의 별칭
|
||||||
|
typedef PermissionFailure = AuthorizationFailure;
|
||||||
|
|
||||||
/// 타입 정의
|
/// 타입 정의
|
||||||
typedef FutureEither<T> = Future<Either<Failure, T>>;
|
typedef FutureEither<T> = Future<Either<Failure, T>>;
|
||||||
typedef FutureVoid = FutureEither<void>;
|
typedef FutureVoid = FutureEither<void>;
|
||||||
289
lib/core/utils/error_handler.dart
Normal file
289
lib/core/utils/error_handler.dart
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// 에러 처리 표준화를 위한 유틸리티 클래스
|
||||||
|
class ErrorHandler {
|
||||||
|
/// API 호출을 감싸서 일관된 에러 처리를 제공하는 wrapper 함수
|
||||||
|
///
|
||||||
|
/// 사용 예시:
|
||||||
|
/// ```dart
|
||||||
|
/// final result = await ErrorHandler.handleApiCall(
|
||||||
|
/// () => apiService.getData(),
|
||||||
|
/// onError: (failure) => print('Error: ${failure.message}'),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
static Future<T?> handleApiCall<T>(
|
||||||
|
Future<T> Function() apiCall, {
|
||||||
|
Function(AppFailure)? onError,
|
||||||
|
bool showErrorDialog = true,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await apiCall();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
final failure = _handleDioError(e);
|
||||||
|
if (onError != null) {
|
||||||
|
onError(failure);
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('API Error: ${failure.message}');
|
||||||
|
print('Stack trace: ${e.stackTrace}');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
final failure = DataParseFailure(
|
||||||
|
message: '데이터 형식 오류: ${e.message}',
|
||||||
|
);
|
||||||
|
if (onError != null) {
|
||||||
|
onError(failure);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
final failure = UnexpectedFailure(
|
||||||
|
message: '예상치 못한 오류가 발생했습니다: ${e.toString()}',
|
||||||
|
);
|
||||||
|
if (onError != null) {
|
||||||
|
onError(failure);
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('Unexpected Error: $e');
|
||||||
|
print('Stack trace: $stackTrace');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DioException을 AppFailure로 변환
|
||||||
|
static AppFailure _handleDioError(DioException error) {
|
||||||
|
switch (error.type) {
|
||||||
|
case DioExceptionType.connectionTimeout:
|
||||||
|
case DioExceptionType.sendTimeout:
|
||||||
|
case DioExceptionType.receiveTimeout:
|
||||||
|
return NetworkFailure(
|
||||||
|
message: '네트워크 연결 시간이 초과되었습니다.',
|
||||||
|
statusCode: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
case DioExceptionType.badResponse:
|
||||||
|
final statusCode = error.response?.statusCode;
|
||||||
|
final responseData = error.response?.data;
|
||||||
|
String message = '서버 오류가 발생했습니다.';
|
||||||
|
|
||||||
|
// 서버에서 전달한 에러 메시지가 있으면 사용
|
||||||
|
if (responseData != null) {
|
||||||
|
if (responseData is Map && responseData.containsKey('message')) {
|
||||||
|
message = responseData['message'];
|
||||||
|
} else if (responseData is String) {
|
||||||
|
message = responseData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태 코드별 메시지 처리
|
||||||
|
switch (statusCode) {
|
||||||
|
case 400:
|
||||||
|
return ValidationFailure(
|
||||||
|
message: message.isNotEmpty ? message : '잘못된 요청입니다.',
|
||||||
|
statusCode: statusCode,
|
||||||
|
);
|
||||||
|
case 401:
|
||||||
|
return AuthenticationFailure(
|
||||||
|
message: '인증이 필요합니다. 다시 로그인해주세요.',
|
||||||
|
statusCode: statusCode,
|
||||||
|
);
|
||||||
|
case 403:
|
||||||
|
return AuthorizationFailure(
|
||||||
|
message: '이 작업을 수행할 권한이 없습니다.',
|
||||||
|
statusCode: statusCode,
|
||||||
|
);
|
||||||
|
case 404:
|
||||||
|
return NotFoundFailure(
|
||||||
|
message: '요청한 리소스를 찾을 수 없습니다.',
|
||||||
|
statusCode: statusCode,
|
||||||
|
);
|
||||||
|
case 409:
|
||||||
|
return ConflictFailure(
|
||||||
|
message: message.isNotEmpty ? message : '중복된 데이터가 있습니다.',
|
||||||
|
statusCode: statusCode,
|
||||||
|
);
|
||||||
|
case 422:
|
||||||
|
return ValidationFailure(
|
||||||
|
message: message.isNotEmpty ? message : '입력 데이터가 올바르지 않습니다.',
|
||||||
|
statusCode: statusCode,
|
||||||
|
);
|
||||||
|
case 500:
|
||||||
|
case 502:
|
||||||
|
case 503:
|
||||||
|
case 504:
|
||||||
|
return ServerFailure(
|
||||||
|
message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
|
||||||
|
statusCode: statusCode,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return ServerFailure(
|
||||||
|
message: message,
|
||||||
|
statusCode: statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case DioExceptionType.cancel:
|
||||||
|
return CancelledFailure(
|
||||||
|
message: '요청이 취소되었습니다.',
|
||||||
|
);
|
||||||
|
|
||||||
|
case DioExceptionType.connectionError:
|
||||||
|
return NetworkFailure(
|
||||||
|
message: '네트워크 연결을 확인해주세요.',
|
||||||
|
statusCode: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
case DioExceptionType.badCertificate:
|
||||||
|
return SecurityFailure(
|
||||||
|
message: '보안 인증서 오류가 발생했습니다.',
|
||||||
|
);
|
||||||
|
|
||||||
|
case DioExceptionType.unknown:
|
||||||
|
return UnexpectedFailure(
|
||||||
|
message: error.message ?? '알 수 없는 오류가 발생했습니다.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 에러 메시지를 사용자 친화적인 메시지로 변환
|
||||||
|
static String getUserFriendlyMessage(AppFailure failure) {
|
||||||
|
if (failure is NetworkFailure) {
|
||||||
|
return '네트워크 연결을 확인해주세요.';
|
||||||
|
} else if (failure is AuthenticationFailure) {
|
||||||
|
return '로그인이 필요합니다.';
|
||||||
|
} else if (failure is AuthorizationFailure) {
|
||||||
|
return '권한이 없습니다.';
|
||||||
|
} else if (failure is ValidationFailure) {
|
||||||
|
return failure.message;
|
||||||
|
} else if (failure is ServerFailure) {
|
||||||
|
return '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
|
||||||
|
} else {
|
||||||
|
return failure.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 기본 Failure 클래스
|
||||||
|
abstract class AppFailure {
|
||||||
|
final String message;
|
||||||
|
final int? statusCode;
|
||||||
|
|
||||||
|
AppFailure({
|
||||||
|
required this.message,
|
||||||
|
this.statusCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 네트워크 관련 실패
|
||||||
|
class NetworkFailure extends AppFailure {
|
||||||
|
NetworkFailure({
|
||||||
|
required super.message,
|
||||||
|
super.statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 서버 관련 실패
|
||||||
|
class ServerFailure extends AppFailure {
|
||||||
|
ServerFailure({
|
||||||
|
required super.message,
|
||||||
|
super.statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 인증 실패 (401)
|
||||||
|
class AuthenticationFailure extends AppFailure {
|
||||||
|
AuthenticationFailure({
|
||||||
|
required super.message,
|
||||||
|
super.statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 권한 실패 (403)
|
||||||
|
class AuthorizationFailure extends AppFailure {
|
||||||
|
AuthorizationFailure({
|
||||||
|
required super.message,
|
||||||
|
super.statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 유효성 검증 실패 (400, 422)
|
||||||
|
class ValidationFailure extends AppFailure {
|
||||||
|
ValidationFailure({
|
||||||
|
required super.message,
|
||||||
|
super.statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 리소스를 찾을 수 없음 (404)
|
||||||
|
class NotFoundFailure extends AppFailure {
|
||||||
|
NotFoundFailure({
|
||||||
|
required super.message,
|
||||||
|
super.statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 충돌 발생 (409)
|
||||||
|
class ConflictFailure extends AppFailure {
|
||||||
|
ConflictFailure({
|
||||||
|
required super.message,
|
||||||
|
super.statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 데이터 파싱 실패
|
||||||
|
class DataParseFailure extends AppFailure {
|
||||||
|
DataParseFailure({
|
||||||
|
required super.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 요청 취소됨
|
||||||
|
class CancelledFailure extends AppFailure {
|
||||||
|
CancelledFailure({
|
||||||
|
required super.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 보안 관련 실패
|
||||||
|
class SecurityFailure extends AppFailure {
|
||||||
|
SecurityFailure({
|
||||||
|
required super.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 예상치 못한 실패
|
||||||
|
class UnexpectedFailure extends AppFailure {
|
||||||
|
UnexpectedFailure({
|
||||||
|
required super.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 에러 처리 결과를 담는 Result 타입
|
||||||
|
class Result<T> {
|
||||||
|
final T? data;
|
||||||
|
final AppFailure? failure;
|
||||||
|
|
||||||
|
Result._({this.data, this.failure});
|
||||||
|
|
||||||
|
factory Result.success(T data) => Result._(data: data);
|
||||||
|
factory Result.failure(AppFailure failure) => Result._(failure: failure);
|
||||||
|
|
||||||
|
bool get isSuccess => data != null;
|
||||||
|
bool get isFailure => failure != null;
|
||||||
|
|
||||||
|
R when<R>({
|
||||||
|
required R Function(T data) success,
|
||||||
|
required R Function(AppFailure failure) failure,
|
||||||
|
}) {
|
||||||
|
if (this.data != null) {
|
||||||
|
return success(this.data as T);
|
||||||
|
} else {
|
||||||
|
return failure(this.failure!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,6 @@ class LoginDiagnostics {
|
|||||||
/// 환경 설정 확인
|
/// 환경 설정 확인
|
||||||
static Map<String, dynamic> _checkEnvironment() {
|
static Map<String, dynamic> _checkEnvironment() {
|
||||||
return {
|
return {
|
||||||
'useApi': env.Environment.useApi,
|
|
||||||
'apiBaseUrl': env.Environment.apiBaseUrl,
|
'apiBaseUrl': env.Environment.apiBaseUrl,
|
||||||
'isDebugMode': kDebugMode,
|
'isDebugMode': kDebugMode,
|
||||||
'platform': defaultTargetPlatform.toString(),
|
'platform': defaultTargetPlatform.toString(),
|
||||||
@@ -70,20 +69,18 @@ class LoginDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// API 서버 연결 테스트
|
// API 서버 연결 테스트
|
||||||
if (env.Environment.useApi) {
|
try {
|
||||||
try {
|
final response = await dio.get(
|
||||||
final response = await dio.get(
|
'${env.Environment.apiBaseUrl}/health',
|
||||||
'${env.Environment.apiBaseUrl}/health',
|
options: Options(
|
||||||
options: Options(
|
validateStatus: (status) => status != null && status < 500,
|
||||||
validateStatus: (status) => status != null && status < 500,
|
),
|
||||||
),
|
);
|
||||||
);
|
results['apiServerReachable'] = true;
|
||||||
results['apiServerReachable'] = true;
|
results['apiServerStatus'] = response.statusCode;
|
||||||
results['apiServerStatus'] = response.statusCode;
|
} catch (e) {
|
||||||
} catch (e) {
|
results['apiServerReachable'] = false;
|
||||||
results['apiServerReachable'] = false;
|
results['apiServerError'] = e.toString();
|
||||||
results['apiServerError'] = e.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
@@ -91,9 +88,6 @@ class LoginDiagnostics {
|
|||||||
|
|
||||||
/// API 엔드포인트 확인
|
/// API 엔드포인트 확인
|
||||||
static Future<Map<String, dynamic>> _checkApiEndpoint() async {
|
static Future<Map<String, dynamic>> _checkApiEndpoint() async {
|
||||||
if (!env.Environment.useApi) {
|
|
||||||
return {'mode': 'mock', 'skip': true};
|
|
||||||
}
|
|
||||||
|
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
final results = <String, dynamic>{};
|
final results = <String, dynamic>{};
|
||||||
|
|||||||
167
lib/core/widgets/auth_guard.dart
Normal file
167
lib/core/widgets/auth_guard.dart
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
import '../../data/models/auth/auth_user.dart';
|
||||||
|
|
||||||
|
/// 역할 기반 접근 제어를 위한 AuthGuard 위젯
|
||||||
|
///
|
||||||
|
/// 사용자의 역할을 확인하여 허용된 역할만 child 위젯에 접근할 수 있도록 제어합니다.
|
||||||
|
/// 권한이 없는 경우 UnauthorizedScreen을 표시합니다.
|
||||||
|
class AuthGuard extends StatelessWidget {
|
||||||
|
/// 접근을 허용할 역할 목록
|
||||||
|
final List<String> allowedRoles;
|
||||||
|
|
||||||
|
/// 권한이 있을 때 표시할 위젯
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// 권한이 없을 때 표시할 커스텀 위젯 (선택사항)
|
||||||
|
final Widget? unauthorizedWidget;
|
||||||
|
|
||||||
|
/// 권한 체크를 건너뛸지 여부 (개발 모드용)
|
||||||
|
final bool skipCheck;
|
||||||
|
|
||||||
|
const AuthGuard({
|
||||||
|
super.key,
|
||||||
|
required this.allowedRoles,
|
||||||
|
required this.child,
|
||||||
|
this.unauthorizedWidget,
|
||||||
|
this.skipCheck = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// 개발 모드에서 권한 체크 건너뛰기
|
||||||
|
if (skipCheck) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthService에서 현재 사용자 정보 가져오기
|
||||||
|
final authService = context.read<AuthService>();
|
||||||
|
|
||||||
|
return FutureBuilder<AuthUser?>(
|
||||||
|
future: authService.getCurrentUser(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
// 로딩 중
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 사용자 정보
|
||||||
|
final currentUser = snapshot.data;
|
||||||
|
|
||||||
|
// 로그인하지 않은 경우
|
||||||
|
if (currentUser == null) {
|
||||||
|
return unauthorizedWidget ?? const UnauthorizedScreen(
|
||||||
|
message: '로그인이 필요합니다.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 역할 확인 - 대소문자 구분 없이 비교
|
||||||
|
final userRole = currentUser.role.toLowerCase();
|
||||||
|
final hasPermission = allowedRoles.any(
|
||||||
|
(role) => role.toLowerCase() == userRole,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 권한이 있는 경우
|
||||||
|
if (hasPermission) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 권한이 없는 경우
|
||||||
|
return unauthorizedWidget ?? UnauthorizedScreen(
|
||||||
|
message: '이 페이지에 접근할 권한이 없습니다.',
|
||||||
|
userRole: currentUser.role,
|
||||||
|
requiredRoles: allowedRoles,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 권한이 없을 때 표시되는 기본 화면
|
||||||
|
class UnauthorizedScreen extends StatelessWidget {
|
||||||
|
final String message;
|
||||||
|
final String? userRole;
|
||||||
|
final List<String>? requiredRoles;
|
||||||
|
|
||||||
|
const UnauthorizedScreen({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
this.userRole,
|
||||||
|
this.requiredRoles,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.lock_outline,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'접근 권한 없음',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
if (userRole != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'현재 권한: $userRole',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (requiredRoles != null && requiredRoles!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'필요한 권한: ${requiredRoles!.join(", ")}',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushReplacementNamed('/dashboard');
|
||||||
|
},
|
||||||
|
child: const Text('대시보드로 이동'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 역할 상수 정의
|
||||||
|
class UserRole {
|
||||||
|
static const String admin = 'Admin';
|
||||||
|
static const String manager = 'Manager';
|
||||||
|
static const String member = 'Member';
|
||||||
|
|
||||||
|
/// 관리자 및 매니저 권한
|
||||||
|
static const List<String> adminAndManager = [admin, manager];
|
||||||
|
|
||||||
|
/// 모든 권한
|
||||||
|
static const List<String> all = [admin, manager, member];
|
||||||
|
|
||||||
|
/// 관리자 전용
|
||||||
|
static const List<String> adminOnly = [admin];
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../../../core/config/environment.dart';
|
import '../../../core/config/environment.dart';
|
||||||
|
import '../../../core/constants/app_constants.dart';
|
||||||
import 'interceptors/auth_interceptor.dart';
|
import 'interceptors/auth_interceptor.dart';
|
||||||
import 'interceptors/error_interceptor.dart';
|
import 'interceptors/error_interceptor.dart';
|
||||||
import 'interceptors/logging_interceptor.dart';
|
import 'interceptors/logging_interceptor.dart';
|
||||||
@@ -41,8 +42,8 @@ class ApiClient {
|
|||||||
// 기본값으로 초기화
|
// 기본값으로 초기화
|
||||||
_dio = Dio(BaseOptions(
|
_dio = Dio(BaseOptions(
|
||||||
baseUrl: 'http://43.201.34.104:8080/api/v1',
|
baseUrl: 'http://43.201.34.104:8080/api/v1',
|
||||||
connectTimeout: const Duration(seconds: 30),
|
connectTimeout: AppConstants.apiConnectTimeout,
|
||||||
receiveTimeout: const Duration(seconds: 30),
|
receiveTimeout: AppConstants.apiReceiveTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
@@ -77,8 +78,8 @@ class ApiClient {
|
|||||||
// Environment가 초기화되지 않은 경우 기본값 사용
|
// Environment가 초기화되지 않은 경우 기본값 사용
|
||||||
return BaseOptions(
|
return BaseOptions(
|
||||||
baseUrl: 'http://43.201.34.104:8080/api/v1',
|
baseUrl: 'http://43.201.34.104:8080/api/v1',
|
||||||
connectTimeout: const Duration(seconds: 30),
|
connectTimeout: AppConstants.apiConnectTimeout,
|
||||||
receiveTimeout: const Duration(seconds: 30),
|
receiveTimeout: AppConstants.apiReceiveTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:superport/core/errors/exceptions.dart';
|
|||||||
import 'package:superport/data/datasources/remote/api_client.dart';
|
import 'package:superport/data/datasources/remote/api_client.dart';
|
||||||
import 'package:superport/data/models/license/license_dto.dart';
|
import 'package:superport/data/models/license/license_dto.dart';
|
||||||
import 'package:superport/data/models/license/license_request_dto.dart';
|
import 'package:superport/data/models/license/license_request_dto.dart';
|
||||||
import 'package:superport/data/models/license/license_query_dto.dart';
|
|
||||||
|
|
||||||
abstract class LicenseRemoteDataSource {
|
abstract class LicenseRemoteDataSource {
|
||||||
Future<LicenseListResponseDto> getLicenses({
|
Future<LicenseListResponseDto> getLicenses({
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:superport/core/constants/api_endpoints.dart';
|
||||||
|
import 'package:superport/core/errors/exceptions.dart';
|
||||||
|
import 'package:superport/data/datasources/remote/api_client.dart';
|
||||||
|
import 'package:superport/data/models/warehouse/warehouse_dto.dart';
|
||||||
|
|
||||||
|
abstract class WarehouseLocationRemoteDataSource {
|
||||||
|
Future<WarehouseLocationListDto> getWarehouseLocations({
|
||||||
|
int page = 1,
|
||||||
|
int perPage = 20,
|
||||||
|
String? search,
|
||||||
|
Map<String, dynamic>? filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<WarehouseLocationDto> getWarehouseLocationDetail(int id);
|
||||||
|
Future<WarehouseLocationDto> createWarehouseLocation(CreateWarehouseLocationRequest request);
|
||||||
|
Future<WarehouseLocationDto> updateWarehouseLocation(int id, UpdateWarehouseLocationRequest request);
|
||||||
|
Future<void> deleteWarehouseLocation(int id);
|
||||||
|
Future<WarehouseCapacityInfo> getWarehouseCapacity(int id);
|
||||||
|
Future<WarehouseEquipmentListDto> getWarehouseEquipment({
|
||||||
|
required int warehouseId,
|
||||||
|
int page = 1,
|
||||||
|
int perPage = 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@LazySingleton(as: WarehouseLocationRemoteDataSource)
|
||||||
|
class WarehouseLocationRemoteDataSourceImpl implements WarehouseLocationRemoteDataSource {
|
||||||
|
final ApiClient _apiClient;
|
||||||
|
|
||||||
|
WarehouseLocationRemoteDataSourceImpl({
|
||||||
|
required ApiClient apiClient,
|
||||||
|
}) : _apiClient = apiClient;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseLocationListDto> getWarehouseLocations({
|
||||||
|
int page = 1,
|
||||||
|
int perPage = 20,
|
||||||
|
String? search,
|
||||||
|
Map<String, dynamic>? filters,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final queryParams = <String, dynamic>{
|
||||||
|
'page': page,
|
||||||
|
'per_page': perPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (search != null && search.isNotEmpty) {
|
||||||
|
queryParams['search'] = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터 적용
|
||||||
|
if (filters != null) {
|
||||||
|
filters.forEach((key, value) {
|
||||||
|
if (value != null) {
|
||||||
|
queryParams[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
ApiEndpoints.warehouseLocations,
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data != null && response.data['success'] == true && response.data['data'] != null) {
|
||||||
|
// API 응답이 배열인 경우와 객체인 경우를 모두 처리
|
||||||
|
final data = response.data['data'];
|
||||||
|
if (data is List) {
|
||||||
|
// 배열 응답을 WarehouseLocationListDto 형식으로 변환
|
||||||
|
final List<WarehouseLocationDto> warehouses = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
try {
|
||||||
|
final item = data[i];
|
||||||
|
debugPrint('📦 Parsing warehouse location item $i: ${item['name']}');
|
||||||
|
|
||||||
|
// null 검사 및 기본값 설정
|
||||||
|
final warehouseDto = WarehouseLocationDto.fromJson({
|
||||||
|
...item,
|
||||||
|
// 필수 필드 보장
|
||||||
|
'name': item['name'] ?? '',
|
||||||
|
'is_active': item['is_active'] ?? true,
|
||||||
|
'created_at': item['created_at'] ?? DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
warehouses.add(warehouseDto);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('❌ Error parsing warehouse location item $i: $e');
|
||||||
|
debugPrint('Item data: ${data[i]}');
|
||||||
|
debugPrint('Stack trace: $stackTrace');
|
||||||
|
// 파싱 실패한 항목은 건너뛰고 계속
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final pagination = response.data['pagination'] ?? {};
|
||||||
|
return WarehouseLocationListDto(
|
||||||
|
items: warehouses,
|
||||||
|
total: pagination['total'] ?? warehouses.length,
|
||||||
|
page: pagination['page'] ?? page,
|
||||||
|
perPage: pagination['per_page'] ?? perPage,
|
||||||
|
totalPages: pagination['total_pages'] ?? 1,
|
||||||
|
);
|
||||||
|
} else if (data['items'] != null) {
|
||||||
|
// 이미 WarehouseLocationListDto 형식인 경우
|
||||||
|
return WarehouseLocationListDto.fromJson(data);
|
||||||
|
} else {
|
||||||
|
// 예상치 못한 형식인 경우
|
||||||
|
throw ApiException(
|
||||||
|
message: 'Unexpected response format for warehouse location list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ApiException(
|
||||||
|
message: response.data?['error']?['message'] ?? 'Failed to fetch warehouse locations',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseLocationDto> getWarehouseLocationDetail(int id) async {
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
'${ApiEndpoints.warehouseLocations}/$id',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data != null && response.data['success'] == true && response.data['data'] != null) {
|
||||||
|
return WarehouseLocationDto.fromJson(response.data['data']);
|
||||||
|
} else {
|
||||||
|
throw ApiException(
|
||||||
|
message: response.data?['error']?['message'] ?? 'Failed to fetch warehouse location',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseLocationDto> createWarehouseLocation(CreateWarehouseLocationRequest request) async {
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.post(
|
||||||
|
ApiEndpoints.warehouseLocations,
|
||||||
|
data: request.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data != null && response.data['success'] == true && response.data['data'] != null) {
|
||||||
|
return WarehouseLocationDto.fromJson(response.data['data']);
|
||||||
|
} else {
|
||||||
|
throw ApiException(
|
||||||
|
message: response.data?['error']?['message'] ?? 'Failed to create warehouse location',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseLocationDto> updateWarehouseLocation(int id, UpdateWarehouseLocationRequest request) async {
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.put(
|
||||||
|
'${ApiEndpoints.warehouseLocations}/$id',
|
||||||
|
data: request.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data != null && response.data['success'] == true && response.data['data'] != null) {
|
||||||
|
return WarehouseLocationDto.fromJson(response.data['data']);
|
||||||
|
} else {
|
||||||
|
throw ApiException(
|
||||||
|
message: response.data?['error']?['message'] ?? 'Failed to update warehouse location',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteWarehouseLocation(int id) async {
|
||||||
|
try {
|
||||||
|
await _apiClient.delete(
|
||||||
|
'${ApiEndpoints.warehouseLocations}/$id',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseCapacityInfo> getWarehouseCapacity(int id) async {
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
'${ApiEndpoints.warehouseLocations}/$id/capacity',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data != null && response.data['success'] == true && response.data['data'] != null) {
|
||||||
|
return WarehouseCapacityInfo.fromJson(response.data['data']);
|
||||||
|
} else {
|
||||||
|
throw ApiException(
|
||||||
|
message: response.data?['error']?['message'] ?? 'Failed to fetch warehouse capacity',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseEquipmentListDto> getWarehouseEquipment({
|
||||||
|
required int warehouseId,
|
||||||
|
int page = 1,
|
||||||
|
int perPage = 20,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final queryParams = <String, dynamic>{
|
||||||
|
'page': page,
|
||||||
|
'per_page': perPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
'${ApiEndpoints.warehouseLocations}/$warehouseId/equipment',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data != null && response.data['success'] == true && response.data['data'] != null) {
|
||||||
|
final data = response.data['data'];
|
||||||
|
final pagination = response.data['pagination'] ?? {};
|
||||||
|
|
||||||
|
if (data is List) {
|
||||||
|
// 배열 응답을 WarehouseEquipmentListDto 형식으로 변환
|
||||||
|
final List<WarehouseEquipmentDto> equipment = [];
|
||||||
|
|
||||||
|
for (var item in data) {
|
||||||
|
try {
|
||||||
|
equipment.add(WarehouseEquipmentDto.fromJson(item));
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error parsing warehouse equipment: $e');
|
||||||
|
debugPrint('Item data: $item');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WarehouseEquipmentListDto(
|
||||||
|
items: equipment,
|
||||||
|
total: pagination['total'] ?? equipment.length,
|
||||||
|
page: pagination['page'] ?? page,
|
||||||
|
perPage: pagination['per_page'] ?? perPage,
|
||||||
|
totalPages: pagination['total_pages'] ?? 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 이미 올바른 형식인 경우
|
||||||
|
return WarehouseEquipmentListDto.fromJson(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw ApiException(
|
||||||
|
message: response.data?['error']?['message'] ?? 'Failed to fetch warehouse equipment',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw _handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception _handleError(dynamic error) {
|
||||||
|
if (error is ApiException) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
return ApiException(
|
||||||
|
message: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
lib/data/models/common/pagination_params.dart
Normal file
52
lib/data/models/common/pagination_params.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:superport/core/constants/app_constants.dart';
|
||||||
|
|
||||||
|
part 'pagination_params.freezed.dart';
|
||||||
|
part 'pagination_params.g.dart';
|
||||||
|
|
||||||
|
/// 페이지네이션 요청 파라미터를 위한 표준화된 클래스
|
||||||
|
@freezed
|
||||||
|
class PaginationParams with _$PaginationParams {
|
||||||
|
const factory PaginationParams({
|
||||||
|
@Default(1) int page,
|
||||||
|
@Default(AppConstants.defaultPageSize) int perPage,
|
||||||
|
String? search,
|
||||||
|
String? sortBy,
|
||||||
|
@Default('asc') String sortOrder,
|
||||||
|
Map<String, dynamic>? filters,
|
||||||
|
}) = _PaginationParams;
|
||||||
|
|
||||||
|
factory PaginationParams.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PaginationParamsFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 페이지네이션 메타데이터
|
||||||
|
@freezed
|
||||||
|
class PaginationMeta with _$PaginationMeta {
|
||||||
|
const factory PaginationMeta({
|
||||||
|
required int currentPage,
|
||||||
|
required int perPage,
|
||||||
|
required int total,
|
||||||
|
required int totalPages,
|
||||||
|
required bool hasNext,
|
||||||
|
required bool hasPrevious,
|
||||||
|
}) = _PaginationMeta;
|
||||||
|
|
||||||
|
factory PaginationMeta.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PaginationMetaFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 페이지네이션된 결과를 위한 래퍼 클래스
|
||||||
|
@Freezed(genericArgumentFactories: true)
|
||||||
|
class PagedResult<T> with _$PagedResult<T> {
|
||||||
|
const factory PagedResult({
|
||||||
|
required List<T> items,
|
||||||
|
required PaginationMeta meta,
|
||||||
|
}) = _PagedResult<T>;
|
||||||
|
|
||||||
|
factory PagedResult.fromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
T Function(Object?) fromJsonT,
|
||||||
|
) =>
|
||||||
|
_$PagedResultFromJson<T>(json, fromJsonT);
|
||||||
|
}
|
||||||
733
lib/data/models/common/pagination_params.freezed.dart
Normal file
733
lib/data/models/common/pagination_params.freezed.dart
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'pagination_params.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) {
|
||||||
|
return _PaginationParams.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$PaginationParams {
|
||||||
|
int get page => throw _privateConstructorUsedError;
|
||||||
|
int get perPage => throw _privateConstructorUsedError;
|
||||||
|
String? get search => throw _privateConstructorUsedError;
|
||||||
|
String? get sortBy => throw _privateConstructorUsedError;
|
||||||
|
String get sortOrder => throw _privateConstructorUsedError;
|
||||||
|
Map<String, dynamic>? get filters => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this PaginationParams to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of PaginationParams
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$PaginationParamsCopyWith<PaginationParams> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $PaginationParamsCopyWith<$Res> {
|
||||||
|
factory $PaginationParamsCopyWith(
|
||||||
|
PaginationParams value, $Res Function(PaginationParams) then) =
|
||||||
|
_$PaginationParamsCopyWithImpl<$Res, PaginationParams>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int page,
|
||||||
|
int perPage,
|
||||||
|
String? search,
|
||||||
|
String? sortBy,
|
||||||
|
String sortOrder,
|
||||||
|
Map<String, dynamic>? filters});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PaginationParamsCopyWithImpl<$Res, $Val extends PaginationParams>
|
||||||
|
implements $PaginationParamsCopyWith<$Res> {
|
||||||
|
_$PaginationParamsCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of PaginationParams
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? page = null,
|
||||||
|
Object? perPage = null,
|
||||||
|
Object? search = freezed,
|
||||||
|
Object? sortBy = freezed,
|
||||||
|
Object? sortOrder = null,
|
||||||
|
Object? filters = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
page: null == page
|
||||||
|
? _value.page
|
||||||
|
: page // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
perPage: null == perPage
|
||||||
|
? _value.perPage
|
||||||
|
: perPage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
search: freezed == search
|
||||||
|
? _value.search
|
||||||
|
: search // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
sortBy: freezed == sortBy
|
||||||
|
? _value.sortBy
|
||||||
|
: sortBy // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
sortOrder: null == sortOrder
|
||||||
|
? _value.sortOrder
|
||||||
|
: sortOrder // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
filters: freezed == filters
|
||||||
|
? _value.filters
|
||||||
|
: filters // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$PaginationParamsImplCopyWith<$Res>
|
||||||
|
implements $PaginationParamsCopyWith<$Res> {
|
||||||
|
factory _$$PaginationParamsImplCopyWith(_$PaginationParamsImpl value,
|
||||||
|
$Res Function(_$PaginationParamsImpl) then) =
|
||||||
|
__$$PaginationParamsImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int page,
|
||||||
|
int perPage,
|
||||||
|
String? search,
|
||||||
|
String? sortBy,
|
||||||
|
String sortOrder,
|
||||||
|
Map<String, dynamic>? filters});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$PaginationParamsImplCopyWithImpl<$Res>
|
||||||
|
extends _$PaginationParamsCopyWithImpl<$Res, _$PaginationParamsImpl>
|
||||||
|
implements _$$PaginationParamsImplCopyWith<$Res> {
|
||||||
|
__$$PaginationParamsImplCopyWithImpl(_$PaginationParamsImpl _value,
|
||||||
|
$Res Function(_$PaginationParamsImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of PaginationParams
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? page = null,
|
||||||
|
Object? perPage = null,
|
||||||
|
Object? search = freezed,
|
||||||
|
Object? sortBy = freezed,
|
||||||
|
Object? sortOrder = null,
|
||||||
|
Object? filters = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$PaginationParamsImpl(
|
||||||
|
page: null == page
|
||||||
|
? _value.page
|
||||||
|
: page // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
perPage: null == perPage
|
||||||
|
? _value.perPage
|
||||||
|
: perPage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
search: freezed == search
|
||||||
|
? _value.search
|
||||||
|
: search // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
sortBy: freezed == sortBy
|
||||||
|
? _value.sortBy
|
||||||
|
: sortBy // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
sortOrder: null == sortOrder
|
||||||
|
? _value.sortOrder
|
||||||
|
: sortOrder // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
filters: freezed == filters
|
||||||
|
? _value._filters
|
||||||
|
: filters // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, dynamic>?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$PaginationParamsImpl implements _PaginationParams {
|
||||||
|
const _$PaginationParamsImpl(
|
||||||
|
{this.page = 1,
|
||||||
|
this.perPage = AppConstants.defaultPageSize,
|
||||||
|
this.search,
|
||||||
|
this.sortBy,
|
||||||
|
this.sortOrder = 'asc',
|
||||||
|
final Map<String, dynamic>? filters})
|
||||||
|
: _filters = filters;
|
||||||
|
|
||||||
|
factory _$PaginationParamsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$PaginationParamsImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int page;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int perPage;
|
||||||
|
@override
|
||||||
|
final String? search;
|
||||||
|
@override
|
||||||
|
final String? sortBy;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final String sortOrder;
|
||||||
|
final Map<String, dynamic>? _filters;
|
||||||
|
@override
|
||||||
|
Map<String, dynamic>? get filters {
|
||||||
|
final value = _filters;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_filters is EqualUnmodifiableMapView) return _filters;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PaginationParams(page: $page, perPage: $perPage, search: $search, sortBy: $sortBy, sortOrder: $sortOrder, filters: $filters)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$PaginationParamsImpl &&
|
||||||
|
(identical(other.page, page) || other.page == page) &&
|
||||||
|
(identical(other.perPage, perPage) || other.perPage == perPage) &&
|
||||||
|
(identical(other.search, search) || other.search == search) &&
|
||||||
|
(identical(other.sortBy, sortBy) || other.sortBy == sortBy) &&
|
||||||
|
(identical(other.sortOrder, sortOrder) ||
|
||||||
|
other.sortOrder == sortOrder) &&
|
||||||
|
const DeepCollectionEquality().equals(other._filters, _filters));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, page, perPage, search, sortBy,
|
||||||
|
sortOrder, const DeepCollectionEquality().hash(_filters));
|
||||||
|
|
||||||
|
/// Create a copy of PaginationParams
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$PaginationParamsImplCopyWith<_$PaginationParamsImpl> get copyWith =>
|
||||||
|
__$$PaginationParamsImplCopyWithImpl<_$PaginationParamsImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$PaginationParamsImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _PaginationParams implements PaginationParams {
|
||||||
|
const factory _PaginationParams(
|
||||||
|
{final int page,
|
||||||
|
final int perPage,
|
||||||
|
final String? search,
|
||||||
|
final String? sortBy,
|
||||||
|
final String sortOrder,
|
||||||
|
final Map<String, dynamic>? filters}) = _$PaginationParamsImpl;
|
||||||
|
|
||||||
|
factory _PaginationParams.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$PaginationParamsImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get page;
|
||||||
|
@override
|
||||||
|
int get perPage;
|
||||||
|
@override
|
||||||
|
String? get search;
|
||||||
|
@override
|
||||||
|
String? get sortBy;
|
||||||
|
@override
|
||||||
|
String get sortOrder;
|
||||||
|
@override
|
||||||
|
Map<String, dynamic>? get filters;
|
||||||
|
|
||||||
|
/// Create a copy of PaginationParams
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$PaginationParamsImplCopyWith<_$PaginationParamsImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaginationMeta _$PaginationMetaFromJson(Map<String, dynamic> json) {
|
||||||
|
return _PaginationMeta.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$PaginationMeta {
|
||||||
|
int get currentPage => throw _privateConstructorUsedError;
|
||||||
|
int get perPage => throw _privateConstructorUsedError;
|
||||||
|
int get total => throw _privateConstructorUsedError;
|
||||||
|
int get totalPages => throw _privateConstructorUsedError;
|
||||||
|
bool get hasNext => throw _privateConstructorUsedError;
|
||||||
|
bool get hasPrevious => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this PaginationMeta to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of PaginationMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$PaginationMetaCopyWith<PaginationMeta> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $PaginationMetaCopyWith<$Res> {
|
||||||
|
factory $PaginationMetaCopyWith(
|
||||||
|
PaginationMeta value, $Res Function(PaginationMeta) then) =
|
||||||
|
_$PaginationMetaCopyWithImpl<$Res, PaginationMeta>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int currentPage,
|
||||||
|
int perPage,
|
||||||
|
int total,
|
||||||
|
int totalPages,
|
||||||
|
bool hasNext,
|
||||||
|
bool hasPrevious});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PaginationMetaCopyWithImpl<$Res, $Val extends PaginationMeta>
|
||||||
|
implements $PaginationMetaCopyWith<$Res> {
|
||||||
|
_$PaginationMetaCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of PaginationMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? currentPage = null,
|
||||||
|
Object? perPage = null,
|
||||||
|
Object? total = null,
|
||||||
|
Object? totalPages = null,
|
||||||
|
Object? hasNext = null,
|
||||||
|
Object? hasPrevious = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
currentPage: null == currentPage
|
||||||
|
? _value.currentPage
|
||||||
|
: currentPage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
perPage: null == perPage
|
||||||
|
? _value.perPage
|
||||||
|
: perPage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
total: null == total
|
||||||
|
? _value.total
|
||||||
|
: total // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
totalPages: null == totalPages
|
||||||
|
? _value.totalPages
|
||||||
|
: totalPages // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
hasNext: null == hasNext
|
||||||
|
? _value.hasNext
|
||||||
|
: hasNext // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
hasPrevious: null == hasPrevious
|
||||||
|
? _value.hasPrevious
|
||||||
|
: hasPrevious // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$PaginationMetaImplCopyWith<$Res>
|
||||||
|
implements $PaginationMetaCopyWith<$Res> {
|
||||||
|
factory _$$PaginationMetaImplCopyWith(_$PaginationMetaImpl value,
|
||||||
|
$Res Function(_$PaginationMetaImpl) then) =
|
||||||
|
__$$PaginationMetaImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int currentPage,
|
||||||
|
int perPage,
|
||||||
|
int total,
|
||||||
|
int totalPages,
|
||||||
|
bool hasNext,
|
||||||
|
bool hasPrevious});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$PaginationMetaImplCopyWithImpl<$Res>
|
||||||
|
extends _$PaginationMetaCopyWithImpl<$Res, _$PaginationMetaImpl>
|
||||||
|
implements _$$PaginationMetaImplCopyWith<$Res> {
|
||||||
|
__$$PaginationMetaImplCopyWithImpl(
|
||||||
|
_$PaginationMetaImpl _value, $Res Function(_$PaginationMetaImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of PaginationMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? currentPage = null,
|
||||||
|
Object? perPage = null,
|
||||||
|
Object? total = null,
|
||||||
|
Object? totalPages = null,
|
||||||
|
Object? hasNext = null,
|
||||||
|
Object? hasPrevious = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$PaginationMetaImpl(
|
||||||
|
currentPage: null == currentPage
|
||||||
|
? _value.currentPage
|
||||||
|
: currentPage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
perPage: null == perPage
|
||||||
|
? _value.perPage
|
||||||
|
: perPage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
total: null == total
|
||||||
|
? _value.total
|
||||||
|
: total // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
totalPages: null == totalPages
|
||||||
|
? _value.totalPages
|
||||||
|
: totalPages // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
hasNext: null == hasNext
|
||||||
|
? _value.hasNext
|
||||||
|
: hasNext // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
hasPrevious: null == hasPrevious
|
||||||
|
? _value.hasPrevious
|
||||||
|
: hasPrevious // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$PaginationMetaImpl implements _PaginationMeta {
|
||||||
|
const _$PaginationMetaImpl(
|
||||||
|
{required this.currentPage,
|
||||||
|
required this.perPage,
|
||||||
|
required this.total,
|
||||||
|
required this.totalPages,
|
||||||
|
required this.hasNext,
|
||||||
|
required this.hasPrevious});
|
||||||
|
|
||||||
|
factory _$PaginationMetaImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$PaginationMetaImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int currentPage;
|
||||||
|
@override
|
||||||
|
final int perPage;
|
||||||
|
@override
|
||||||
|
final int total;
|
||||||
|
@override
|
||||||
|
final int totalPages;
|
||||||
|
@override
|
||||||
|
final bool hasNext;
|
||||||
|
@override
|
||||||
|
final bool hasPrevious;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PaginationMeta(currentPage: $currentPage, perPage: $perPage, total: $total, totalPages: $totalPages, hasNext: $hasNext, hasPrevious: $hasPrevious)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$PaginationMetaImpl &&
|
||||||
|
(identical(other.currentPage, currentPage) ||
|
||||||
|
other.currentPage == currentPage) &&
|
||||||
|
(identical(other.perPage, perPage) || other.perPage == perPage) &&
|
||||||
|
(identical(other.total, total) || other.total == total) &&
|
||||||
|
(identical(other.totalPages, totalPages) ||
|
||||||
|
other.totalPages == totalPages) &&
|
||||||
|
(identical(other.hasNext, hasNext) || other.hasNext == hasNext) &&
|
||||||
|
(identical(other.hasPrevious, hasPrevious) ||
|
||||||
|
other.hasPrevious == hasPrevious));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, currentPage, perPage, total,
|
||||||
|
totalPages, hasNext, hasPrevious);
|
||||||
|
|
||||||
|
/// Create a copy of PaginationMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$PaginationMetaImplCopyWith<_$PaginationMetaImpl> get copyWith =>
|
||||||
|
__$$PaginationMetaImplCopyWithImpl<_$PaginationMetaImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$PaginationMetaImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _PaginationMeta implements PaginationMeta {
|
||||||
|
const factory _PaginationMeta(
|
||||||
|
{required final int currentPage,
|
||||||
|
required final int perPage,
|
||||||
|
required final int total,
|
||||||
|
required final int totalPages,
|
||||||
|
required final bool hasNext,
|
||||||
|
required final bool hasPrevious}) = _$PaginationMetaImpl;
|
||||||
|
|
||||||
|
factory _PaginationMeta.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$PaginationMetaImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get currentPage;
|
||||||
|
@override
|
||||||
|
int get perPage;
|
||||||
|
@override
|
||||||
|
int get total;
|
||||||
|
@override
|
||||||
|
int get totalPages;
|
||||||
|
@override
|
||||||
|
bool get hasNext;
|
||||||
|
@override
|
||||||
|
bool get hasPrevious;
|
||||||
|
|
||||||
|
/// Create a copy of PaginationMeta
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$PaginationMetaImplCopyWith<_$PaginationMetaImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
PagedResult<T> _$PagedResultFromJson<T>(
|
||||||
|
Map<String, dynamic> json, T Function(Object?) fromJsonT) {
|
||||||
|
return _PagedResult<T>.fromJson(json, fromJsonT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$PagedResult<T> {
|
||||||
|
List<T> get items => throw _privateConstructorUsedError;
|
||||||
|
PaginationMeta get meta => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this PagedResult to a JSON map.
|
||||||
|
Map<String, dynamic> toJson(Object? Function(T) toJsonT) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of PagedResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$PagedResultCopyWith<T, PagedResult<T>> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $PagedResultCopyWith<T, $Res> {
|
||||||
|
factory $PagedResultCopyWith(
|
||||||
|
PagedResult<T> value, $Res Function(PagedResult<T>) then) =
|
||||||
|
_$PagedResultCopyWithImpl<T, $Res, PagedResult<T>>;
|
||||||
|
@useResult
|
||||||
|
$Res call({List<T> items, PaginationMeta meta});
|
||||||
|
|
||||||
|
$PaginationMetaCopyWith<$Res> get meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PagedResultCopyWithImpl<T, $Res, $Val extends PagedResult<T>>
|
||||||
|
implements $PagedResultCopyWith<T, $Res> {
|
||||||
|
_$PagedResultCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of PagedResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? items = null,
|
||||||
|
Object? meta = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
items: null == items
|
||||||
|
? _value.items
|
||||||
|
: items // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<T>,
|
||||||
|
meta: null == meta
|
||||||
|
? _value.meta
|
||||||
|
: meta // ignore: cast_nullable_to_non_nullable
|
||||||
|
as PaginationMeta,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of PagedResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$PaginationMetaCopyWith<$Res> get meta {
|
||||||
|
return $PaginationMetaCopyWith<$Res>(_value.meta, (value) {
|
||||||
|
return _then(_value.copyWith(meta: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$PagedResultImplCopyWith<T, $Res>
|
||||||
|
implements $PagedResultCopyWith<T, $Res> {
|
||||||
|
factory _$$PagedResultImplCopyWith(_$PagedResultImpl<T> value,
|
||||||
|
$Res Function(_$PagedResultImpl<T>) then) =
|
||||||
|
__$$PagedResultImplCopyWithImpl<T, $Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({List<T> items, PaginationMeta meta});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$PaginationMetaCopyWith<$Res> get meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$PagedResultImplCopyWithImpl<T, $Res>
|
||||||
|
extends _$PagedResultCopyWithImpl<T, $Res, _$PagedResultImpl<T>>
|
||||||
|
implements _$$PagedResultImplCopyWith<T, $Res> {
|
||||||
|
__$$PagedResultImplCopyWithImpl(
|
||||||
|
_$PagedResultImpl<T> _value, $Res Function(_$PagedResultImpl<T>) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of PagedResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? items = null,
|
||||||
|
Object? meta = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$PagedResultImpl<T>(
|
||||||
|
items: null == items
|
||||||
|
? _value._items
|
||||||
|
: items // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<T>,
|
||||||
|
meta: null == meta
|
||||||
|
? _value.meta
|
||||||
|
: meta // ignore: cast_nullable_to_non_nullable
|
||||||
|
as PaginationMeta,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable(genericArgumentFactories: true)
|
||||||
|
class _$PagedResultImpl<T> implements _PagedResult<T> {
|
||||||
|
const _$PagedResultImpl({required final List<T> items, required this.meta})
|
||||||
|
: _items = items;
|
||||||
|
|
||||||
|
factory _$PagedResultImpl.fromJson(
|
||||||
|
Map<String, dynamic> json, T Function(Object?) fromJsonT) =>
|
||||||
|
_$$PagedResultImplFromJson(json, fromJsonT);
|
||||||
|
|
||||||
|
final List<T> _items;
|
||||||
|
@override
|
||||||
|
List<T> get items {
|
||||||
|
if (_items is EqualUnmodifiableListView) return _items;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final PaginationMeta meta;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PagedResult<$T>(items: $items, meta: $meta)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$PagedResultImpl<T> &&
|
||||||
|
const DeepCollectionEquality().equals(other._items, _items) &&
|
||||||
|
(identical(other.meta, meta) || other.meta == meta));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType, const DeepCollectionEquality().hash(_items), meta);
|
||||||
|
|
||||||
|
/// Create a copy of PagedResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$PagedResultImplCopyWith<T, _$PagedResultImpl<T>> get copyWith =>
|
||||||
|
__$$PagedResultImplCopyWithImpl<T, _$PagedResultImpl<T>>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson(Object? Function(T) toJsonT) {
|
||||||
|
return _$$PagedResultImplToJson<T>(this, toJsonT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _PagedResult<T> implements PagedResult<T> {
|
||||||
|
const factory _PagedResult(
|
||||||
|
{required final List<T> items,
|
||||||
|
required final PaginationMeta meta}) = _$PagedResultImpl<T>;
|
||||||
|
|
||||||
|
factory _PagedResult.fromJson(
|
||||||
|
Map<String, dynamic> json, T Function(Object?) fromJsonT) =
|
||||||
|
_$PagedResultImpl<T>.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<T> get items;
|
||||||
|
@override
|
||||||
|
PaginationMeta get meta;
|
||||||
|
|
||||||
|
/// Create a copy of PagedResult
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$PagedResultImplCopyWith<T, _$PagedResultImpl<T>> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
69
lib/data/models/common/pagination_params.g.dart
Normal file
69
lib/data/models/common/pagination_params.g.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'pagination_params.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$PaginationParamsImpl _$$PaginationParamsImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$PaginationParamsImpl(
|
||||||
|
page: (json['page'] as num?)?.toInt() ?? 1,
|
||||||
|
perPage:
|
||||||
|
(json['perPage'] as num?)?.toInt() ?? AppConstants.defaultPageSize,
|
||||||
|
search: json['search'] as String?,
|
||||||
|
sortBy: json['sortBy'] as String?,
|
||||||
|
sortOrder: json['sortOrder'] as String? ?? 'asc',
|
||||||
|
filters: json['filters'] as Map<String, dynamic>?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$PaginationParamsImplToJson(
|
||||||
|
_$PaginationParamsImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'page': instance.page,
|
||||||
|
'perPage': instance.perPage,
|
||||||
|
'search': instance.search,
|
||||||
|
'sortBy': instance.sortBy,
|
||||||
|
'sortOrder': instance.sortOrder,
|
||||||
|
'filters': instance.filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$PaginationMetaImpl _$$PaginationMetaImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PaginationMetaImpl(
|
||||||
|
currentPage: (json['currentPage'] as num).toInt(),
|
||||||
|
perPage: (json['perPage'] as num).toInt(),
|
||||||
|
total: (json['total'] as num).toInt(),
|
||||||
|
totalPages: (json['totalPages'] as num).toInt(),
|
||||||
|
hasNext: json['hasNext'] as bool,
|
||||||
|
hasPrevious: json['hasPrevious'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$PaginationMetaImplToJson(
|
||||||
|
_$PaginationMetaImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'currentPage': instance.currentPage,
|
||||||
|
'perPage': instance.perPage,
|
||||||
|
'total': instance.total,
|
||||||
|
'totalPages': instance.totalPages,
|
||||||
|
'hasNext': instance.hasNext,
|
||||||
|
'hasPrevious': instance.hasPrevious,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$PagedResultImpl<T> _$$PagedResultImplFromJson<T>(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
T Function(Object? json) fromJsonT,
|
||||||
|
) =>
|
||||||
|
_$PagedResultImpl<T>(
|
||||||
|
items: (json['items'] as List<dynamic>).map(fromJsonT).toList(),
|
||||||
|
meta: PaginationMeta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$PagedResultImplToJson<T>(
|
||||||
|
_$PagedResultImpl<T> instance,
|
||||||
|
Object? Function(T value) toJsonT,
|
||||||
|
) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'items': instance.items.map(toJsonT).toList(),
|
||||||
|
'meta': instance.meta,
|
||||||
|
};
|
||||||
24
lib/data/repositories/license_repository.dart
Normal file
24
lib/data/repositories/license_repository.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import '../models/license/license_dto.dart';
|
||||||
|
|
||||||
|
/// 라이선스 Repository 인터페이스
|
||||||
|
abstract class LicenseRepository {
|
||||||
|
/// 라이선스 목록 조회
|
||||||
|
Future<LicenseListResponseDto> getLicenses({
|
||||||
|
int page = 1,
|
||||||
|
int perPage = 20,
|
||||||
|
String? search,
|
||||||
|
Map<String, dynamic>? filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 라이선스 상세 조회
|
||||||
|
Future<LicenseDto> getLicenseDetail(int id);
|
||||||
|
|
||||||
|
/// 라이선스 생성
|
||||||
|
Future<LicenseDto> createLicense(Map<String, dynamic> data);
|
||||||
|
|
||||||
|
/// 라이선스 수정
|
||||||
|
Future<LicenseDto> updateLicense(int id, Map<String, dynamic> data);
|
||||||
|
|
||||||
|
/// 라이선스 삭제
|
||||||
|
Future<void> deleteLicense(int id);
|
||||||
|
}
|
||||||
58
lib/data/repositories/license_repository_impl.dart
Normal file
58
lib/data/repositories/license_repository_impl.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import '../datasources/remote/license_remote_datasource.dart';
|
||||||
|
import '../models/license/license_dto.dart';
|
||||||
|
import '../models/license/license_request_dto.dart';
|
||||||
|
import 'license_repository.dart';
|
||||||
|
|
||||||
|
/// 라이선스 Repository 구현체
|
||||||
|
@Injectable(as: LicenseRepository)
|
||||||
|
class LicenseRepositoryImpl implements LicenseRepository {
|
||||||
|
final LicenseRemoteDataSource remoteDataSource;
|
||||||
|
|
||||||
|
LicenseRepositoryImpl(this.remoteDataSource);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LicenseListResponseDto> getLicenses({
|
||||||
|
int page = 1,
|
||||||
|
int perPage = 20,
|
||||||
|
String? search,
|
||||||
|
Map<String, dynamic>? filters,
|
||||||
|
}) async {
|
||||||
|
// 검색 및 필터 파라미터를 DataSource 형식에 맞게 변환
|
||||||
|
bool? isActive = filters?['is_active'];
|
||||||
|
int? companyId = filters?['company_id'];
|
||||||
|
int? assignedUserId = filters?['assigned_user_id'];
|
||||||
|
String? licenseType = filters?['license_type'];
|
||||||
|
|
||||||
|
return await remoteDataSource.getLicenses(
|
||||||
|
page: page,
|
||||||
|
perPage: perPage,
|
||||||
|
isActive: isActive,
|
||||||
|
companyId: companyId,
|
||||||
|
assignedUserId: assignedUserId,
|
||||||
|
licenseType: licenseType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LicenseDto> getLicenseDetail(int id) async {
|
||||||
|
return await remoteDataSource.getLicenseById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LicenseDto> createLicense(Map<String, dynamic> data) async {
|
||||||
|
final request = CreateLicenseRequest.fromJson(data);
|
||||||
|
return await remoteDataSource.createLicense(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LicenseDto> updateLicense(int id, Map<String, dynamic> data) async {
|
||||||
|
final request = UpdateLicenseRequest.fromJson(data);
|
||||||
|
return await remoteDataSource.updateLicense(id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteLicense(int id) async {
|
||||||
|
await remoteDataSource.deleteLicense(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/data/repositories/warehouse_location_repository.dart
Normal file
27
lib/data/repositories/warehouse_location_repository.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import '../models/warehouse/warehouse_dto.dart';
|
||||||
|
|
||||||
|
/// 창고 위치 Repository 인터페이스
|
||||||
|
abstract class WarehouseLocationRepository {
|
||||||
|
/// 창고 위치 목록 조회
|
||||||
|
Future<WarehouseLocationListDto> getWarehouseLocations({
|
||||||
|
int page = 1,
|
||||||
|
int perPage = 20,
|
||||||
|
String? search,
|
||||||
|
Map<String, dynamic>? filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 창고 위치 상세 조회
|
||||||
|
Future<WarehouseLocationDto> getWarehouseLocationDetail(int id);
|
||||||
|
|
||||||
|
/// 창고 위치 생성
|
||||||
|
Future<WarehouseLocationDto> createWarehouseLocation(Map<String, dynamic> data);
|
||||||
|
|
||||||
|
/// 창고 위치 수정
|
||||||
|
Future<WarehouseLocationDto> updateWarehouseLocation(int id, Map<String, dynamic> data);
|
||||||
|
|
||||||
|
/// 창고 위치 삭제
|
||||||
|
Future<void> deleteWarehouseLocation(int id);
|
||||||
|
|
||||||
|
/// 창고에 장비가 있는지 확인
|
||||||
|
Future<bool> checkWarehouseHasEquipment(int id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import '../datasources/remote/warehouse_location_remote_datasource.dart';
|
||||||
|
import '../models/warehouse/warehouse_dto.dart';
|
||||||
|
import 'warehouse_location_repository.dart';
|
||||||
|
|
||||||
|
/// 창고 위치 Repository 구현체
|
||||||
|
@Injectable(as: WarehouseLocationRepository)
|
||||||
|
class WarehouseLocationRepositoryImpl implements WarehouseLocationRepository {
|
||||||
|
final WarehouseLocationRemoteDataSource remoteDataSource;
|
||||||
|
|
||||||
|
WarehouseLocationRepositoryImpl(this.remoteDataSource);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseLocationListDto> getWarehouseLocations({
|
||||||
|
int page = 1,
|
||||||
|
int perPage = 20,
|
||||||
|
String? search,
|
||||||
|
Map<String, dynamic>? filters,
|
||||||
|
}) async {
|
||||||
|
return await remoteDataSource.getWarehouseLocations(
|
||||||
|
page: page,
|
||||||
|
perPage: perPage,
|
||||||
|
search: search,
|
||||||
|
filters: filters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseLocationDto> getWarehouseLocationDetail(int id) async {
|
||||||
|
return await remoteDataSource.getWarehouseLocationDetail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseLocationDto> createWarehouseLocation(Map<String, dynamic> data) async {
|
||||||
|
final request = CreateWarehouseLocationRequest.fromJson(data);
|
||||||
|
return await remoteDataSource.createWarehouseLocation(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<WarehouseLocationDto> updateWarehouseLocation(int id, Map<String, dynamic> data) async {
|
||||||
|
final request = UpdateWarehouseLocationRequest.fromJson(data);
|
||||||
|
return await remoteDataSource.updateWarehouseLocation(id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteWarehouseLocation(int id) async {
|
||||||
|
await remoteDataSource.deleteWarehouseLocation(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> checkWarehouseHasEquipment(int id) async {
|
||||||
|
// TODO: API 엔드포인트 구현 필요
|
||||||
|
// 현재는 항상 false 반환
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
lib/domain/usecases/auth/auth_usecases.dart
Normal file
6
lib/domain/usecases/auth/auth_usecases.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/// Auth 도메인 UseCase 모음
|
||||||
|
export 'login_usecase.dart';
|
||||||
|
export 'logout_usecase.dart';
|
||||||
|
export 'refresh_token_usecase.dart';
|
||||||
|
export 'get_current_user_usecase.dart';
|
||||||
|
export 'check_auth_status_usecase.dart';
|
||||||
24
lib/domain/usecases/auth/check_auth_status_usecase.dart
Normal file
24
lib/domain/usecases/auth/check_auth_status_usecase.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/auth_service.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 인증 상태 확인 UseCase
|
||||||
|
/// 현재 사용자가 로그인되어 있는지 확인
|
||||||
|
class CheckAuthStatusUseCase extends UseCase<bool, NoParams> {
|
||||||
|
final AuthService _authService;
|
||||||
|
|
||||||
|
CheckAuthStatusUseCase(this._authService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, bool>> call(NoParams params) async {
|
||||||
|
try {
|
||||||
|
final isAuthenticated = await _authService.isLoggedIn();
|
||||||
|
return Right(isAuthenticated);
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '인증 상태 확인 중 오류가 발생했습니다.',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/domain/usecases/auth/get_current_user_usecase.dart
Normal file
34
lib/domain/usecases/auth/get_current_user_usecase.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/auth_service.dart';
|
||||||
|
import '../../../data/models/user/user_dto.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 현재 로그인한 사용자 정보 조회 UseCase
|
||||||
|
class GetCurrentUserUseCase extends UseCase<UserDto?, NoParams> {
|
||||||
|
final AuthService _authService;
|
||||||
|
|
||||||
|
GetCurrentUserUseCase(this._authService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, UserDto?>> call(NoParams params) async {
|
||||||
|
try {
|
||||||
|
final user = await _authService.getCurrentUser();
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return Left(AuthFailure(
|
||||||
|
message: '로그인이 필요합니다.',
|
||||||
|
code: 'NOT_AUTHENTICATED',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthUser를 UserDto로 변환 (임시로 null 반환)
|
||||||
|
return const Right(null);
|
||||||
|
} catch (e) {
|
||||||
|
return Left(UnknownFailure(
|
||||||
|
message: '사용자 정보를 가져오는 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/domain/usecases/auth/login_usecase.dart
Normal file
94
lib/domain/usecases/auth/login_usecase.dart
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../../services/auth_service.dart';
|
||||||
|
import '../../../data/models/auth/login_request.dart';
|
||||||
|
import '../../../data/models/auth/login_response.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 로그인 UseCase 파라미터
|
||||||
|
class LoginParams {
|
||||||
|
final String email;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
const LoginParams({
|
||||||
|
required this.email,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 로그인 UseCase
|
||||||
|
/// 사용자 인증을 처리하고 토큰을 저장
|
||||||
|
class LoginUseCase extends UseCase<LoginResponse, LoginParams> {
|
||||||
|
final AuthService _authService;
|
||||||
|
|
||||||
|
LoginUseCase(this._authService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, LoginResponse>> call(LoginParams params) async {
|
||||||
|
try {
|
||||||
|
// 이메일 유효성 검증
|
||||||
|
if (!_isValidEmail(params.email)) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '올바른 이메일 형식이 아닙니다.',
|
||||||
|
errors: {'email': '올바른 이메일 형식이 아닙니다.'},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비밀번호 유효성 검증
|
||||||
|
if (params.password.isEmpty) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '비밀번호를 입력해주세요.',
|
||||||
|
errors: {'password': '비밀번호를 입력해주세요.'},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인 요청
|
||||||
|
final loginRequest = LoginRequest(
|
||||||
|
email: params.email,
|
||||||
|
password: params.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await _authService.login(loginRequest);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 401) {
|
||||||
|
return Left(AuthFailure(
|
||||||
|
message: '이메일 또는 비밀번호가 올바르지 않습니다.',
|
||||||
|
code: 'INVALID_CREDENTIALS',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.receiveTimeout) {
|
||||||
|
return Left(NetworkFailure(
|
||||||
|
message: '네트워크 연결 시간이 초과되었습니다.',
|
||||||
|
code: 'TIMEOUT',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.type == DioExceptionType.connectionError) {
|
||||||
|
return Left(NetworkFailure(
|
||||||
|
message: '서버에 연결할 수 없습니다.',
|
||||||
|
code: 'CONNECTION_ERROR',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: e.response?.data['message'] ?? '서버 오류가 발생했습니다.',
|
||||||
|
code: e.response?.statusCode?.toString(),
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return Left(UnknownFailure(
|
||||||
|
message: '알 수 없는 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isValidEmail(String email) {
|
||||||
|
final emailRegex = RegExp(
|
||||||
|
r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+',
|
||||||
|
);
|
||||||
|
return emailRegex.hasMatch(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lib/domain/usecases/auth/logout_usecase.dart
Normal file
17
lib/domain/usecases/auth/logout_usecase.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/auth_service.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 로그아웃 UseCase
|
||||||
|
/// 사용자 로그아웃 처리 및 토큰 삭제
|
||||||
|
class LogoutUseCase extends UseCase<void, NoParams> {
|
||||||
|
final AuthService _authService;
|
||||||
|
|
||||||
|
LogoutUseCase(this._authService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> call(NoParams params) async {
|
||||||
|
return await _authService.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/domain/usecases/auth/refresh_token_usecase.dart
Normal file
56
lib/domain/usecases/auth/refresh_token_usecase.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../../services/auth_service.dart';
|
||||||
|
import '../../../data/models/auth/token_response.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 토큰 갱신 UseCase
|
||||||
|
/// JWT 토큰을 갱신하여 세션 유지
|
||||||
|
class RefreshTokenUseCase extends UseCase<TokenResponse, NoParams> {
|
||||||
|
final AuthService _authService;
|
||||||
|
|
||||||
|
RefreshTokenUseCase(this._authService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, TokenResponse>> call(NoParams params) async {
|
||||||
|
try {
|
||||||
|
final refreshToken = await _authService.getRefreshToken();
|
||||||
|
|
||||||
|
if (refreshToken == null) {
|
||||||
|
return Left(AuthFailure(
|
||||||
|
message: '갱신 토큰이 없습니다. 다시 로그인해주세요.',
|
||||||
|
code: 'NO_REFRESH_TOKEN',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _authService.refreshToken();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 401) {
|
||||||
|
return Left(AuthFailure(
|
||||||
|
message: '세션이 만료되었습니다. 다시 로그인해주세요.',
|
||||||
|
code: 'SESSION_EXPIRED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.receiveTimeout) {
|
||||||
|
return Left(NetworkFailure(
|
||||||
|
message: '네트워크 연결 시간이 초과되었습니다.',
|
||||||
|
code: 'TIMEOUT',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '서버 오류가 발생했습니다.',
|
||||||
|
code: e.response?.statusCode?.toString(),
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return Left(UnknownFailure(
|
||||||
|
message: '토큰 갱신 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/domain/usecases/base_usecase.dart
Normal file
18
lib/domain/usecases/base_usecase.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../core/errors/failures.dart';
|
||||||
|
|
||||||
|
/// UseCase 추상 클래스
|
||||||
|
/// 모든 UseCase는 이 클래스를 상속받아 구현
|
||||||
|
///
|
||||||
|
/// [Type]: 반환 타입
|
||||||
|
/// [Params]: 파라미터 타입
|
||||||
|
abstract class UseCase<Type, Params> {
|
||||||
|
/// UseCase 실행 메서드
|
||||||
|
Future<Either<Failure, Type>> call(Params params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 파라미터가 없는 UseCase를 위한 클래스
|
||||||
|
class NoParams {
|
||||||
|
const NoParams();
|
||||||
|
}
|
||||||
|
|
||||||
7
lib/domain/usecases/company/company_usecases.dart
Normal file
7
lib/domain/usecases/company/company_usecases.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/// Company 도메인 UseCase 모음
|
||||||
|
export 'get_companies_usecase.dart';
|
||||||
|
export 'create_company_usecase.dart';
|
||||||
|
export 'update_company_usecase.dart';
|
||||||
|
export 'delete_company_usecase.dart';
|
||||||
|
export 'get_company_detail_usecase.dart';
|
||||||
|
export 'toggle_company_status_usecase.dart';
|
||||||
84
lib/domain/usecases/company/create_company_usecase.dart
Normal file
84
lib/domain/usecases/company/create_company_usecase.dart
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/company_service.dart';
|
||||||
|
import '../../../models/company_model.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 회사 생성 파라미터
|
||||||
|
class CreateCompanyParams {
|
||||||
|
final Company company;
|
||||||
|
|
||||||
|
const CreateCompanyParams({
|
||||||
|
required this.company,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 생성 UseCase
|
||||||
|
class CreateCompanyUseCase extends UseCase<Company, CreateCompanyParams> {
|
||||||
|
final CompanyService _companyService;
|
||||||
|
|
||||||
|
CreateCompanyUseCase(this._companyService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, Company>> call(CreateCompanyParams params) async {
|
||||||
|
try {
|
||||||
|
// 유효성 검증
|
||||||
|
final validationResult = _validateCompany(params.company);
|
||||||
|
if (validationResult != null) {
|
||||||
|
return Left(validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final company = await _companyService.createCompany(params.company);
|
||||||
|
return Right(company);
|
||||||
|
} on ServerFailure catch (e) {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: e.message,
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(UnknownFailure(
|
||||||
|
message: '회사 생성 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationFailure? _validateCompany(Company company) {
|
||||||
|
final errors = <String, String>{};
|
||||||
|
|
||||||
|
if (company.name.isEmpty) {
|
||||||
|
errors['name'] = '회사명을 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.address.isEmpty) {
|
||||||
|
errors['address'] = '주소를 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.companyTypes.isEmpty) {
|
||||||
|
errors['companyTypes'] = '회사 유형을 선택해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.contactEmail != null && company.contactEmail!.isNotEmpty) {
|
||||||
|
final emailRegex = RegExp(r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+');
|
||||||
|
if (!emailRegex.hasMatch(company.contactEmail!)) {
|
||||||
|
errors['contactEmail'] = '올바른 이메일 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.contactPhone != null && company.contactPhone!.isNotEmpty) {
|
||||||
|
final phoneRegex = RegExp(r'^01[0-9]{1}-?[0-9]{4}-?[0-9]{4}$');
|
||||||
|
if (!phoneRegex.hasMatch(company.contactPhone!)) {
|
||||||
|
errors['contactPhone'] = '올바른 전화번호 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
return ValidationFailure(
|
||||||
|
message: '입력값을 확인해주세요.',
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
lib/domain/usecases/company/delete_company_usecase.dart
Normal file
44
lib/domain/usecases/company/delete_company_usecase.dart
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/company_service.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 회사 삭제 파라미터
|
||||||
|
class DeleteCompanyParams {
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const DeleteCompanyParams({
|
||||||
|
required this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 삭제 UseCase
|
||||||
|
class DeleteCompanyUseCase extends UseCase<void, DeleteCompanyParams> {
|
||||||
|
final CompanyService _companyService;
|
||||||
|
|
||||||
|
DeleteCompanyUseCase(this._companyService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> call(DeleteCompanyParams params) async {
|
||||||
|
try {
|
||||||
|
await _companyService.deleteCompany(params.id);
|
||||||
|
return const Right(null);
|
||||||
|
} on ServerFailure catch (e) {
|
||||||
|
if (e.message.contains('associated')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '연관된 데이터가 있어 삭제할 수 없습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: e.message,
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(UnknownFailure(
|
||||||
|
message: '회사 삭제 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
lib/domain/usecases/company/get_companies_usecase.dart
Normal file
51
lib/domain/usecases/company/get_companies_usecase.dart
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/company_service.dart';
|
||||||
|
import '../../../models/company_model.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 회사 목록 조회 파라미터
|
||||||
|
class GetCompaniesParams {
|
||||||
|
final int page;
|
||||||
|
final int perPage;
|
||||||
|
final String? search;
|
||||||
|
final bool? isActive;
|
||||||
|
|
||||||
|
const GetCompaniesParams({
|
||||||
|
this.page = 1,
|
||||||
|
this.perPage = 20,
|
||||||
|
this.search,
|
||||||
|
this.isActive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 목록 조회 UseCase
|
||||||
|
class GetCompaniesUseCase extends UseCase<List<Company>, GetCompaniesParams> {
|
||||||
|
final CompanyService _companyService;
|
||||||
|
|
||||||
|
GetCompaniesUseCase(this._companyService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<Company>>> call(GetCompaniesParams params) async {
|
||||||
|
try {
|
||||||
|
final companies = await _companyService.getCompanies(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
search: params.search,
|
||||||
|
isActive: params.isActive,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(companies);
|
||||||
|
} on ServerFailure catch (e) {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: e.message,
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(UnknownFailure(
|
||||||
|
message: '회사 목록을 불러오는 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/domain/usecases/company/get_company_detail_usecase.dart
Normal file
55
lib/domain/usecases/company/get_company_detail_usecase.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/company_service.dart';
|
||||||
|
import '../../../models/company_model.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 회사 상세 조회 파라미터
|
||||||
|
class GetCompanyDetailParams {
|
||||||
|
final int id;
|
||||||
|
final bool includeBranches;
|
||||||
|
|
||||||
|
const GetCompanyDetailParams({
|
||||||
|
required this.id,
|
||||||
|
this.includeBranches = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 상세 조회 UseCase
|
||||||
|
class GetCompanyDetailUseCase extends UseCase<Company, GetCompanyDetailParams> {
|
||||||
|
final CompanyService _companyService;
|
||||||
|
|
||||||
|
GetCompanyDetailUseCase(this._companyService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, Company>> call(GetCompanyDetailParams params) async {
|
||||||
|
try {
|
||||||
|
final Company company;
|
||||||
|
|
||||||
|
if (params.includeBranches) {
|
||||||
|
company = await _companyService.getCompanyWithBranches(params.id);
|
||||||
|
} else {
|
||||||
|
company = await _companyService.getCompanyDetail(params.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Right(company);
|
||||||
|
} on ServerFailure catch (e) {
|
||||||
|
if (e.message.contains('not found')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '회사를 찾을 수 없습니다.',
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: e.message,
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(UnknownFailure(
|
||||||
|
message: '회사 정보를 불러오는 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/company_service.dart';
|
||||||
|
import '../../../models/company_model.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 회사 상태 토글 파라미터
|
||||||
|
class ToggleCompanyStatusParams {
|
||||||
|
final int id;
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
|
const ToggleCompanyStatusParams({
|
||||||
|
required this.id,
|
||||||
|
required this.isActive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 활성화/비활성화 UseCase
|
||||||
|
class ToggleCompanyStatusUseCase extends UseCase<void, ToggleCompanyStatusParams> {
|
||||||
|
final CompanyService _companyService;
|
||||||
|
|
||||||
|
ToggleCompanyStatusUseCase(this._companyService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> call(ToggleCompanyStatusParams params) async {
|
||||||
|
try {
|
||||||
|
await _companyService.updateCompanyStatus(params.id, params.isActive);
|
||||||
|
return const Right(null);
|
||||||
|
} on ServerFailure catch (e) {
|
||||||
|
if (e.message.contains('equipment')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '활성 장비가 있는 회사는 비활성화할 수 없습니다.',
|
||||||
|
code: 'HAS_ACTIVE_EQUIPMENT',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: e.message,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '회사 상태 변경 중 오류가 발생했습니다.',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
lib/domain/usecases/company/update_company_usecase.dart
Normal file
86
lib/domain/usecases/company/update_company_usecase.dart
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/company_service.dart';
|
||||||
|
import '../../../models/company_model.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 회사 수정 파라미터
|
||||||
|
class UpdateCompanyParams {
|
||||||
|
final int id;
|
||||||
|
final Company company;
|
||||||
|
|
||||||
|
const UpdateCompanyParams({
|
||||||
|
required this.id,
|
||||||
|
required this.company,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 수정 UseCase
|
||||||
|
class UpdateCompanyUseCase extends UseCase<Company, UpdateCompanyParams> {
|
||||||
|
final CompanyService _companyService;
|
||||||
|
|
||||||
|
UpdateCompanyUseCase(this._companyService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, Company>> call(UpdateCompanyParams params) async {
|
||||||
|
try {
|
||||||
|
// 유효성 검증
|
||||||
|
final validationResult = _validateCompany(params.company);
|
||||||
|
if (validationResult != null) {
|
||||||
|
return Left(validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final company = await _companyService.updateCompany(params.id, params.company);
|
||||||
|
return Right(company);
|
||||||
|
} on ServerFailure catch (e) {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: e.message,
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(UnknownFailure(
|
||||||
|
message: '회사 정보 수정 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationFailure? _validateCompany(Company company) {
|
||||||
|
final errors = <String, String>{};
|
||||||
|
|
||||||
|
if (company.name.isEmpty) {
|
||||||
|
errors['name'] = '회사명을 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.address.isEmpty) {
|
||||||
|
errors['address'] = '주소를 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.companyTypes.isEmpty) {
|
||||||
|
errors['companyTypes'] = '회사 유형을 선택해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.contactEmail != null && company.contactEmail!.isNotEmpty) {
|
||||||
|
final emailRegex = RegExp(r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+');
|
||||||
|
if (!emailRegex.hasMatch(company.contactEmail!)) {
|
||||||
|
errors['contactEmail'] = '올바른 이메일 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.contactPhone != null && company.contactPhone!.isNotEmpty) {
|
||||||
|
final phoneRegex = RegExp(r'^01[0-9]{1}-?[0-9]{4}-?[0-9]{4}$');
|
||||||
|
if (!phoneRegex.hasMatch(company.contactPhone!)) {
|
||||||
|
errors['contactPhone'] = '올바른 전화번호 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
return ValidationFailure(
|
||||||
|
message: '입력값을 확인해주세요.',
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
lib/domain/usecases/equipment/equipment_in_usecase.dart
Normal file
123
lib/domain/usecases/equipment/equipment_in_usecase.dart
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/equipment_service.dart';
|
||||||
|
import '../../../data/models/equipment/equipment_in_request.dart';
|
||||||
|
import '../../../data/models/equipment/equipment_io_response.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 장비 입고 파라미터
|
||||||
|
class EquipmentInParams {
|
||||||
|
final int equipmentId;
|
||||||
|
final int warehouseLocationId;
|
||||||
|
final int quantity;
|
||||||
|
final String serialNumber;
|
||||||
|
final String? remark;
|
||||||
|
final DateTime? purchaseDate;
|
||||||
|
final double? purchasePrice;
|
||||||
|
|
||||||
|
const EquipmentInParams({
|
||||||
|
required this.equipmentId,
|
||||||
|
required this.warehouseLocationId,
|
||||||
|
required this.quantity,
|
||||||
|
required this.serialNumber,
|
||||||
|
this.remark,
|
||||||
|
this.purchaseDate,
|
||||||
|
this.purchasePrice,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 입고 UseCase
|
||||||
|
/// 새로운 장비를 창고에 입고 처리
|
||||||
|
class EquipmentInUseCase extends UseCase<EquipmentIoResponse, EquipmentInParams> {
|
||||||
|
final EquipmentService _equipmentService;
|
||||||
|
|
||||||
|
EquipmentInUseCase(this._equipmentService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, EquipmentIoResponse>> call(EquipmentInParams params) async {
|
||||||
|
try {
|
||||||
|
// 유효성 검증
|
||||||
|
final validationResult = _validateInput(params);
|
||||||
|
if (validationResult != null) {
|
||||||
|
return Left(validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시리얼 번호 중복 체크 (프론트엔드 임시 로직)
|
||||||
|
// TODO: 백엔드 API 구현 후 제거
|
||||||
|
|
||||||
|
final response = await _equipmentService.equipmentIn(
|
||||||
|
equipmentId: params.equipmentId,
|
||||||
|
quantity: params.quantity,
|
||||||
|
warehouseLocationId: params.warehouseLocationId,
|
||||||
|
notes: params.remark,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(response);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('시리얼')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '이미 등록된 시리얼 번호입니다.',
|
||||||
|
code: 'DUPLICATE_SERIAL',
|
||||||
|
errors: {'serialNumber': '중복된 시리얼 번호입니다.'},
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('재고')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '재고 수량이 부족합니다.',
|
||||||
|
code: 'INSUFFICIENT_STOCK',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '장비 입고 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '장비 입고 처리 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationFailure? _validateInput(EquipmentInParams params) {
|
||||||
|
final errors = <String, String>{};
|
||||||
|
|
||||||
|
// 수량 검증
|
||||||
|
if (params.quantity <= 0) {
|
||||||
|
errors['quantity'] = '수량은 1개 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
if (params.quantity > 999) {
|
||||||
|
errors['quantity'] = '한 번에 입고 가능한 최대 수량은 999개입니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시리얼 번호 검증
|
||||||
|
if (params.serialNumber.isEmpty) {
|
||||||
|
errors['serialNumber'] = '시리얼 번호를 입력해주세요.';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^[A-Za-z0-9-]+$').hasMatch(params.serialNumber)) {
|
||||||
|
errors['serialNumber'] = '시리얼 번호는 영문, 숫자, 하이픈만 사용 가능합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 구매 가격 검증 (선택사항)
|
||||||
|
if (params.purchasePrice != null && params.purchasePrice! < 0) {
|
||||||
|
errors['purchasePrice'] = '구매 가격은 0 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 구매 날짜 검증 (선택사항)
|
||||||
|
if (params.purchaseDate != null && params.purchaseDate!.isAfter(DateTime.now())) {
|
||||||
|
errors['purchaseDate'] = '구매 날짜는 미래 날짜일 수 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
return ValidationFailure(
|
||||||
|
message: '입력값을 확인해주세요.',
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
lib/domain/usecases/equipment/equipment_out_usecase.dart
Normal file
117
lib/domain/usecases/equipment/equipment_out_usecase.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/equipment_service.dart';
|
||||||
|
import '../../../data/models/equipment/equipment_out_request.dart';
|
||||||
|
import '../../../data/models/equipment/equipment_io_response.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 장비 출고 파라미터
|
||||||
|
class EquipmentOutParams {
|
||||||
|
final int equipmentInId;
|
||||||
|
final int companyId;
|
||||||
|
final int quantity;
|
||||||
|
final String? remark;
|
||||||
|
final String? recipientName;
|
||||||
|
final String? recipientPhone;
|
||||||
|
final DateTime? deliveryDate;
|
||||||
|
|
||||||
|
const EquipmentOutParams({
|
||||||
|
required this.equipmentInId,
|
||||||
|
required this.companyId,
|
||||||
|
required this.quantity,
|
||||||
|
this.remark,
|
||||||
|
this.recipientName,
|
||||||
|
this.recipientPhone,
|
||||||
|
this.deliveryDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 출고 UseCase
|
||||||
|
/// 창고에서 회사로 장비 출고 처리
|
||||||
|
class EquipmentOutUseCase extends UseCase<EquipmentIoResponse, EquipmentOutParams> {
|
||||||
|
final EquipmentService _equipmentService;
|
||||||
|
|
||||||
|
EquipmentOutUseCase(this._equipmentService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, EquipmentIoResponse>> call(EquipmentOutParams params) async {
|
||||||
|
try {
|
||||||
|
// 유효성 검증
|
||||||
|
final validationResult = _validateInput(params);
|
||||||
|
if (validationResult != null) {
|
||||||
|
return Left(validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _equipmentService.equipmentOut(
|
||||||
|
equipmentId: params.equipmentInId, // equipmentInId를 equipmentId로 사용
|
||||||
|
quantity: params.quantity,
|
||||||
|
companyId: params.companyId,
|
||||||
|
notes: params.remark,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(response);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('재고')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '출고 가능한 재고가 부족합니다.',
|
||||||
|
code: 'INSUFFICIENT_STOCK',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('찾을 수 없')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '장비 정보를 찾을 수 없습니다.',
|
||||||
|
code: 'EQUIPMENT_NOT_FOUND',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '장비 출고 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '장비 출고 처리 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationFailure? _validateInput(EquipmentOutParams params) {
|
||||||
|
final errors = <String, String>{};
|
||||||
|
|
||||||
|
// 수량 검증
|
||||||
|
if (params.quantity <= 0) {
|
||||||
|
errors['quantity'] = '출고 수량은 1개 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
if (params.quantity > 999) {
|
||||||
|
errors['quantity'] = '한 번에 출고 가능한 최대 수량은 999개입니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 수령자 정보 검증 (선택사항이지만 제공된 경우)
|
||||||
|
if (params.recipientName != null && params.recipientName!.isEmpty) {
|
||||||
|
errors['recipientName'] = '수령자 이름을 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.recipientPhone != null && params.recipientPhone!.isNotEmpty) {
|
||||||
|
if (!RegExp(r'^01[0-9]{1}-?[0-9]{4}-?[0-9]{4}$').hasMatch(params.recipientPhone!)) {
|
||||||
|
errors['recipientPhone'] = '올바른 전화번호 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 배송 날짜 검증 (선택사항)
|
||||||
|
if (params.deliveryDate != null && params.deliveryDate!.isBefore(DateTime.now().subtract(Duration(days: 1)))) {
|
||||||
|
errors['deliveryDate'] = '배송 날짜는 과거 날짜일 수 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
return ValidationFailure(
|
||||||
|
message: '입력값을 확인해주세요.',
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
lib/domain/usecases/equipment/equipment_usecases.dart
Normal file
5
lib/domain/usecases/equipment/equipment_usecases.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// Equipment 도메인 UseCase 모음
|
||||||
|
export 'get_equipments_usecase.dart';
|
||||||
|
export 'equipment_in_usecase.dart';
|
||||||
|
export 'equipment_out_usecase.dart';
|
||||||
|
export 'get_equipment_history_usecase.dart';
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/equipment_service.dart';
|
||||||
|
import '../../../data/models/equipment/equipment_history_dto.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 장비 이력 조회 파라미터
|
||||||
|
class GetEquipmentHistoryParams {
|
||||||
|
final int equipmentId;
|
||||||
|
final DateTime? startDate;
|
||||||
|
final DateTime? endDate;
|
||||||
|
final String? historyType; // 'in', 'out', 'maintenance', 'disposal'
|
||||||
|
|
||||||
|
const GetEquipmentHistoryParams({
|
||||||
|
required this.equipmentId,
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
this.historyType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 이력 조회 UseCase
|
||||||
|
/// 특정 장비의 입출고 및 상태 변경 이력 조회
|
||||||
|
class GetEquipmentHistoryUseCase extends UseCase<List<EquipmentHistoryDto>, GetEquipmentHistoryParams> {
|
||||||
|
final EquipmentService _equipmentService;
|
||||||
|
|
||||||
|
GetEquipmentHistoryUseCase(this._equipmentService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<EquipmentHistoryDto>>> call(GetEquipmentHistoryParams params) async {
|
||||||
|
try {
|
||||||
|
// 날짜 유효성 검증
|
||||||
|
if (params.startDate != null && params.endDate != null) {
|
||||||
|
if (params.startDate!.isAfter(params.endDate!)) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '시작일이 종료일보다 늦을 수 없습니다.',
|
||||||
|
errors: {'date': '날짜 범위를 확인해주세요.'},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이력 타입 유효성 검증
|
||||||
|
if (params.historyType != null &&
|
||||||
|
!['in', 'out', 'maintenance', 'disposal'].contains(params.historyType)) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '올바르지 않은 이력 타입입니다.',
|
||||||
|
errors: {'historyType': '유효한 이력 타입을 선택해주세요.'},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
final history = await _equipmentService.getEquipmentHistory(params.equipmentId);
|
||||||
|
|
||||||
|
// 필터링 적용
|
||||||
|
List<EquipmentHistoryDto> filteredHistory = history;
|
||||||
|
|
||||||
|
if (params.historyType != null) {
|
||||||
|
filteredHistory = filteredHistory
|
||||||
|
.where((h) => h.transactionType == params.historyType)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.startDate != null) {
|
||||||
|
filteredHistory = filteredHistory
|
||||||
|
.where((h) => h.createdAt.isAfter(params.startDate!))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.endDate != null) {
|
||||||
|
filteredHistory = filteredHistory
|
||||||
|
.where((h) => h.createdAt.isBefore(params.endDate!))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Right(filteredHistory);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('찾을 수 없')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '장비를 찾을 수 없습니다.',
|
||||||
|
code: 'EQUIPMENT_NOT_FOUND',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '장비 이력을 조회하는 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/domain/usecases/equipment/get_equipments_usecase.dart
Normal file
69
lib/domain/usecases/equipment/get_equipments_usecase.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/equipment_service.dart';
|
||||||
|
import '../../../models/equipment_unified_model.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 장비 목록 조회 파라미터
|
||||||
|
class GetEquipmentsParams {
|
||||||
|
final int page;
|
||||||
|
final int perPage;
|
||||||
|
final String? status;
|
||||||
|
final int? companyId;
|
||||||
|
final int? warehouseLocationId;
|
||||||
|
final String? search;
|
||||||
|
|
||||||
|
const GetEquipmentsParams({
|
||||||
|
this.page = 1,
|
||||||
|
this.perPage = 20,
|
||||||
|
this.status,
|
||||||
|
this.companyId,
|
||||||
|
this.warehouseLocationId,
|
||||||
|
this.search,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 목록 조회 UseCase
|
||||||
|
/// 필터링 및 페이지네이션 지원
|
||||||
|
class GetEquipmentsUseCase extends UseCase<List<Equipment>, GetEquipmentsParams> {
|
||||||
|
final EquipmentService _equipmentService;
|
||||||
|
|
||||||
|
GetEquipmentsUseCase(this._equipmentService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<Equipment>>> call(GetEquipmentsParams params) async {
|
||||||
|
try {
|
||||||
|
// 상태 유효성 검증
|
||||||
|
if (params.status != null &&
|
||||||
|
!['available', 'in_use', 'maintenance', 'disposed', 'rented'].contains(params.status)) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '올바르지 않은 장비 상태입니다.',
|
||||||
|
errors: {'status': '유효한 상태를 선택해주세요.'},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
final equipments = await _equipmentService.getEquipments(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
status: params.status,
|
||||||
|
companyId: params.companyId,
|
||||||
|
warehouseLocationId: params.warehouseLocationId,
|
||||||
|
search: params.search,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(equipments);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('네트워크')) {
|
||||||
|
return Left(NetworkFailure(
|
||||||
|
message: '네트워크 연결을 확인해주세요.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '장비 목록을 불러오는 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
68
lib/domain/usecases/license/create_license_usecase.dart
Normal file
68
lib/domain/usecases/license/create_license_usecase.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/domain/usecases/license/delete_license_usecase.dart
Normal file
29
lib/domain/usecases/license/delete_license_usecase.dart
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/domain/usecases/license/get_license_detail_usecase.dart
Normal file
24
lib/domain/usecases/license/get_license_detail_usecase.dart
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/domain/usecases/license/get_licenses_usecase.dart
Normal file
55
lib/domain/usecases/license/get_licenses_usecase.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
lib/domain/usecases/license/license_usecases.dart
Normal file
7
lib/domain/usecases/license/license_usecases.dart
Normal 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';
|
||||||
75
lib/domain/usecases/license/update_license_usecase.dart
Normal file
75
lib/domain/usecases/license/update_license_usecase.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
135
lib/domain/usecases/user/create_user_usecase.dart
Normal file
135
lib/domain/usecases/user/create_user_usecase.dart
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/user_service.dart';
|
||||||
|
import '../../../models/user_model.dart' as model;
|
||||||
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 사용자 생성 파라미터
|
||||||
|
class CreateUserParams {
|
||||||
|
final String username;
|
||||||
|
final String email;
|
||||||
|
final String password;
|
||||||
|
final String name;
|
||||||
|
final String role;
|
||||||
|
final int companyId;
|
||||||
|
final String? phone;
|
||||||
|
final String? position;
|
||||||
|
|
||||||
|
const CreateUserParams({
|
||||||
|
required this.username,
|
||||||
|
required this.email,
|
||||||
|
required this.password,
|
||||||
|
required this.name,
|
||||||
|
required this.role,
|
||||||
|
required this.companyId,
|
||||||
|
this.phone,
|
||||||
|
this.position,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 생성 UseCase
|
||||||
|
/// 유효성 검증 및 중복 체크 포함
|
||||||
|
class CreateUserUseCase extends UseCase<model.User, CreateUserParams> {
|
||||||
|
final UserService _userService;
|
||||||
|
|
||||||
|
CreateUserUseCase(this._userService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, model.User>> call(CreateUserParams params) async {
|
||||||
|
try {
|
||||||
|
// 유효성 검증
|
||||||
|
final validationResult = _validateUserInput(params);
|
||||||
|
if (validationResult != null) {
|
||||||
|
return Left(validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = await _userService.createUser(
|
||||||
|
username: params.username,
|
||||||
|
email: params.email,
|
||||||
|
password: params.password,
|
||||||
|
name: params.name,
|
||||||
|
role: params.role,
|
||||||
|
companyId: params.companyId,
|
||||||
|
phone: params.phone,
|
||||||
|
position: params.position,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(user);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('이미 존재')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '이미 존재하는 사용자입니다.',
|
||||||
|
code: 'USER_EXISTS',
|
||||||
|
errors: {
|
||||||
|
'username': '이미 사용중인 사용자명입니다.',
|
||||||
|
'email': '이미 등록된 이메일입니다.',
|
||||||
|
},
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '사용자를 생성할 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '사용자 생성 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationFailure? _validateUserInput(CreateUserParams params) {
|
||||||
|
final errors = <String, String>{};
|
||||||
|
|
||||||
|
// 사용자명 검증
|
||||||
|
if (params.username.length < 3) {
|
||||||
|
errors['username'] = '사용자명은 3자 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(params.username)) {
|
||||||
|
errors['username'] = '사용자명은 영문, 숫자, 언더스코어만 사용 가능합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이메일 검증
|
||||||
|
if (!AppConstants.emailRegex.hasMatch(params.email)) {
|
||||||
|
errors['email'] = '올바른 이메일 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비밀번호 검증
|
||||||
|
if (params.password.length < 8) {
|
||||||
|
errors['password'] = '비밀번호는 8자 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]').hasMatch(params.password)) {
|
||||||
|
errors['password'] = '비밀번호는 영문, 숫자, 특수문자를 포함해야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이름 검증
|
||||||
|
if (params.name.isEmpty) {
|
||||||
|
errors['name'] = '이름을 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 역할 검증
|
||||||
|
if (!['S', 'M', 'U', 'V'].contains(params.role)) {
|
||||||
|
errors['role'] = '올바른 역할을 선택해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전화번호 검증 (선택사항)
|
||||||
|
if (params.phone != null && params.phone!.isNotEmpty) {
|
||||||
|
if (!AppConstants.phoneRegex.hasMatch(params.phone!)) {
|
||||||
|
errors['phone'] = '올바른 전화번호 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
return ValidationFailure(
|
||||||
|
message: '입력값을 확인해주세요.',
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/domain/usecases/user/delete_user_usecase.dart
Normal file
56
lib/domain/usecases/user/delete_user_usecase.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/user_service.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 사용자 삭제 파라미터
|
||||||
|
class DeleteUserParams {
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const DeleteUserParams({
|
||||||
|
required this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 삭제 UseCase
|
||||||
|
class DeleteUserUseCase extends UseCase<void, DeleteUserParams> {
|
||||||
|
final UserService _userService;
|
||||||
|
|
||||||
|
DeleteUserUseCase(this._userService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> call(DeleteUserParams params) async {
|
||||||
|
try {
|
||||||
|
// 자기 자신 삭제 방지 로직 필요
|
||||||
|
// 실제 구현에서는 현재 로그인한 사용자 ID와 비교
|
||||||
|
|
||||||
|
await _userService.deleteUser(params.id);
|
||||||
|
return const Right(null);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('찾을 수 없')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '사용자를 찾을 수 없습니다.',
|
||||||
|
code: 'USER_NOT_FOUND',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('삭제할 수 없')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '해당 사용자는 삭제할 수 없습니다.',
|
||||||
|
code: 'CANNOT_DELETE',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '사용자를 삭제할 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '사용자 삭제 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
lib/domain/usecases/user/get_user_detail_usecase.dart
Normal file
48
lib/domain/usecases/user/get_user_detail_usecase.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/user_service.dart';
|
||||||
|
import '../../../models/user_model.dart' as model;
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 사용자 상세 조회 파라미터
|
||||||
|
class GetUserDetailParams {
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const GetUserDetailParams({
|
||||||
|
required this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 상세 조회 UseCase
|
||||||
|
class GetUserDetailUseCase extends UseCase<model.User, GetUserDetailParams> {
|
||||||
|
final UserService _userService;
|
||||||
|
|
||||||
|
GetUserDetailUseCase(this._userService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, model.User>> call(GetUserDetailParams params) async {
|
||||||
|
try {
|
||||||
|
final user = await _userService.getUser(params.id);
|
||||||
|
return Right(user);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('찾을 수 없')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '사용자를 찾을 수 없습니다.',
|
||||||
|
code: 'USER_NOT_FOUND',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '사용자 정보를 조회할 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '사용자 정보를 불러오는 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
lib/domain/usecases/user/get_users_usecase.dart
Normal file
66
lib/domain/usecases/user/get_users_usecase.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/user_service.dart';
|
||||||
|
import '../../../models/user_model.dart' as model;
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 사용자 목록 조회 파라미터
|
||||||
|
class GetUsersParams {
|
||||||
|
final int page;
|
||||||
|
final int perPage;
|
||||||
|
final bool? isActive;
|
||||||
|
final int? companyId;
|
||||||
|
final String? role;
|
||||||
|
|
||||||
|
const GetUsersParams({
|
||||||
|
this.page = 1,
|
||||||
|
this.perPage = 20,
|
||||||
|
this.isActive,
|
||||||
|
this.companyId,
|
||||||
|
this.role,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 목록 조회 UseCase
|
||||||
|
/// 필터링 및 페이지네이션 지원
|
||||||
|
class GetUsersUseCase extends UseCase<List<model.User>, GetUsersParams> {
|
||||||
|
final UserService _userService;
|
||||||
|
|
||||||
|
GetUsersUseCase(this._userService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<model.User>>> call(GetUsersParams params) async {
|
||||||
|
try {
|
||||||
|
// 권한 검증 (관리자, 매니저만 사용자 목록 조회 가능)
|
||||||
|
// 실제 구현에서는 현재 사용자 권한 체크 필요
|
||||||
|
|
||||||
|
final users = await _userService.getUsers(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
isActive: params.isActive,
|
||||||
|
companyId: params.companyId,
|
||||||
|
role: params.role,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(users);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '사용자 목록을 조회할 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('네트워크')) {
|
||||||
|
return Left(NetworkFailure(
|
||||||
|
message: '네트워크 연결을 확인해주세요.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '사용자 목록을 불러오는 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/domain/usecases/user/reset_password_usecase.dart
Normal file
94
lib/domain/usecases/user/reset_password_usecase.dart
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/user_service.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 비밀번호 재설정 파라미터
|
||||||
|
class ResetPasswordParams {
|
||||||
|
final int userId;
|
||||||
|
final String newPassword;
|
||||||
|
final String? currentPassword; // 자기 자신의 비밀번호 변경 시 필요
|
||||||
|
|
||||||
|
const ResetPasswordParams({
|
||||||
|
required this.userId,
|
||||||
|
required this.newPassword,
|
||||||
|
this.currentPassword,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 비밀번호 재설정 UseCase
|
||||||
|
/// 관리자는 다른 사용자 비밀번호 재설정 가능
|
||||||
|
/// 일반 사용자는 자신의 비밀번호만 변경 가능 (현재 비밀번호 필요)
|
||||||
|
class ResetPasswordUseCase extends UseCase<bool, ResetPasswordParams> {
|
||||||
|
final UserService _userService;
|
||||||
|
|
||||||
|
ResetPasswordUseCase(this._userService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, bool>> call(ResetPasswordParams params) async {
|
||||||
|
try {
|
||||||
|
// 비밀번호 유효성 검증
|
||||||
|
final validationResult = _validatePassword(params.newPassword);
|
||||||
|
if (validationResult != null) {
|
||||||
|
return Left(validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final success = await _userService.resetPassword(
|
||||||
|
userId: params.userId,
|
||||||
|
newPassword: params.newPassword,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(success);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '비밀번호를 재설정할 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('일치하지 않')) {
|
||||||
|
return Left(AuthFailure(
|
||||||
|
message: '현재 비밀번호가 일치하지 않습니다.',
|
||||||
|
code: 'INVALID_CURRENT_PASSWORD',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('찾을 수 없')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '사용자를 찾을 수 없습니다.',
|
||||||
|
code: 'USER_NOT_FOUND',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '비밀번호 재설정 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationFailure? _validatePassword(String password) {
|
||||||
|
final errors = <String, String>{};
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
errors['password'] = '비밀번호는 8자 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RegExp(r'^(?=.*[A-Za-z])(?=.*\d)').hasMatch(password)) {
|
||||||
|
errors['password'] = '비밀번호는 영문과 숫자를 포함해야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
|
||||||
|
errors['password'] = '비밀번호는 특수문자를 포함해야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
return ValidationFailure(
|
||||||
|
message: '비밀번호가 보안 요구사항을 충족하지 않습니다.',
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/domain/usecases/user/toggle_user_status_usecase.dart
Normal file
57
lib/domain/usecases/user/toggle_user_status_usecase.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/user_service.dart';
|
||||||
|
import '../../../models/user_model.dart' as model;
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 사용자 상태 토글 파라미터
|
||||||
|
class ToggleUserStatusParams {
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
const ToggleUserStatusParams({
|
||||||
|
required this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 활성화/비활성화 UseCase
|
||||||
|
class ToggleUserStatusUseCase extends UseCase<model.User, ToggleUserStatusParams> {
|
||||||
|
final UserService _userService;
|
||||||
|
|
||||||
|
ToggleUserStatusUseCase(this._userService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, model.User>> call(ToggleUserStatusParams params) async {
|
||||||
|
try {
|
||||||
|
// 자기 자신 비활성화 방지
|
||||||
|
// 실제 구현에서는 현재 로그인한 사용자 ID와 비교
|
||||||
|
|
||||||
|
final user = await _userService.toggleUserStatus(params.id);
|
||||||
|
return Right(user);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('자신')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '자기 자신은 비활성화할 수 없습니다.',
|
||||||
|
code: 'CANNOT_DEACTIVATE_SELF',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('찾을 수 없')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '사용자를 찾을 수 없습니다.',
|
||||||
|
code: 'USER_NOT_FOUND',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '사용자 상태를 변경할 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '사용자 상태 변경 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
lib/domain/usecases/user/update_user_usecase.dart
Normal file
110
lib/domain/usecases/user/update_user_usecase.dart
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../services/user_service.dart';
|
||||||
|
import '../../../models/user_model.dart' as model;
|
||||||
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../base_usecase.dart';
|
||||||
|
|
||||||
|
/// 사용자 수정 파라미터
|
||||||
|
class UpdateUserParams {
|
||||||
|
final int id;
|
||||||
|
final String? email;
|
||||||
|
final String? name;
|
||||||
|
final String? role;
|
||||||
|
final int? companyId;
|
||||||
|
final String? phone;
|
||||||
|
final String? position;
|
||||||
|
final bool? isActive;
|
||||||
|
|
||||||
|
const UpdateUserParams({
|
||||||
|
required this.id,
|
||||||
|
this.email,
|
||||||
|
this.name,
|
||||||
|
this.role,
|
||||||
|
this.companyId,
|
||||||
|
this.phone,
|
||||||
|
this.position,
|
||||||
|
this.isActive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 정보 수정 UseCase
|
||||||
|
class UpdateUserUseCase extends UseCase<model.User, UpdateUserParams> {
|
||||||
|
final UserService _userService;
|
||||||
|
|
||||||
|
UpdateUserUseCase(this._userService);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, model.User>> call(UpdateUserParams params) async {
|
||||||
|
try {
|
||||||
|
// 유효성 검증
|
||||||
|
final validationResult = _validateUpdateInput(params);
|
||||||
|
if (validationResult != null) {
|
||||||
|
return Left(validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = await _userService.updateUser(
|
||||||
|
params.id,
|
||||||
|
email: params.email,
|
||||||
|
name: params.name,
|
||||||
|
role: params.role,
|
||||||
|
companyId: params.companyId,
|
||||||
|
phone: params.phone,
|
||||||
|
position: params.position,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(user);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('찾을 수 없')) {
|
||||||
|
return Left(ValidationFailure(
|
||||||
|
message: '사용자를 찾을 수 없습니다.',
|
||||||
|
code: 'USER_NOT_FOUND',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else if (e.toString().contains('권한')) {
|
||||||
|
return Left(PermissionFailure(
|
||||||
|
message: '사용자 정보를 수정할 권한이 없습니다.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Left(ServerFailure(
|
||||||
|
message: '사용자 정보 수정 중 오류가 발생했습니다.',
|
||||||
|
originalError: e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationFailure? _validateUpdateInput(UpdateUserParams params) {
|
||||||
|
final errors = <String, String>{};
|
||||||
|
|
||||||
|
// 이메일 검증 (제공된 경우)
|
||||||
|
if (params.email != null && params.email!.isNotEmpty) {
|
||||||
|
if (!AppConstants.emailRegex.hasMatch(params.email!)) {
|
||||||
|
errors['email'] = '올바른 이메일 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 역할 검증 (제공된 경우)
|
||||||
|
if (params.role != null && !['S', 'M', 'U', 'V'].contains(params.role)) {
|
||||||
|
errors['role'] = '올바른 역할을 선택해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전화번호 검증 (제공된 경우)
|
||||||
|
if (params.phone != null && params.phone!.isNotEmpty) {
|
||||||
|
if (!AppConstants.phoneRegex.hasMatch(params.phone!)) {
|
||||||
|
errors['phone'] = '올바른 전화번호 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
return ValidationFailure(
|
||||||
|
message: '입력값을 확인해주세요.',
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
lib/domain/usecases/user/user_usecases.dart
Normal file
8
lib/domain/usecases/user/user_usecases.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/// User 도메인 UseCase 모음
|
||||||
|
export 'get_users_usecase.dart';
|
||||||
|
export 'create_user_usecase.dart';
|
||||||
|
export 'update_user_usecase.dart';
|
||||||
|
export 'delete_user_usecase.dart';
|
||||||
|
export 'get_user_detail_usecase.dart';
|
||||||
|
export 'reset_password_usecase.dart';
|
||||||
|
export 'toggle_user_status_usecase.dart';
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -27,7 +27,6 @@ void main() async {
|
|||||||
// 에러가 발생해도 앱은 실행되도록 함
|
// 에러가 발생해도 앱은 실행되도록 함
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockDataService는 싱글톤으로 자동 초기화됨
|
|
||||||
runApp(const SuperportApp());
|
runApp(const SuperportApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||||
import 'package:superport/screens/overview/overview_screen_redesign.dart';
|
import 'package:superport/screens/overview/overview_screen_redesign.dart';
|
||||||
@@ -10,6 +11,7 @@ import 'package:superport/screens/license/license_list_redesign.dart';
|
|||||||
import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart';
|
import 'package:superport/screens/warehouse_location/warehouse_location_list_redesign.dart';
|
||||||
import 'package:superport/services/auth_service.dart';
|
import 'package:superport/services/auth_service.dart';
|
||||||
import 'package:superport/services/dashboard_service.dart';
|
import 'package:superport/services/dashboard_service.dart';
|
||||||
|
import 'package:superport/services/lookup_service.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
import 'package:superport/data/models/auth/auth_user.dart';
|
import 'package:superport/data/models/auth/auth_user.dart';
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
|||||||
AuthUser? _currentUser;
|
AuthUser? _currentUser;
|
||||||
late final AuthService _authService;
|
late final AuthService _authService;
|
||||||
late final DashboardService _dashboardService;
|
late final DashboardService _dashboardService;
|
||||||
|
late final LookupService _lookupService;
|
||||||
late Animation<double> _sidebarAnimation;
|
late Animation<double> _sidebarAnimation;
|
||||||
int _expiringLicenseCount = 0; // 30일 내 만료 예정 라이선스 수
|
int _expiringLicenseCount = 0; // 30일 내 만료 예정 라이선스 수
|
||||||
|
|
||||||
@@ -50,8 +53,10 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
|||||||
_setupAnimations();
|
_setupAnimations();
|
||||||
_authService = GetIt.instance<AuthService>();
|
_authService = GetIt.instance<AuthService>();
|
||||||
_dashboardService = GetIt.instance<DashboardService>();
|
_dashboardService = GetIt.instance<DashboardService>();
|
||||||
|
_lookupService = GetIt.instance<LookupService>();
|
||||||
_loadCurrentUser();
|
_loadCurrentUser();
|
||||||
_loadLicenseExpirySummary();
|
_loadLicenseExpirySummary();
|
||||||
|
_initializeLookupData(); // Lookup 데이터 초기화
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadCurrentUser() async {
|
Future<void> _loadCurrentUser() async {
|
||||||
@@ -92,6 +97,38 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
|||||||
print('[ERROR] 스택 트레이스: ${StackTrace.current}');
|
print('[ERROR] 스택 트레이스: ${StackTrace.current}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lookup 데이터 초기화 (앱 시작 시 한 번만 호출)
|
||||||
|
Future<void> _initializeLookupData() async {
|
||||||
|
try {
|
||||||
|
print('[DEBUG] Lookup 데이터 초기화 시작...');
|
||||||
|
|
||||||
|
// 캐시가 유효하지 않을 때만 로드
|
||||||
|
if (!_lookupService.isCacheValid) {
|
||||||
|
await _lookupService.loadAllLookups();
|
||||||
|
|
||||||
|
if (_lookupService.hasData) {
|
||||||
|
print('[DEBUG] Lookup 데이터 로드 성공!');
|
||||||
|
print('[DEBUG] - 장비 타입: ${_lookupService.equipmentTypes.length}개');
|
||||||
|
print('[DEBUG] - 장비 상태: ${_lookupService.equipmentStatuses.length}개');
|
||||||
|
print('[DEBUG] - 라이선스 타입: ${_lookupService.licenseTypes.length}개');
|
||||||
|
print('[DEBUG] - 제조사: ${_lookupService.manufacturers.length}개');
|
||||||
|
print('[DEBUG] - 사용자 역할: ${_lookupService.userRoles.length}개');
|
||||||
|
print('[DEBUG] - 회사 상태: ${_lookupService.companyStatuses.length}개');
|
||||||
|
} else {
|
||||||
|
print('[WARNING] Lookup 데이터가 비어있습니다.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('[DEBUG] Lookup 데이터 캐시 사용 (유효)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lookupService.error != null) {
|
||||||
|
print('[ERROR] Lookup 데이터 로드 실패: ${_lookupService.error}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('[ERROR] Lookup 데이터 초기화 중 예외 발생: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _setupAnimations() {
|
void _setupAnimations() {
|
||||||
_sidebarAnimationController = AnimationController(
|
_sidebarAnimationController = AnimationController(
|
||||||
@@ -222,81 +259,84 @@ class _AppLayoutRedesignState extends State<AppLayoutRedesign>
|
|||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
final isWideScreen = screenWidth >= 1920;
|
final isWideScreen = screenWidth >= 1920;
|
||||||
|
|
||||||
return Scaffold(
|
return Provider<AuthService>.value(
|
||||||
backgroundColor: ShadcnTheme.backgroundSecondary,
|
value: _authService,
|
||||||
body: Column(
|
child: Scaffold(
|
||||||
children: [
|
backgroundColor: ShadcnTheme.backgroundSecondary,
|
||||||
// F-Pattern: 1차 시선 - 상단 헤더
|
body: Column(
|
||||||
_buildTopHeader(),
|
children: [
|
||||||
|
// F-Pattern: 1차 시선 - 상단 헤더
|
||||||
|
_buildTopHeader(),
|
||||||
|
|
||||||
// 메인 콘텐츠 영역
|
// 메인 콘텐츠 영역
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 좌측 사이드바
|
// 좌측 사이드바
|
||||||
AnimatedBuilder(
|
AnimatedBuilder(
|
||||||
animation: _sidebarAnimation,
|
animation: _sidebarAnimation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Container(
|
return Container(
|
||||||
width: _sidebarAnimation.value,
|
width: _sidebarAnimation.value,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ShadcnTheme.background,
|
color: ShadcnTheme.background,
|
||||||
border: Border(
|
border: Border(
|
||||||
right: BorderSide(
|
right: BorderSide(
|
||||||
color: ShadcnTheme.border,
|
color: ShadcnTheme.border,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: _buildSidebar(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// 메인 콘텐츠 (최대 너비 제한)
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: isWideScreen ? _maxContentWidth : double.infinity,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.all(
|
|
||||||
isWideScreen ? ShadcnTheme.spacing6 : ShadcnTheme.spacing4
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// F-Pattern: 2차 시선 - 페이지 헤더 + 액션
|
|
||||||
_buildPageHeader(),
|
|
||||||
|
|
||||||
const SizedBox(height: ShadcnTheme.spacing4),
|
|
||||||
|
|
||||||
// F-Pattern: 주요 작업 영역
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: ShadcnTheme.background,
|
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
|
||||||
border: Border.all(
|
|
||||||
color: ShadcnTheme.border,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
boxShadow: ShadcnTheme.shadowSm,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg - 1),
|
|
||||||
child: _getContentForRoute(_currentRoute),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
child: _buildSidebar(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 메인 콘텐츠 (최대 너비 제한)
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: isWideScreen ? _maxContentWidth : double.infinity,
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(
|
||||||
|
isWideScreen ? ShadcnTheme.spacing6 : ShadcnTheme.spacing4
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// F-Pattern: 2차 시선 - 페이지 헤더 + 액션
|
||||||
|
_buildPageHeader(),
|
||||||
|
|
||||||
|
const SizedBox(height: ShadcnTheme.spacing4),
|
||||||
|
|
||||||
|
// F-Pattern: 주요 작업 영역
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ShadcnTheme.background,
|
||||||
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||||
|
border: Border.all(
|
||||||
|
color: ShadcnTheme.border,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
boxShadow: ShadcnTheme.shadowSm,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg - 1),
|
||||||
|
child: _getContentForRoute(_currentRoute),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ class AppThemeTailwind {
|
|||||||
colorScheme: const ColorScheme.light(
|
colorScheme: const ColorScheme.light(
|
||||||
primary: primary,
|
primary: primary,
|
||||||
secondary: secondary,
|
secondary: secondary,
|
||||||
background: surface,
|
surface: surface,
|
||||||
surface: cardBackground,
|
|
||||||
error: danger,
|
error: danger,
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: surface,
|
scaffoldBackgroundColor: surface,
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ class _AddressInputState extends State<AddressInput> {
|
|||||||
/// 시/도 드롭다운 오버레이를 표시합니다.
|
/// 시/도 드롭다운 오버레이를 표시합니다.
|
||||||
void _showRegionOverlay() {
|
void _showRegionOverlay() {
|
||||||
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||||
final size = renderBox.size;
|
|
||||||
final offset = renderBox.localToGlobal(Offset.zero);
|
final offset = renderBox.localToGlobal(Offset.zero);
|
||||||
|
|
||||||
final availableHeight =
|
final availableHeight =
|
||||||
@@ -142,7 +141,7 @@ class _AddressInputState extends State<AddressInput> {
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withOpacity(0.3),
|
color: Colors.grey.withValues(alpha: 0.3),
|
||||||
spreadRadius: 1,
|
spreadRadius: 1,
|
||||||
blurRadius: 3,
|
blurRadius: 3,
|
||||||
offset: const Offset(0, 1),
|
offset: const Offset(0, 1),
|
||||||
|
|||||||
@@ -13,18 +13,18 @@
|
|||||||
/// - 유틸리티:
|
/// - 유틸리티:
|
||||||
/// - PhoneUtils: 전화번호 관련 유틸리티
|
/// - PhoneUtils: 전화번호 관련 유틸리티
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:superport/models/address_model.dart';
|
// import 'package:superport/models/address_model.dart'; // 사용되지 않는 import
|
||||||
import 'package:superport/models/company_model.dart';
|
import 'package:superport/models/company_model.dart';
|
||||||
import 'package:superport/screens/common/custom_widgets.dart';
|
// import 'package:superport/screens/common/custom_widgets.dart'; // 사용되지 않는 import
|
||||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||||
import 'package:superport/screens/company/controllers/company_form_controller.dart';
|
import 'package:superport/screens/company/controllers/company_form_controller.dart';
|
||||||
import 'package:superport/screens/company/widgets/branch_card.dart';
|
// import 'package:superport/screens/company/widgets/branch_card.dart'; // 사용되지 않는 import
|
||||||
import 'package:superport/screens/company/widgets/company_form_header.dart';
|
import 'package:superport/screens/company/widgets/company_form_header.dart';
|
||||||
import 'package:superport/screens/company/widgets/contact_info_form.dart';
|
import 'package:superport/screens/company/widgets/contact_info_form.dart';
|
||||||
import 'package:superport/screens/company/widgets/duplicate_company_dialog.dart';
|
import 'package:superport/screens/company/widgets/duplicate_company_dialog.dart';
|
||||||
import 'package:superport/screens/company/widgets/map_dialog.dart';
|
import 'package:superport/screens/company/widgets/map_dialog.dart';
|
||||||
import 'package:superport/screens/company/widgets/branch_form_widget.dart';
|
import 'package:superport/screens/company/widgets/branch_form_widget.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
// import 'package:superport/services/mock_data_service.dart'; // Mock 서비스 제거
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:superport/screens/company/controllers/branch_form_controller.dart';
|
import 'package:superport/screens/company/controllers/branch_form_controller.dart';
|
||||||
@@ -114,9 +114,8 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
|||||||
debugPrint('📌 회사 폼 초기화 - API 모드: $useApi, companyId: $companyId');
|
debugPrint('📌 회사 폼 초기화 - API 모드: $useApi, companyId: $companyId');
|
||||||
|
|
||||||
_controller = CompanyFormController(
|
_controller = CompanyFormController(
|
||||||
dataService: useApi ? null : MockDataService(),
|
|
||||||
companyId: companyId,
|
companyId: companyId,
|
||||||
useApi: useApi,
|
useApi: true, // 항상 API 사용
|
||||||
);
|
);
|
||||||
|
|
||||||
// 일반 회사 수정 모드일 때 데이터 로드
|
// 일반 회사 수정 모드일 때 데이터 로드
|
||||||
@@ -140,10 +139,13 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
|||||||
|
|
||||||
// 지점 수정 모드일 때 branchId로 branch 정보 세팅
|
// 지점 수정 모드일 때 branchId로 branch 정보 세팅
|
||||||
if (isBranch && branchId != null) {
|
if (isBranch && branchId != null) {
|
||||||
final company = MockDataService().getCompanyById(companyId!);
|
// Mock 서비스 제거 - API를 통해 데이터 로드
|
||||||
// 디버그: 진입 시 companyId, branchId, company, branches 정보 출력
|
// 디버그: 진입 시 companyId, branchId 정보 출력
|
||||||
print('[DEBUG] 지점 수정 진입: companyId=$companyId, branchId=$branchId');
|
print('[DEBUG] 지점 수정 진입: companyId=$companyId, branchId=$branchId');
|
||||||
if (company != null && company.branches != null) {
|
// TODO: API를 통해 회사 데이터 로드 필요
|
||||||
|
// 아래 코드는 Mock 서비스 제거로 인해 주석 처리됨
|
||||||
|
/*
|
||||||
|
if (false) { // 임시로 비활성화
|
||||||
print(
|
print(
|
||||||
'[DEBUG] 불러온 company.name=${company.name}, branches=${company.branches!.map((b) => 'id:${b.id}, name:${b.name}, remark:${b.remark}').toList()}',
|
'[DEBUG] 불러온 company.name=${company.name}, branches=${company.branches!.map((b) => 'id:${b.id}, name:${b.name}, remark:${b.remark}').toList()}',
|
||||||
);
|
);
|
||||||
@@ -181,6 +183,7 @@ class _CompanyFormScreenState extends State<CompanyFormScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:superport/core/constants/app_constants.dart';
|
||||||
import 'package:superport/models/company_model.dart';
|
import 'package:superport/models/company_model.dart';
|
||||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||||
@@ -11,7 +12,7 @@ import 'package:superport/screens/common/widgets/standard_data_table.dart'
|
|||||||
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
|
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
|
||||||
import 'package:superport/screens/common/widgets/standard_states.dart';
|
import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
// import 'package:superport/services/mock_data_service.dart'; // Mock 서비스 제거
|
||||||
import 'package:superport/screens/company/widgets/company_branch_dialog.dart';
|
import 'package:superport/screens/company/widgets/company_branch_dialog.dart';
|
||||||
import 'package:superport/screens/company/controllers/company_list_controller.dart';
|
import 'package:superport/screens/company/controllers/company_list_controller.dart';
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = CompanyListController(dataService: MockDataService());
|
_controller = CompanyListController();
|
||||||
_controller.initializeWithPageSize(_pageSize);
|
_controller.initializeWithPageSize(_pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +49,11 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
|||||||
/// 검색어 입력 처리 (디바운싱)
|
/// 검색어 입력 처리 (디바운싱)
|
||||||
void _onSearchChanged(String value) {
|
void _onSearchChanged(String value) {
|
||||||
_debounceTimer?.cancel();
|
_debounceTimer?.cancel();
|
||||||
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
|
_debounceTimer = Timer(AppConstants.searchDebounce, () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
});
|
});
|
||||||
_controller.updateSearchKeyword(value);
|
_controller.search(value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,12 +81,13 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
final success = await _controller.deleteCompany(id);
|
try {
|
||||||
if (!success) {
|
await _controller.deleteCompany(id);
|
||||||
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(_controller.error ?? '삭제에 실패했습니다'),
|
content: Text(e.toString()),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -268,7 +270,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
|||||||
error: controller.error,
|
error: controller.error,
|
||||||
onRefresh: controller.refresh,
|
onRefresh: controller.refresh,
|
||||||
emptyMessage:
|
emptyMessage:
|
||||||
controller.searchKeyword.isNotEmpty
|
controller.searchQuery.isNotEmpty
|
||||||
? '검색 결과가 없습니다'
|
? '검색 결과가 없습니다'
|
||||||
: '등록된 회사가 없습니다',
|
: '등록된 회사가 없습니다',
|
||||||
emptyIcon: Icons.business_outlined,
|
emptyIcon: Icons.business_outlined,
|
||||||
@@ -279,7 +281,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
|||||||
placeholder: '회사명, 담당자명, 연락처로 검색',
|
placeholder: '회사명, 담당자명, 연락처로 검색',
|
||||||
onChanged: _onSearchChanged, // 실시간 검색 (디바운싱)
|
onChanged: _onSearchChanged, // 실시간 검색 (디바운싱)
|
||||||
onSearch:
|
onSearch:
|
||||||
() => _controller.updateSearchKeyword(
|
() => _controller.search(
|
||||||
_searchController.text,
|
_searchController.text,
|
||||||
), // 즉시 검색
|
), // 즉시 검색
|
||||||
onClear: () {
|
onClear: () {
|
||||||
@@ -298,8 +300,8 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
|||||||
totalCount: totalCount,
|
totalCount: totalCount,
|
||||||
onRefresh: controller.refresh,
|
onRefresh: controller.refresh,
|
||||||
statusMessage:
|
statusMessage:
|
||||||
controller.searchKeyword.isNotEmpty
|
controller.searchQuery.isNotEmpty
|
||||||
? '"${controller.searchKeyword}" 검색 결과'
|
? '"${controller.searchQuery}" 검색 결과'
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -319,12 +321,12 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
|||||||
displayCompanies.isEmpty
|
displayCompanies.isEmpty
|
||||||
? StandardEmptyState(
|
? StandardEmptyState(
|
||||||
title:
|
title:
|
||||||
controller.searchKeyword.isNotEmpty
|
controller.searchQuery.isNotEmpty
|
||||||
? '검색 결과가 없습니다'
|
? '검색 결과가 없습니다'
|
||||||
: '등록된 회사가 없습니다',
|
: '등록된 회사가 없습니다',
|
||||||
icon: Icons.business_outlined,
|
icon: Icons.business_outlined,
|
||||||
action:
|
action:
|
||||||
controller.searchKeyword.isEmpty
|
controller.searchQuery.isEmpty
|
||||||
? StandardActionButtons.addButton(
|
? StandardActionButtons.addButton(
|
||||||
text: '첫 회사 추가하기',
|
text: '첫 회사 추가하기',
|
||||||
onPressed: _navigateToAddScreen,
|
onPressed: _navigateToAddScreen,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:superport/models/address_model.dart';
|
import 'package:superport/models/address_model.dart';
|
||||||
import 'package:superport/models/company_model.dart';
|
import 'package:superport/models/company_model.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
// import 'package:superport/services/mock_data_service.dart'; // Mock 서비스 제거
|
||||||
import 'package:superport/services/company_service.dart';
|
import 'package:superport/services/company_service.dart';
|
||||||
import 'package:superport/core/errors/failures.dart';
|
import 'package:superport/core/errors/failures.dart';
|
||||||
import 'package:superport/utils/phone_utils.dart';
|
import 'package:superport/utils/phone_utils.dart';
|
||||||
@@ -21,7 +21,7 @@ import 'branch_form_controller.dart'; // 분리된 지점 컨트롤러 import
|
|||||||
|
|
||||||
/// 회사 폼 컨트롤러 - 비즈니스 로직 처리
|
/// 회사 폼 컨트롤러 - 비즈니스 로직 처리
|
||||||
class CompanyFormController {
|
class CompanyFormController {
|
||||||
final MockDataService? dataService;
|
// final MockDataService? dataService; // Mock 서비스 제거
|
||||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||||
final int? companyId;
|
final int? companyId;
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class CompanyFormController {
|
|||||||
bool preventAutoFocus = false;
|
bool preventAutoFocus = false;
|
||||||
final Map<int, bool> isNewlyAddedBranch = {};
|
final Map<int, bool> isNewlyAddedBranch = {};
|
||||||
|
|
||||||
CompanyFormController({this.dataService, this.companyId, bool useApi = false})
|
CompanyFormController({this.companyId, bool useApi = true})
|
||||||
: _useApi = useApi {
|
: _useApi = useApi {
|
||||||
_setupFocusNodes();
|
_setupFocusNodes();
|
||||||
_setupControllerListeners();
|
_setupControllerListeners();
|
||||||
@@ -96,13 +96,8 @@ class CompanyFormController {
|
|||||||
try {
|
try {
|
||||||
List<Company> companies;
|
List<Company> companies;
|
||||||
|
|
||||||
if (_useApi) {
|
// API만 사용
|
||||||
companies = await _companyService.getCompanies();
|
companies = await _companyService.getCompanies();
|
||||||
} else if (dataService != null) {
|
|
||||||
companies = dataService!.getAllCompanies();
|
|
||||||
} else {
|
|
||||||
companies = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
companyNames = companies.map((c) => c.name).toList();
|
companyNames = companies.map((c) => c.name).toList();
|
||||||
filteredCompanyNames = companyNames;
|
filteredCompanyNames = companyNames;
|
||||||
@@ -125,9 +120,9 @@ class CompanyFormController {
|
|||||||
if (_useApi) {
|
if (_useApi) {
|
||||||
debugPrint('📝 API에서 회사 정보 로드 중...');
|
debugPrint('📝 API에서 회사 정보 로드 중...');
|
||||||
company = await _companyService.getCompanyDetail(companyId!);
|
company = await _companyService.getCompanyDetail(companyId!);
|
||||||
} else if (dataService != null) {
|
} else {
|
||||||
debugPrint('📝 Mock에서 회사 정보 로드 중...');
|
debugPrint('📝 API만 사용 가능');
|
||||||
company = dataService!.getCompanyById(companyId!);
|
throw Exception('API를 통해만 데이터를 로드할 수 있습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('📝 로드된 회사: $company');
|
debugPrint('📝 로드된 회사: $company');
|
||||||
@@ -234,8 +229,9 @@ class CompanyFormController {
|
|||||||
debugPrint('Failed to load company data: ${e.message}');
|
debugPrint('Failed to load company data: ${e.message}');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (dataService != null) {
|
} else {
|
||||||
company = dataService!.getCompanyById(companyId!);
|
// API만 사용
|
||||||
|
debugPrint('API를 통해만 데이터를 로드할 수 있습니다');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (company != null) {
|
if (company != null) {
|
||||||
@@ -364,8 +360,9 @@ class CompanyFormController {
|
|||||||
// 오류 발생 시 중복 없음으로 처리
|
// 오류 발생 시 중복 없음으로 처리
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else if (dataService != null) {
|
} else {
|
||||||
return dataService!.findCompanyByName(name);
|
// API만 사용
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -440,12 +437,9 @@ class CompanyFormController {
|
|||||||
debugPrint('Unexpected error saving company: $e');
|
debugPrint('Unexpected error saving company: $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (dataService != null) {
|
} else {
|
||||||
if (companyId == null) {
|
// API만 사용
|
||||||
dataService!.addCompany(company);
|
throw Exception('API를 통해만 데이터를 저장할 수 있습니다');
|
||||||
} else {
|
|
||||||
dataService!.updateCompany(company);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -484,10 +478,9 @@ class CompanyFormController {
|
|||||||
debugPrint('Failed to save branch: ${e.message}');
|
debugPrint('Failed to save branch: ${e.message}');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (dataService != null) {
|
} else {
|
||||||
// Mock 데이터 서비스 사용
|
// API만 사용
|
||||||
dataService!.updateBranch(companyId!, branch);
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,331 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:superport/models/company_model.dart';
|
||||||
|
import 'package:superport/services/company_service.dart';
|
||||||
|
import 'package:superport/core/errors/failures.dart';
|
||||||
|
|
||||||
|
// 회사 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||||
|
class CompanyListController extends ChangeNotifier {
|
||||||
|
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||||
|
|
||||||
|
List<Company> companies = [];
|
||||||
|
List<Company> filteredCompanies = [];
|
||||||
|
String searchKeyword = '';
|
||||||
|
final Set<int> selectedCompanyIds = {};
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
// API만 사용
|
||||||
|
|
||||||
|
// 페이지네이션
|
||||||
|
int _currentPage = 1;
|
||||||
|
int _perPage = 20;
|
||||||
|
bool _hasMore = true;
|
||||||
|
|
||||||
|
// 필터
|
||||||
|
bool? _isActiveFilter;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
String? get error => _error;
|
||||||
|
bool get hasMore => _hasMore;
|
||||||
|
int get currentPage => _currentPage;
|
||||||
|
bool? get isActiveFilter => _isActiveFilter;
|
||||||
|
|
||||||
|
CompanyListController();
|
||||||
|
|
||||||
|
// 초기 데이터 로드
|
||||||
|
Future<void> initialize() async {
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 🚀 회사 목록 초기화 시작');
|
||||||
|
print('║ • 페이지 크기: $_perPage개');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
await loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 크기를 지정하여 초기화
|
||||||
|
Future<void> initializeWithPageSize(int pageSize) async {
|
||||||
|
_perPage = pageSize;
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 🚀 회사 목록 초기화 시작 (커스텀 페이지 크기)');
|
||||||
|
print('║ • 페이지 크기: $_perPage개');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
await loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터 로드 및 필터 적용
|
||||||
|
Future<void> loadData({bool isRefresh = false}) async {
|
||||||
|
print('🔍 [DEBUG] loadData 시작 - currentPage: $_currentPage, hasMore: $_hasMore, companies.length: ${companies.length}');
|
||||||
|
print('[CompanyListController] loadData called - isRefresh: $isRefresh');
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
_currentPage = 1;
|
||||||
|
_hasMore = true;
|
||||||
|
companies.clear();
|
||||||
|
filteredCompanies.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isLoading || (!_hasMore && !isRefresh)) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API 호출 - 지점 정보 포함
|
||||||
|
print('[CompanyListController] Using API to fetch companies with branches');
|
||||||
|
|
||||||
|
// 지점 정보를 포함한 전체 회사 목록 가져오기
|
||||||
|
final apiCompaniesWithBranches = await _companyService.getCompaniesWithBranchesFlat();
|
||||||
|
|
||||||
|
// 상세한 회사 정보 로그 출력
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 📊 회사 목록 로드 완료');
|
||||||
|
print('║ ▶ 총 회사 수: ${apiCompaniesWithBranches.length}개');
|
||||||
|
print('╟──────────────────────────────────────────────────────────');
|
||||||
|
|
||||||
|
// 지점이 있는 회사와 없는 회사 구분
|
||||||
|
int companiesWithBranches = 0;
|
||||||
|
int totalBranches = 0;
|
||||||
|
|
||||||
|
for (final company in apiCompaniesWithBranches) {
|
||||||
|
if (company.branches?.isNotEmpty ?? false) {
|
||||||
|
companiesWithBranches++;
|
||||||
|
totalBranches += company.branches!.length;
|
||||||
|
print('║ • ${company.name}: ${company.branches!.length}개 지점');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final companiesWithoutBranches = apiCompaniesWithBranches.length - companiesWithBranches;
|
||||||
|
|
||||||
|
print('╟──────────────────────────────────────────────────────────');
|
||||||
|
print('║ 📈 통계');
|
||||||
|
print('║ • 지점이 있는 회사: ${companiesWithBranches}개');
|
||||||
|
print('║ • 지점이 없는 회사: ${companiesWithoutBranches}개');
|
||||||
|
print('║ • 총 지점 수: ${totalBranches}개');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
// 검색어 필터 적용 (서버에서 필터링이 안 되므로 클라이언트에서 처리)
|
||||||
|
List<Company> filteredApiCompanies = apiCompaniesWithBranches;
|
||||||
|
if (searchKeyword.isNotEmpty) {
|
||||||
|
final keyword = searchKeyword.toLowerCase();
|
||||||
|
filteredApiCompanies = apiCompaniesWithBranches.where((company) {
|
||||||
|
return company.name.toLowerCase().contains(keyword) ||
|
||||||
|
(company.contactName?.toLowerCase().contains(keyword) ?? false) ||
|
||||||
|
(company.contactPhone?.toLowerCase().contains(keyword) ?? false);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 🔍 검색 필터 적용');
|
||||||
|
print('║ • 검색어: "$searchKeyword"');
|
||||||
|
print('║ • 필터 전: ${apiCompaniesWithBranches.length}개');
|
||||||
|
print('║ • 필터 후: ${filteredApiCompanies.length}개');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 활성 상태 필터 적용 (현재 API에서 지원하지 않으므로 주석 처리)
|
||||||
|
// if (_isActiveFilter != null) {
|
||||||
|
// filteredApiCompanies = filteredApiCompanies.where((c) => c.isActive == _isActiveFilter).toList();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 전체 데이터를 한 번에 로드 (View에서 페이지네이션 처리)
|
||||||
|
companies = filteredApiCompanies;
|
||||||
|
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||||
|
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 📑 전체 데이터 로드 완료');
|
||||||
|
print('║ • 로드된 회사 수: ${companies.length}개');
|
||||||
|
print('║ • 필터링된 회사 수: ${filteredApiCompanies.length}개');
|
||||||
|
print('║ • View에서 페이지네이션 처리 예정');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
// 필터 적용
|
||||||
|
applyFilters();
|
||||||
|
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ ✅ 최종 화면 표시');
|
||||||
|
print('║ • 화면에 표시될 회사 수: ${filteredCompanies.length}개');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
selectedCompanyIds.clear();
|
||||||
|
} on Failure catch (e) {
|
||||||
|
print('[CompanyListController] Failure loading companies: ${e.message}');
|
||||||
|
_error = e.message;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
print('[CompanyListController] Error loading companies: $e');
|
||||||
|
print('[CompanyListController] Error type: ${e.runtimeType}');
|
||||||
|
print('[CompanyListController] Stack trace: $stackTrace');
|
||||||
|
_error = '회사 목록을 불러오는 중 오류가 발생했습니다: $e';
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 및 필터 적용
|
||||||
|
void applyFilters() {
|
||||||
|
filteredCompanies = companies.where((company) {
|
||||||
|
// 검색어 필터
|
||||||
|
if (searchKeyword.isNotEmpty) {
|
||||||
|
final keyword = searchKeyword.toLowerCase();
|
||||||
|
final matchesName = company.name.toLowerCase().contains(keyword);
|
||||||
|
final matchesContact = company.contactName?.toLowerCase().contains(keyword) ?? false;
|
||||||
|
final matchesPhone = company.contactPhone?.toLowerCase().contains(keyword) ?? false;
|
||||||
|
|
||||||
|
if (!matchesName && !matchesContact && !matchesPhone) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 활성 상태 필터 (현재 API에서 지원안함)
|
||||||
|
// if (_isActiveFilter != null) {
|
||||||
|
// 추후 API 지원 시 구현
|
||||||
|
// }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색어 변경
|
||||||
|
Future<void> updateSearchKeyword(String keyword) async {
|
||||||
|
searchKeyword = keyword;
|
||||||
|
|
||||||
|
if (keyword.isNotEmpty) {
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 🔍 검색어 변경: "$keyword"');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
} else {
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ ❌ 검색어 초기화');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 사용 시 새로 조회
|
||||||
|
await loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 활성 상태 필터 변경
|
||||||
|
Future<void> changeActiveFilter(bool? isActive) async {
|
||||||
|
_isActiveFilter = isActive;
|
||||||
|
await loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회사 선택/해제
|
||||||
|
void toggleCompanySelection(int? companyId) {
|
||||||
|
if (companyId == null) return;
|
||||||
|
|
||||||
|
if (selectedCompanyIds.contains(companyId)) {
|
||||||
|
selectedCompanyIds.remove(companyId);
|
||||||
|
} else {
|
||||||
|
selectedCompanyIds.add(companyId);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 선택/해제
|
||||||
|
void toggleSelectAll() {
|
||||||
|
if (selectedCompanyIds.length == filteredCompanies.length) {
|
||||||
|
selectedCompanyIds.clear();
|
||||||
|
} else {
|
||||||
|
selectedCompanyIds.clear();
|
||||||
|
for (final company in filteredCompanies) {
|
||||||
|
if (company.id != null) {
|
||||||
|
selectedCompanyIds.add(company.id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 회사 수 반환
|
||||||
|
int getSelectedCount() {
|
||||||
|
return selectedCompanyIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회사 삭제
|
||||||
|
Future<bool> deleteCompany(int companyId) async {
|
||||||
|
try {
|
||||||
|
// API를 통한 삭제
|
||||||
|
await _companyService.deleteCompany(companyId);
|
||||||
|
|
||||||
|
// 로컬 리스트에서도 제거
|
||||||
|
companies.removeWhere((c) => c.id == companyId);
|
||||||
|
filteredCompanies.removeWhere((c) => c.id == companyId);
|
||||||
|
selectedCompanyIds.remove(companyId);
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} on Failure catch (e) {
|
||||||
|
_error = e.message;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
_error = '회사 삭제 중 오류가 발생했습니다: $e';
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 회사들 삭제
|
||||||
|
Future<bool> deleteSelectedCompanies() async {
|
||||||
|
final selectedIds = selectedCompanyIds.toList();
|
||||||
|
int successCount = 0;
|
||||||
|
|
||||||
|
for (final companyId in selectedIds) {
|
||||||
|
if (await deleteCompany(companyId)) {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return successCount == selectedIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회사 정보 업데이트 (로컬)
|
||||||
|
void updateCompanyLocally(Company updatedCompany) {
|
||||||
|
final index = companies.indexWhere((c) => c.id == updatedCompany.id);
|
||||||
|
if (index != -1) {
|
||||||
|
companies[index] = updatedCompany;
|
||||||
|
applyFilters();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회사 추가 (로컬)
|
||||||
|
void addCompanyLocally(Company newCompany) {
|
||||||
|
companies.insert(0, newCompany);
|
||||||
|
applyFilters();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 더 많은 데이터 로드
|
||||||
|
Future<void> loadMore() async {
|
||||||
|
print('🔍 [DEBUG] loadMore 호출됨 - hasMore: $_hasMore, isLoading: $_isLoading');
|
||||||
|
if (!_hasMore || _isLoading) {
|
||||||
|
print('🔍 [DEBUG] loadMore 조건 미충족으로 종료 (hasMore: $_hasMore, isLoading: $_isLoading)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
print('🔍 [DEBUG] loadMore 실행 - 추가 데이터 로드 시작');
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API만 사용하므로 토글 기능 제거
|
||||||
|
|
||||||
|
// 에러 처리
|
||||||
|
void clearError() {
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 리프레시
|
||||||
|
Future<void> refresh() async {
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 🔄 회사 목록 새로고침 시작');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
await loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,241 +2,94 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:superport/models/company_model.dart';
|
import 'package:superport/models/company_model.dart';
|
||||||
import 'package:superport/services/company_service.dart';
|
import 'package:superport/services/company_service.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
import 'package:superport/core/utils/error_handler.dart';
|
||||||
import 'package:superport/core/errors/failures.dart';
|
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||||
|
import 'package:superport/data/models/common/pagination_params.dart';
|
||||||
|
|
||||||
// 회사 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
/// 회사 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||||
class CompanyListController extends ChangeNotifier {
|
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||||
final MockDataService dataService;
|
class CompanyListController extends BaseListController<Company> {
|
||||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
late final CompanyService _companyService;
|
||||||
|
|
||||||
List<Company> companies = [];
|
// 추가 상태 관리
|
||||||
List<Company> filteredCompanies = [];
|
|
||||||
String searchKeyword = '';
|
|
||||||
final Set<int> selectedCompanyIds = {};
|
final Set<int> selectedCompanyIds = {};
|
||||||
|
|
||||||
bool _isLoading = false;
|
|
||||||
String? _error;
|
|
||||||
bool _useApi = true; // Feature flag for API usage
|
|
||||||
|
|
||||||
// 페이지네이션
|
|
||||||
int _currentPage = 1;
|
|
||||||
int _perPage = 20;
|
|
||||||
bool _hasMore = true;
|
|
||||||
|
|
||||||
// 필터
|
// 필터
|
||||||
bool? _isActiveFilter;
|
bool? _isActiveFilter;
|
||||||
|
CompanyType? _typeFilter;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
bool get isLoading => _isLoading;
|
List<Company> get companies => items;
|
||||||
String? get error => _error;
|
List<Company> get filteredCompanies => items;
|
||||||
bool get hasMore => _hasMore;
|
|
||||||
int get currentPage => _currentPage;
|
|
||||||
bool? get isActiveFilter => _isActiveFilter;
|
bool? get isActiveFilter => _isActiveFilter;
|
||||||
|
CompanyType? get typeFilter => _typeFilter;
|
||||||
|
|
||||||
CompanyListController({required this.dataService});
|
CompanyListController() {
|
||||||
|
if (GetIt.instance.isRegistered<CompanyService>()) {
|
||||||
|
_companyService = GetIt.instance<CompanyService>();
|
||||||
|
} else {
|
||||||
|
throw Exception('CompanyService not registered in GetIt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 초기 데이터 로드
|
// 초기 데이터 로드
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 🚀 회사 목록 초기화 시작');
|
|
||||||
print('║ • 페이지 크기: $_perPage개');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
await loadData(isRefresh: true);
|
await loadData(isRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 페이지 크기를 지정하여 초기화
|
// 페이지 크기를 지정하여 초기화
|
||||||
Future<void> initializeWithPageSize(int pageSize) async {
|
Future<void> initializeWithPageSize(int newPageSize) async {
|
||||||
_perPage = pageSize;
|
pageSize = newPageSize;
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 🚀 회사 목록 초기화 시작 (커스텀 페이지 크기)');
|
|
||||||
print('║ • 페이지 크기: $_perPage개');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
await loadData(isRefresh: true);
|
await loadData(isRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 데이터 로드 및 필터 적용
|
@override
|
||||||
Future<void> loadData({bool isRefresh = false}) async {
|
Future<PagedResult<Company>> fetchData({
|
||||||
print('🔍 [DEBUG] loadData 시작 - currentPage: $_currentPage, hasMore: $_hasMore, companies.length: ${companies.length}');
|
required PaginationParams params,
|
||||||
print('[CompanyListController] loadData called - isRefresh: $isRefresh');
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
}) async {
|
||||||
|
// API 호출 - 회사 목록 조회
|
||||||
|
final apiCompanies = await ErrorHandler.handleApiCall<List<Company>>(
|
||||||
|
() => _companyService.getCompanies(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
search: params.search,
|
||||||
|
isActive: _isActiveFilter,
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (isRefresh) {
|
final items = apiCompanies ?? [];
|
||||||
_currentPage = 1;
|
|
||||||
_hasMore = true;
|
|
||||||
companies.clear();
|
|
||||||
filteredCompanies.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isLoading || (!_hasMore && !isRefresh)) return;
|
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||||
|
final meta = PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: items.length < params.perPage ?
|
||||||
|
(params.page - 1) * params.perPage + items.length :
|
||||||
|
params.page * params.perPage + 1,
|
||||||
|
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||||
|
hasNext: items.length >= params.perPage,
|
||||||
|
hasPrevious: params.page > 1,
|
||||||
|
);
|
||||||
|
|
||||||
_isLoading = true;
|
return PagedResult(items: items, meta: meta);
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (_useApi) {
|
|
||||||
// API 호출 - 지점 정보 포함
|
|
||||||
print('[CompanyListController] Using API to fetch companies with branches');
|
|
||||||
|
|
||||||
// 지점 정보를 포함한 전체 회사 목록 가져오기
|
|
||||||
final apiCompaniesWithBranches = await _companyService.getCompaniesWithBranchesFlat();
|
|
||||||
|
|
||||||
// 상세한 회사 정보 로그 출력
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 📊 회사 목록 로드 완료');
|
|
||||||
print('║ ▶ 총 회사 수: ${apiCompaniesWithBranches.length}개');
|
|
||||||
print('╟──────────────────────────────────────────────────────────');
|
|
||||||
|
|
||||||
// 지점이 있는 회사와 없는 회사 구분
|
|
||||||
int companiesWithBranches = 0;
|
|
||||||
int totalBranches = 0;
|
|
||||||
|
|
||||||
for (final company in apiCompaniesWithBranches) {
|
|
||||||
if (company.branches?.isNotEmpty ?? false) {
|
|
||||||
companiesWithBranches++;
|
|
||||||
totalBranches += company.branches!.length;
|
|
||||||
print('║ • ${company.name}: ${company.branches!.length}개 지점');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final companiesWithoutBranches = apiCompaniesWithBranches.length - companiesWithBranches;
|
|
||||||
|
|
||||||
print('╟──────────────────────────────────────────────────────────');
|
|
||||||
print('║ 📈 통계');
|
|
||||||
print('║ • 지점이 있는 회사: ${companiesWithBranches}개');
|
|
||||||
print('║ • 지점이 없는 회사: ${companiesWithoutBranches}개');
|
|
||||||
print('║ • 총 지점 수: ${totalBranches}개');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
// 검색어 필터 적용 (서버에서 필터링이 안 되므로 클라이언트에서 처리)
|
|
||||||
List<Company> filteredApiCompanies = apiCompaniesWithBranches;
|
|
||||||
if (searchKeyword.isNotEmpty) {
|
|
||||||
final keyword = searchKeyword.toLowerCase();
|
|
||||||
filteredApiCompanies = apiCompaniesWithBranches.where((company) {
|
|
||||||
return company.name.toLowerCase().contains(keyword) ||
|
|
||||||
(company.contactName?.toLowerCase().contains(keyword) ?? false) ||
|
|
||||||
(company.contactPhone?.toLowerCase().contains(keyword) ?? false);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 🔍 검색 필터 적용');
|
|
||||||
print('║ • 검색어: "$searchKeyword"');
|
|
||||||
print('║ • 필터 전: ${apiCompaniesWithBranches.length}개');
|
|
||||||
print('║ • 필터 후: ${filteredApiCompanies.length}개');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 활성 상태 필터 적용 (현재 API에서 지원하지 않으므로 주석 처리)
|
|
||||||
// if (_isActiveFilter != null) {
|
|
||||||
// filteredApiCompanies = filteredApiCompanies.where((c) => c.isActive == _isActiveFilter).toList();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 전체 데이터를 한 번에 로드 (View에서 페이지네이션 처리)
|
|
||||||
companies = filteredApiCompanies;
|
|
||||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
|
||||||
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 📑 전체 데이터 로드 완료');
|
|
||||||
print('║ • 로드된 회사 수: ${companies.length}개');
|
|
||||||
print('║ • 필터링된 회사 수: ${filteredApiCompanies.length}개');
|
|
||||||
print('║ • View에서 페이지네이션 처리 예정');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
} else {
|
|
||||||
// Mock 데이터 사용
|
|
||||||
companies = dataService.getAllCompanies();
|
|
||||||
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 🔧 Mock 데이터 로드 완료');
|
|
||||||
print('║ ▶ 총 회사 수: ${companies.length}개');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
_hasMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 필터 적용
|
|
||||||
applyFilters();
|
|
||||||
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ ✅ 최종 화면 표시');
|
|
||||||
print('║ • 화면에 표시될 회사 수: ${filteredCompanies.length}개');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
selectedCompanyIds.clear();
|
|
||||||
} on Failure catch (e) {
|
|
||||||
print('[CompanyListController] Failure loading companies: ${e.message}');
|
|
||||||
_error = e.message;
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
print('[CompanyListController] Error loading companies: $e');
|
|
||||||
print('[CompanyListController] Error type: ${e.runtimeType}');
|
|
||||||
print('[CompanyListController] Stack trace: $stackTrace');
|
|
||||||
_error = '회사 목록을 불러오는 중 오류가 발생했습니다: $e';
|
|
||||||
} finally {
|
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검색 및 필터 적용
|
@override
|
||||||
void applyFilters() {
|
bool filterItem(Company item, String query) {
|
||||||
filteredCompanies = companies.where((company) {
|
final q = query.toLowerCase();
|
||||||
// 검색어 필터
|
return item.name.toLowerCase().contains(q) ||
|
||||||
if (searchKeyword.isNotEmpty) {
|
(item.contactPhone?.toLowerCase().contains(q) ?? false) ||
|
||||||
final keyword = searchKeyword.toLowerCase();
|
(item.contactEmail?.toLowerCase().contains(q) ?? false) ||
|
||||||
final matchesName = company.name.toLowerCase().contains(keyword);
|
(item.companyTypes.any((type) => type.name.toLowerCase().contains(q))) ||
|
||||||
final matchesContact = company.contactName?.toLowerCase().contains(keyword) ?? false;
|
(item.address.toString().toLowerCase().contains(q));
|
||||||
final matchesPhone = company.contactPhone?.toLowerCase().contains(keyword) ?? false;
|
|
||||||
|
|
||||||
if (!matchesName && !matchesContact && !matchesPhone) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 활성 상태 필터 (API 사용 시에는 서버에서 필터링되므로 여기서는 Mock 데이터용)
|
|
||||||
if (_isActiveFilter != null && !_useApi) {
|
|
||||||
// Mock 데이터에는 isActive 필드가 없으므로 모두 활성으로 간주
|
|
||||||
if (_isActiveFilter == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검색어 변경
|
// 회사 선택/선택 해제
|
||||||
Future<void> updateSearchKeyword(String keyword) async {
|
void toggleSelection(int companyId) {
|
||||||
searchKeyword = keyword;
|
|
||||||
|
|
||||||
if (keyword.isNotEmpty) {
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 🔍 검색어 변경: "$keyword"');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
} else {
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ ❌ 검색어 초기화');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_useApi) {
|
|
||||||
// API 사용 시 새로 조회
|
|
||||||
await loadData(isRefresh: true);
|
|
||||||
} else {
|
|
||||||
// Mock 데이터 사용 시 필터만 적용
|
|
||||||
applyFilters();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 활성 상태 필터 변경
|
|
||||||
Future<void> changeActiveFilter(bool? isActive) async {
|
|
||||||
_isActiveFilter = isActive;
|
|
||||||
await loadData(isRefresh: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 회사 선택/해제
|
|
||||||
void toggleCompanySelection(int? companyId) {
|
|
||||||
if (companyId == null) return;
|
|
||||||
|
|
||||||
if (selectedCompanyIds.contains(companyId)) {
|
if (selectedCompanyIds.contains(companyId)) {
|
||||||
selectedCompanyIds.remove(companyId);
|
selectedCompanyIds.remove(companyId);
|
||||||
} else {
|
} else {
|
||||||
@@ -245,119 +98,73 @@ class CompanyListController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전체 선택/해제
|
// 모든 선택 해제
|
||||||
void toggleSelectAll() {
|
void clearSelection() {
|
||||||
if (selectedCompanyIds.length == filteredCompanies.length) {
|
selectedCompanyIds.clear();
|
||||||
selectedCompanyIds.clear();
|
|
||||||
} else {
|
|
||||||
selectedCompanyIds.clear();
|
|
||||||
for (final company in filteredCompanies) {
|
|
||||||
if (company.id != null) {
|
|
||||||
selectedCompanyIds.add(company.id!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 회사 수 반환
|
// 필터 설정
|
||||||
int getSelectedCount() {
|
void setFilters({bool? isActive, CompanyType? type}) {
|
||||||
return selectedCompanyIds.length;
|
_isActiveFilter = isActive;
|
||||||
|
_typeFilter = type;
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터 초기화
|
||||||
|
void clearFilters() {
|
||||||
|
_isActiveFilter = null;
|
||||||
|
_typeFilter = null;
|
||||||
|
search('');
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회사 추가
|
||||||
|
Future<void> addCompany(Company company) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _companyService.createCompany(company),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회사 수정
|
||||||
|
Future<void> updateCompany(Company company) async {
|
||||||
|
if (company.id == null) {
|
||||||
|
throw Exception('회사 ID가 없습니다');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _companyService.updateCompany(company.id!, company),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
updateItemLocally(company, (c) => c.id == company.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 회사 삭제
|
// 회사 삭제
|
||||||
Future<bool> deleteCompany(int companyId) async {
|
Future<void> deleteCompany(int id) async {
|
||||||
try {
|
await ErrorHandler.handleApiCall<void>(
|
||||||
if (_useApi) {
|
() => _companyService.deleteCompany(id),
|
||||||
// API를 통한 삭제
|
onError: (failure) {
|
||||||
await _companyService.deleteCompany(companyId);
|
throw failure;
|
||||||
} else {
|
},
|
||||||
// Mock 데이터 삭제
|
);
|
||||||
dataService.deleteCompany(companyId);
|
|
||||||
}
|
removeItemLocally((c) => c.id == id);
|
||||||
|
selectedCompanyIds.remove(id);
|
||||||
// 로컬 리스트에서도 제거
|
|
||||||
companies.removeWhere((c) => c.id == companyId);
|
|
||||||
filteredCompanies.removeWhere((c) => c.id == companyId);
|
|
||||||
selectedCompanyIds.remove(companyId);
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} on Failure catch (e) {
|
|
||||||
_error = e.message;
|
|
||||||
notifyListeners();
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
_error = '회사 삭제 중 오류가 발생했습니다: $e';
|
|
||||||
notifyListeners();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 회사들 삭제
|
// 선택된 회사들 삭제
|
||||||
Future<bool> deleteSelectedCompanies() async {
|
Future<void> deleteSelectedCompanies() async {
|
||||||
final selectedIds = selectedCompanyIds.toList();
|
for (final id in selectedCompanyIds.toList()) {
|
||||||
int successCount = 0;
|
await deleteCompany(id);
|
||||||
|
|
||||||
for (final companyId in selectedIds) {
|
|
||||||
if (await deleteCompany(companyId)) {
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
clearSelection();
|
||||||
return successCount == selectedIds.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 회사 정보 업데이트 (로컬)
|
|
||||||
void updateCompanyLocally(Company updatedCompany) {
|
|
||||||
final index = companies.indexWhere((c) => c.id == updatedCompany.id);
|
|
||||||
if (index != -1) {
|
|
||||||
companies[index] = updatedCompany;
|
|
||||||
applyFilters();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 회사 추가 (로컬)
|
|
||||||
void addCompanyLocally(Company newCompany) {
|
|
||||||
companies.insert(0, newCompany);
|
|
||||||
applyFilters();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 더 많은 데이터 로드
|
|
||||||
Future<void> loadMore() async {
|
|
||||||
print('🔍 [DEBUG] loadMore 호출됨 - hasMore: $_hasMore, isLoading: $_isLoading, useApi: $_useApi');
|
|
||||||
if (!_hasMore || _isLoading || !_useApi) {
|
|
||||||
print('🔍 [DEBUG] loadMore 조건 미충족으로 종료 (hasMore: $_hasMore, isLoading: $_isLoading, useApi: $_useApi)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
print('🔍 [DEBUG] loadMore 실행 - 추가 데이터 로드 시작');
|
|
||||||
await loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 사용 여부 토글 (테스트용)
|
|
||||||
void toggleApiUsage() {
|
|
||||||
_useApi = !_useApi;
|
|
||||||
loadData(isRefresh: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 에러 처리
|
|
||||||
void clearError() {
|
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 리프레시
|
|
||||||
Future<void> refresh() async {
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 🔄 회사 목록 새로고침 시작');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
await loadData(isRefresh: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../core/controllers/base_list_controller.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../../../domain/usecases/base_usecase.dart';
|
||||||
|
import '../../../domain/usecases/company/company_usecases.dart';
|
||||||
|
import '../../../models/company_model.dart';
|
||||||
|
import '../../../services/company_service.dart';
|
||||||
|
import '../../../di/injection_container.dart';
|
||||||
|
import '../../../data/models/common/pagination_params.dart';
|
||||||
|
|
||||||
|
/// UseCase를 활용한 회사 목록 관리 컨트롤러
|
||||||
|
/// BaseListController를 상속받아 공통 기능 재사용
|
||||||
|
class CompanyListControllerWithUseCase extends BaseListController<Company> {
|
||||||
|
// UseCases
|
||||||
|
late final GetCompaniesUseCase _getCompaniesUseCase;
|
||||||
|
late final CreateCompanyUseCase _createCompanyUseCase;
|
||||||
|
late final UpdateCompanyUseCase _updateCompanyUseCase;
|
||||||
|
late final DeleteCompanyUseCase _deleteCompanyUseCase;
|
||||||
|
late final GetCompanyDetailUseCase _getCompanyDetailUseCase;
|
||||||
|
late final ToggleCompanyStatusUseCase _toggleCompanyStatusUseCase;
|
||||||
|
|
||||||
|
// 필터 상태
|
||||||
|
String? selectedType;
|
||||||
|
bool? isActive;
|
||||||
|
|
||||||
|
// 선택된 회사들
|
||||||
|
final Set<int> _selectedCompanyIds = {};
|
||||||
|
Set<int> get selectedCompanyIds => _selectedCompanyIds;
|
||||||
|
bool get hasSelection => _selectedCompanyIds.isNotEmpty;
|
||||||
|
|
||||||
|
CompanyListControllerWithUseCase() {
|
||||||
|
// UseCase 초기화
|
||||||
|
final companyService = inject<CompanyService>();
|
||||||
|
_getCompaniesUseCase = GetCompaniesUseCase(companyService);
|
||||||
|
_createCompanyUseCase = CreateCompanyUseCase(companyService);
|
||||||
|
_updateCompanyUseCase = UpdateCompanyUseCase(companyService);
|
||||||
|
_deleteCompanyUseCase = DeleteCompanyUseCase(companyService);
|
||||||
|
_getCompanyDetailUseCase = GetCompanyDetailUseCase(companyService);
|
||||||
|
_toggleCompanyStatusUseCase = ToggleCompanyStatusUseCase(companyService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PagedResult<Company>> fetchData({
|
||||||
|
required PaginationParams params,
|
||||||
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
}) async {
|
||||||
|
// UseCase를 통한 데이터 조회
|
||||||
|
final usecaseParams = GetCompaniesParams(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
search: params.search,
|
||||||
|
isActive: isActive,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await _getCompaniesUseCase(usecaseParams);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) {
|
||||||
|
throw Exception(failure.message);
|
||||||
|
},
|
||||||
|
(companies) {
|
||||||
|
// PagedResult로 래핑하여 반환 (임시로 메타데이터 생성)
|
||||||
|
final meta = PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: companies.length, // 실제로는 서버에서 받아와야 함
|
||||||
|
totalPages: (companies.length / params.perPage).ceil(),
|
||||||
|
hasNext: companies.length >= params.perPage,
|
||||||
|
hasPrevious: params.page > 1,
|
||||||
|
);
|
||||||
|
return PagedResult(items: companies, meta: meta);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 생성
|
||||||
|
Future<bool> createCompany(Company company) async {
|
||||||
|
isLoadingState = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final params = CreateCompanyParams(company: company);
|
||||||
|
final result = await _createCompanyUseCase(params);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) {
|
||||||
|
errorState = failure.message;
|
||||||
|
|
||||||
|
// ValidationFailure의 경우 상세 에러 표시
|
||||||
|
if (failure is ValidationFailure && failure.errors != null) {
|
||||||
|
final errorMessages = failure.errors!.entries
|
||||||
|
.map((e) => '${e.key}: ${e.value}')
|
||||||
|
.join('\n');
|
||||||
|
errorState = errorMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(newCompany) {
|
||||||
|
// 로컬 리스트에 추가
|
||||||
|
addItemLocally(newCompany);
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 수정
|
||||||
|
Future<bool> updateCompany(int id, Company company) async {
|
||||||
|
isLoadingState = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final params = UpdateCompanyParams(id: id, company: company);
|
||||||
|
final result = await _updateCompanyUseCase(params);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) {
|
||||||
|
errorState = failure.message;
|
||||||
|
|
||||||
|
// ValidationFailure의 경우 상세 에러 표시
|
||||||
|
if (failure is ValidationFailure && failure.errors != null) {
|
||||||
|
final errorMessages = failure.errors!.entries
|
||||||
|
.map((e) => '${e.key}: ${e.value}')
|
||||||
|
.join('\n');
|
||||||
|
errorState = errorMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(updatedCompany) {
|
||||||
|
// 로컬 리스트 업데이트
|
||||||
|
updateItemLocally(updatedCompany, (item) => item.id == id);
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 삭제
|
||||||
|
Future<bool> deleteCompany(int id) async {
|
||||||
|
isLoadingState = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final params = DeleteCompanyParams(id: id);
|
||||||
|
final result = await _deleteCompanyUseCase(params);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) {
|
||||||
|
errorState = failure.message;
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(_) {
|
||||||
|
// 로컬 리스트에서 제거
|
||||||
|
removeItemLocally((item) => item.id == id);
|
||||||
|
_selectedCompanyIds.remove(id);
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 상세 조회
|
||||||
|
Future<Company?> getCompanyDetail(int id, {bool includeBranches = false}) async {
|
||||||
|
final params = GetCompanyDetailParams(
|
||||||
|
id: id,
|
||||||
|
includeBranches: includeBranches,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await _getCompanyDetailUseCase(params);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) {
|
||||||
|
errorState = failure.message;
|
||||||
|
notifyListeners();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(company) => company,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 상태 토글 (활성화/비활성화)
|
||||||
|
Future<bool> toggleCompanyStatus(int id) async {
|
||||||
|
isLoadingState = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// 현재 회사 상태를 확인하여 토글 (기본값 true로 가정)
|
||||||
|
final params = ToggleCompanyStatusParams(
|
||||||
|
id: id,
|
||||||
|
isActive: false, // 임시로 false로 설정 (실제로는 현재 상태를 API로 확인해야 함)
|
||||||
|
);
|
||||||
|
final result = await _toggleCompanyStatusUseCase(params);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) {
|
||||||
|
errorState = failure.message;
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(_) {
|
||||||
|
// 로컬 리스트에서 상태 업데이트 (실제로는 API에서 업데이트된 Company 객체를 받아와야 함)
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 선택/해제
|
||||||
|
void toggleSelection(int companyId) {
|
||||||
|
if (_selectedCompanyIds.contains(companyId)) {
|
||||||
|
_selectedCompanyIds.remove(companyId);
|
||||||
|
} else {
|
||||||
|
_selectedCompanyIds.add(companyId);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전체 선택/해제
|
||||||
|
void toggleSelectAll() {
|
||||||
|
if (_selectedCompanyIds.length == items.length) {
|
||||||
|
_selectedCompanyIds.clear();
|
||||||
|
} else {
|
||||||
|
_selectedCompanyIds.clear();
|
||||||
|
_selectedCompanyIds.addAll(items.where((c) => c.id != null).map((c) => c.id!));
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택 초기화
|
||||||
|
void clearSelection() {
|
||||||
|
_selectedCompanyIds.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 적용
|
||||||
|
void applyFilters({String? type, bool? active}) {
|
||||||
|
selectedType = type;
|
||||||
|
isActive = active;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 초기화
|
||||||
|
void clearFilters() {
|
||||||
|
selectedType = null;
|
||||||
|
isActive = null;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 회사들 일괄 삭제
|
||||||
|
Future<bool> deleteSelectedCompanies() async {
|
||||||
|
if (_selectedCompanyIds.isEmpty) return false;
|
||||||
|
|
||||||
|
isLoadingState = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
bool allSuccess = true;
|
||||||
|
final failedIds = <int>[];
|
||||||
|
|
||||||
|
for (final id in _selectedCompanyIds.toList()) {
|
||||||
|
final params = DeleteCompanyParams(id: id);
|
||||||
|
final result = await _deleteCompanyUseCase(params);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
allSuccess = false;
|
||||||
|
failedIds.add(id);
|
||||||
|
debugPrint('회사 $id 삭제 실패: ${failure.message}');
|
||||||
|
},
|
||||||
|
(_) {
|
||||||
|
removeItemLocally((item) => item.id == id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedIds.isNotEmpty) {
|
||||||
|
errorState = '일부 회사 삭제 실패: ${failedIds.join(', ')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedCompanyIds.clear();
|
||||||
|
isLoadingState = false;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
return allSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import 'package:get_it/get_it.dart';
|
|||||||
import 'package:superport/models/equipment_unified_model.dart';
|
import 'package:superport/models/equipment_unified_model.dart';
|
||||||
import 'package:superport/models/company_model.dart';
|
import 'package:superport/models/company_model.dart';
|
||||||
import 'package:superport/services/equipment_service.dart';
|
import 'package:superport/services/equipment_service.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/services/warehouse_service.dart';
|
import 'package:superport/services/warehouse_service.dart';
|
||||||
import 'package:superport/services/company_service.dart';
|
import 'package:superport/services/company_service.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
@@ -14,7 +13,6 @@ import 'package:superport/core/utils/debug_logger.dart';
|
|||||||
///
|
///
|
||||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||||
class EquipmentInFormController extends ChangeNotifier {
|
class EquipmentInFormController extends ChangeNotifier {
|
||||||
final MockDataService dataService;
|
|
||||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||||
final WarehouseService _warehouseService = GetIt.instance<WarehouseService>();
|
final WarehouseService _warehouseService = GetIt.instance<WarehouseService>();
|
||||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||||
@@ -24,7 +22,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
bool _useApi = true; // Feature flag
|
// API만 사용
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
@@ -76,7 +74,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
|
|
||||||
final TextEditingController remarkController = TextEditingController();
|
final TextEditingController remarkController = TextEditingController();
|
||||||
|
|
||||||
EquipmentInFormController({required this.dataService, this.equipmentInId}) {
|
EquipmentInFormController({this.equipmentInId}) {
|
||||||
isEditMode = equipmentInId != null;
|
isEditMode = equipmentInId != null;
|
||||||
_loadManufacturers();
|
_loadManufacturers();
|
||||||
_loadEquipmentNames();
|
_loadEquipmentNames();
|
||||||
@@ -95,91 +93,71 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
await _loadEquipmentIn();
|
await _loadEquipmentIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 제조사 목록 로드
|
// 자동완성 데이터는 API를 통해 로드해야 하지만, 현재는 빈 목록으로 설정
|
||||||
void _loadManufacturers() {
|
void _loadManufacturers() {
|
||||||
manufacturers = dataService.getAllManufacturers();
|
// TODO: API를 통해 제조사 목록 로드
|
||||||
|
manufacturers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 장비명 목록 로드
|
|
||||||
void _loadEquipmentNames() {
|
void _loadEquipmentNames() {
|
||||||
equipmentNames = dataService.getAllEquipmentNames();
|
// TODO: API를 통해 장비명 목록 로드
|
||||||
|
equipmentNames = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 카테고리 목록 로드
|
|
||||||
void _loadCategories() {
|
void _loadCategories() {
|
||||||
categories = dataService.getAllCategories();
|
// TODO: API를 통해 카테고리 목록 로드
|
||||||
|
categories = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 서브카테고리 목록 로드
|
|
||||||
void _loadSubCategories() {
|
void _loadSubCategories() {
|
||||||
subCategories = dataService.getAllSubCategories();
|
// TODO: API를 통해 서브카테고리 목록 로드
|
||||||
|
subCategories = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 서브서브카테고리 목록 로드
|
|
||||||
void _loadSubSubCategories() {
|
void _loadSubSubCategories() {
|
||||||
subSubCategories = dataService.getAllSubSubCategories();
|
// TODO: API를 통해 서브서브카테고리 목록 로드
|
||||||
|
subSubCategories = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 입고지 목록 로드
|
// 입고지 목록 로드
|
||||||
void _loadWarehouseLocations() async {
|
void _loadWarehouseLocations() async {
|
||||||
if (_useApi) {
|
try {
|
||||||
try {
|
DebugLogger.log('입고지 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||||
DebugLogger.log('입고지 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
final locations = await _warehouseService.getWarehouseLocations();
|
||||||
final locations = await _warehouseService.getWarehouseLocations();
|
warehouseLocations = locations.map((e) => e.name).toList();
|
||||||
warehouseLocations = locations.map((e) => e.name).toList();
|
// 이름-ID 매핑 저장
|
||||||
// 이름-ID 매핑 저장
|
warehouseLocationMap = {for (var loc in locations) loc.name: loc.id};
|
||||||
warehouseLocationMap = {for (var loc in locations) loc.name: loc.id};
|
DebugLogger.log('입고지 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||||
DebugLogger.log('입고지 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
'count': warehouseLocations.length,
|
||||||
'count': warehouseLocations.length,
|
'locations': warehouseLocations,
|
||||||
'locations': warehouseLocations,
|
'locationMap': warehouseLocationMap,
|
||||||
'locationMap': warehouseLocationMap,
|
});
|
||||||
});
|
notifyListeners();
|
||||||
notifyListeners();
|
} catch (e) {
|
||||||
} catch (e) {
|
DebugLogger.logError('입고지 목록 로드 실패', error: e);
|
||||||
DebugLogger.logError('입고지 목록 로드 실패', error: e);
|
// API 실패 시 빈 목록
|
||||||
// 실패 시 Mock 데이터 사용
|
warehouseLocations = [];
|
||||||
final mockLocations = dataService.getAllWarehouseLocations();
|
warehouseLocationMap = {};
|
||||||
warehouseLocations = mockLocations.map((e) => e.name).toList();
|
notifyListeners();
|
||||||
warehouseLocationMap = {for (var loc in mockLocations) loc.name: loc.id};
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final mockLocations = dataService.getAllWarehouseLocations();
|
|
||||||
warehouseLocations = mockLocations.map((e) => e.name).toList();
|
|
||||||
warehouseLocationMap = {for (var loc in mockLocations) loc.name: loc.id};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파트너사 목록 로드
|
// 파트너사 목록 로드
|
||||||
void _loadPartnerCompanies() async {
|
void _loadPartnerCompanies() async {
|
||||||
if (_useApi) {
|
try {
|
||||||
try {
|
DebugLogger.log('파트너사 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
||||||
DebugLogger.log('파트너사 목록 API 로드 시작', tag: 'EQUIPMENT_IN');
|
final companies = await _companyService.getCompanies();
|
||||||
final companies = await _companyService.getCompanies();
|
partnerCompanies = companies.map((c) => c.name).toList();
|
||||||
partnerCompanies = companies.map((c) => c.name).toList();
|
DebugLogger.log('파트너사 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||||
DebugLogger.log('파트너사 목록 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
'count': partnerCompanies.length,
|
||||||
'count': partnerCompanies.length,
|
'companies': partnerCompanies,
|
||||||
'companies': partnerCompanies,
|
});
|
||||||
});
|
notifyListeners();
|
||||||
notifyListeners();
|
} catch (e) {
|
||||||
} catch (e) {
|
DebugLogger.logError('파트너사 목록 로드 실패', error: e);
|
||||||
DebugLogger.logError('파트너사 목록 로드 실패', error: e);
|
// API 실패 시 빈 목록
|
||||||
// 실패 시 Mock 데이터 사용
|
partnerCompanies = [];
|
||||||
partnerCompanies =
|
notifyListeners();
|
||||||
dataService
|
|
||||||
.getAllCompanies()
|
|
||||||
.where((c) => c.companyTypes.contains(CompanyType.partner))
|
|
||||||
.map((c) => c.name)
|
|
||||||
.toList();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
partnerCompanies =
|
|
||||||
dataService
|
|
||||||
.getAllCompanies()
|
|
||||||
.where((c) => c.companyTypes.contains(CompanyType.partner))
|
|
||||||
.map((c) => c.name)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,12 +176,11 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (_useApi) {
|
// equipmentInId는 실제로 장빔 ID임 (입고 ID가 아님)
|
||||||
// equipmentInId는 실제로 장비 ID임 (입고 ID가 아님)
|
actualEquipmentId = equipmentInId;
|
||||||
actualEquipmentId = equipmentInId;
|
|
||||||
|
try {
|
||||||
try {
|
// API에서 장비 정보 가져오기
|
||||||
// API에서 장비 정보 가져오기
|
|
||||||
DebugLogger.log('장비 정보 로드 시작', tag: 'EQUIPMENT_IN', data: {
|
DebugLogger.log('장비 정보 로드 시작', tag: 'EQUIPMENT_IN', data: {
|
||||||
'equipmentId': actualEquipmentId,
|
'equipmentId': actualEquipmentId,
|
||||||
});
|
});
|
||||||
@@ -238,25 +215,8 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.logError('장비 정보 로드 실패', error: e);
|
DebugLogger.logError('장비 정보 로드 실패', error: e);
|
||||||
// API 실패 시 Mock 데이터 시도
|
|
||||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
|
||||||
if (equipmentIn != null) {
|
|
||||||
actualEquipmentId = equipmentIn.equipment.id;
|
|
||||||
_loadFromMockData(equipmentIn);
|
|
||||||
} else {
|
|
||||||
throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Mock 데이터 사용
|
|
||||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
|
||||||
if (equipmentIn != null) {
|
|
||||||
actualEquipmentId = equipmentIn.equipment.id;
|
|
||||||
_loadFromMockData(equipmentIn);
|
|
||||||
} else {
|
|
||||||
throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.');
|
throw ServerFailure(message: '장비 정보를 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = '장비 정보를 불러오는데 실패했습니다: $e';
|
_error = '장비 정보를 불러오는데 실패했습니다: $e';
|
||||||
DebugLogger.logError('장비 로드 실패', error: e);
|
DebugLogger.logError('장비 로드 실패', error: e);
|
||||||
@@ -266,28 +226,6 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadFromMockData(EquipmentIn equipmentIn) {
|
|
||||||
manufacturer = equipmentIn.equipment.manufacturer;
|
|
||||||
name = equipmentIn.equipment.name;
|
|
||||||
category = equipmentIn.equipment.category;
|
|
||||||
subCategory = equipmentIn.equipment.subCategory;
|
|
||||||
subSubCategory = equipmentIn.equipment.subSubCategory;
|
|
||||||
serialNumber = equipmentIn.equipment.serialNumber ?? '';
|
|
||||||
barcode = equipmentIn.equipment.barcode ?? '';
|
|
||||||
quantity = equipmentIn.equipment.quantity;
|
|
||||||
inDate = equipmentIn.inDate;
|
|
||||||
hasSerialNumber = serialNumber.isNotEmpty;
|
|
||||||
equipmentType = equipmentIn.type;
|
|
||||||
warehouseLocation = equipmentIn.warehouseLocation;
|
|
||||||
partnerCompany = equipmentIn.partnerCompany;
|
|
||||||
remarkController.text = equipmentIn.remark ?? '';
|
|
||||||
|
|
||||||
// 워런티 정보 로드
|
|
||||||
warrantyLicense = equipmentIn.partnerCompany;
|
|
||||||
warrantyStartDate = equipmentIn.inDate;
|
|
||||||
warrantyEndDate = equipmentIn.inDate.add(const Duration(days: 365));
|
|
||||||
warrantyCode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 워런티 기간 계산
|
// 워런티 기간 계산
|
||||||
String getWarrantyPeriodSummary() {
|
String getWarrantyPeriodSummary() {
|
||||||
@@ -374,9 +312,8 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
// 워런티 코드 저장 필요시 여기에 추가
|
// 워런티 코드 저장 필요시 여기에 추가
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_useApi) {
|
// API 호출
|
||||||
// API 호출
|
if (isEditMode) {
|
||||||
if (isEditMode) {
|
|
||||||
// 수정 모드 - API로 장비 정보 업데이트
|
// 수정 모드 - API로 장비 정보 업데이트
|
||||||
if (actualEquipmentId == null) {
|
if (actualEquipmentId == null) {
|
||||||
throw ServerFailure(message: '장비 ID가 없습니다.');
|
throw ServerFailure(message: '장비 ID가 없습니다.');
|
||||||
@@ -437,35 +374,6 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
throw e; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
|
throw e; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Mock 데이터 사용
|
|
||||||
if (isEditMode) {
|
|
||||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
|
||||||
if (equipmentIn != null) {
|
|
||||||
final updatedEquipmentIn = EquipmentIn(
|
|
||||||
id: equipmentIn.id,
|
|
||||||
equipment: equipment,
|
|
||||||
inDate: inDate,
|
|
||||||
status: equipmentIn.status,
|
|
||||||
type: equipmentType,
|
|
||||||
warehouseLocation: warehouseLocation,
|
|
||||||
partnerCompany: partnerCompany,
|
|
||||||
remark: remarkController.text.trim(),
|
|
||||||
);
|
|
||||||
dataService.updateEquipmentIn(updatedEquipmentIn);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final newEquipmentIn = EquipmentIn(
|
|
||||||
equipment: equipment,
|
|
||||||
inDate: inDate,
|
|
||||||
type: equipmentType,
|
|
||||||
warehouseLocation: warehouseLocation,
|
|
||||||
partnerCompany: partnerCompany,
|
|
||||||
remark: remarkController.text.trim(),
|
|
||||||
);
|
|
||||||
dataService.addEquipmentIn(newEquipmentIn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 저장 후 리스트 재로딩 (중복 방지 및 최신화)
|
// 저장 후 리스트 재로딩 (중복 방지 및 최신화)
|
||||||
_loadManufacturers();
|
_loadManufacturers();
|
||||||
@@ -498,11 +406,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 사용 여부 토글 (테스트용)
|
// API만 사용하므로 토글 기능 제거
|
||||||
void toggleApiUsage() {
|
|
||||||
_useApi = !_useApi;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:superport/models/equipment_unified_model.dart';
|
||||||
|
import 'package:superport/services/equipment_service.dart';
|
||||||
|
import 'package:superport/utils/constants.dart';
|
||||||
|
import 'package:superport/core/errors/failures.dart';
|
||||||
|
import 'package:superport/models/equipment_unified_model.dart' as legacy;
|
||||||
|
import 'package:superport/core/utils/debug_logger.dart';
|
||||||
|
|
||||||
|
// companyTypeToString 함수 import
|
||||||
|
import 'package:superport/utils/constants.dart'
|
||||||
|
show companyTypeToString, CompanyType;
|
||||||
|
import 'package:superport/models/company_model.dart';
|
||||||
|
import 'package:superport/models/address_model.dart';
|
||||||
|
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||||
|
|
||||||
|
// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||||
|
class EquipmentListController extends ChangeNotifier {
|
||||||
|
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||||
|
|
||||||
|
List<UnifiedEquipment> equipments = [];
|
||||||
|
String? selectedStatusFilter;
|
||||||
|
String searchKeyword = ''; // 검색어 추가
|
||||||
|
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
// API만 사용
|
||||||
|
|
||||||
|
// 페이지네이션
|
||||||
|
int _currentPage = 1;
|
||||||
|
final int _perPage = 20;
|
||||||
|
bool _hasMore = true;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
String? get error => _error;
|
||||||
|
bool get hasMore => _hasMore;
|
||||||
|
int get currentPage => _currentPage;
|
||||||
|
|
||||||
|
EquipmentListController();
|
||||||
|
|
||||||
|
// 데이터 로드 및 상태 필터 적용
|
||||||
|
Future<void> loadData({bool isRefresh = false, String? search}) async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API 호출 - 전체 데이터 로드
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 📦 장비 목록 API 호출 시작');
|
||||||
|
print('║ • 상태 필터: ${selectedStatusFilter ?? "전체"}');
|
||||||
|
print('║ • 검색어: ${search ?? searchKeyword}');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||||
|
final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus(
|
||||||
|
page: 1,
|
||||||
|
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||||
|
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
|
||||||
|
search: search ?? searchKeyword,
|
||||||
|
);
|
||||||
|
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 📊 장비 목록 로드 완료');
|
||||||
|
print('║ ▶ 총 장비 수: ${apiEquipmentDtos.length}개');
|
||||||
|
print('╟──────────────────────────────────────────────────────────');
|
||||||
|
|
||||||
|
// 상태별 통계
|
||||||
|
Map<String, int> statusCount = {};
|
||||||
|
for (final dto in apiEquipmentDtos) {
|
||||||
|
final clientStatus = EquipmentStatusConverter.serverToClient(dto.status);
|
||||||
|
statusCount[clientStatus] = (statusCount[clientStatus] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCount.forEach((status, count) {
|
||||||
|
print('║ • $status: $count개');
|
||||||
|
});
|
||||||
|
|
||||||
|
print('╟──────────────────────────────────────────────────────────');
|
||||||
|
print('║ 📑 전체 데이터 로드 완료');
|
||||||
|
print('║ • View에서 페이지네이션 처리 예정');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
// DTO를 UnifiedEquipment로 변환 (status 정보 포함)
|
||||||
|
final List<UnifiedEquipment> unifiedEquipments = apiEquipmentDtos.map((dto) {
|
||||||
|
final equipment = Equipment(
|
||||||
|
id: dto.id,
|
||||||
|
manufacturer: dto.manufacturer,
|
||||||
|
name: dto.modelName ?? dto.equipmentNumber,
|
||||||
|
category: '', // 세부 정보는 상세 조회에서 가져와야 함
|
||||||
|
subCategory: '',
|
||||||
|
subSubCategory: '',
|
||||||
|
serialNumber: dto.serialNumber,
|
||||||
|
quantity: 1,
|
||||||
|
inDate: dto.createdAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
return UnifiedEquipment(
|
||||||
|
id: dto.id,
|
||||||
|
equipment: equipment,
|
||||||
|
date: dto.createdAt,
|
||||||
|
status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
equipments = unifiedEquipments;
|
||||||
|
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||||
|
|
||||||
|
selectedEquipmentIds.clear();
|
||||||
|
} on Failure catch (e) {
|
||||||
|
_error = e.message;
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'An unexpected error occurred: $e';
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태 필터 변경
|
||||||
|
Future<void> changeStatusFilter(String? status) async {
|
||||||
|
selectedStatusFilter = status;
|
||||||
|
await loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색어 변경
|
||||||
|
Future<void> updateSearchKeyword(String keyword) async {
|
||||||
|
searchKeyword = keyword;
|
||||||
|
await loadData(isRefresh: true, search: keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 장비 선택/해제 (모든 상태 지원)
|
||||||
|
void selectEquipment(int? id, String status, bool? isSelected) {
|
||||||
|
if (id == null || isSelected == null) return;
|
||||||
|
final key = '$id:$status';
|
||||||
|
if (isSelected) {
|
||||||
|
selectedEquipmentIds.add(key);
|
||||||
|
} else {
|
||||||
|
selectedEquipmentIds.remove(key);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 입고 장비 수 반환
|
||||||
|
int getSelectedInStockCount() {
|
||||||
|
int count = 0;
|
||||||
|
for (final idStatusPair in selectedEquipmentIds) {
|
||||||
|
final parts = idStatusPair.split(':');
|
||||||
|
if (parts.length == 2 && parts[1] == EquipmentStatus.in_) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 전체 장비 수 반환
|
||||||
|
int getSelectedEquipmentCount() {
|
||||||
|
return selectedEquipmentIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 특정 상태의 장비 수 반환
|
||||||
|
int getSelectedEquipmentCountByStatus(String status) {
|
||||||
|
int count = 0;
|
||||||
|
for (final idStatusPair in selectedEquipmentIds) {
|
||||||
|
final parts = idStatusPair.split(':');
|
||||||
|
if (parts.length == 2 && parts[1] == status) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 장비들의 UnifiedEquipment 객체 목록 반환
|
||||||
|
List<UnifiedEquipment> getSelectedEquipments() {
|
||||||
|
List<UnifiedEquipment> selected = [];
|
||||||
|
for (final idStatusPair in selectedEquipmentIds) {
|
||||||
|
final parts = idStatusPair.split(':');
|
||||||
|
if (parts.length == 2) {
|
||||||
|
final id = int.tryParse(parts[0]);
|
||||||
|
if (id != null) {
|
||||||
|
final equipment = equipments.firstWhere(
|
||||||
|
(e) => e.id == id && e.status == parts[1],
|
||||||
|
orElse: () => null as UnifiedEquipment,
|
||||||
|
);
|
||||||
|
if (equipment != null) {
|
||||||
|
selected.add(equipment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환
|
||||||
|
List<UnifiedEquipment> getSelectedEquipmentsByStatus(String status) {
|
||||||
|
List<UnifiedEquipment> selected = [];
|
||||||
|
for (final idStatusPair in selectedEquipmentIds) {
|
||||||
|
final parts = idStatusPair.split(':');
|
||||||
|
if (parts.length == 2 && parts[1] == status) {
|
||||||
|
final id = int.tryParse(parts[0]);
|
||||||
|
if (id != null) {
|
||||||
|
final equipment = equipments.firstWhere(
|
||||||
|
(e) => e.id == id && e.status == status,
|
||||||
|
orElse: () => null as UnifiedEquipment,
|
||||||
|
);
|
||||||
|
if (equipment != null) {
|
||||||
|
selected.add(equipment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용)
|
||||||
|
List<Map<String, dynamic>> getSelectedEquipmentsSummary() {
|
||||||
|
List<Map<String, dynamic>> summaryList = [];
|
||||||
|
List<UnifiedEquipment> selectedEquipmentsInStock =
|
||||||
|
getSelectedEquipmentsByStatus(EquipmentStatus.in_);
|
||||||
|
|
||||||
|
for (final equipment in selectedEquipmentsInStock) {
|
||||||
|
summaryList.add({
|
||||||
|
'equipment': equipment.equipment,
|
||||||
|
'equipmentInId': equipment.id,
|
||||||
|
'status': equipment.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaryList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 출고 정보(회사, 담당자, 라이센스 등) 반환
|
||||||
|
// 출고 정보는 API를 통해 번별로 조회해야 하므로 별도 서비스로 분리 예정
|
||||||
|
String getOutEquipmentInfo(int equipmentId, String infoType) {
|
||||||
|
// TODO: API로 출고 정보 조회 구현
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 장비 삭제
|
||||||
|
Future<bool> deleteEquipment(UnifiedEquipment equipment) async {
|
||||||
|
try {
|
||||||
|
// API를 통한 삭제
|
||||||
|
if (equipment.equipment.id != null) {
|
||||||
|
await _equipmentService.deleteEquipment(equipment.equipment.id!);
|
||||||
|
} else {
|
||||||
|
throw Exception('Equipment ID is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로컬 리스트에서도 제거
|
||||||
|
equipments.removeWhere((e) => e.id == equipment.id && e.status == equipment.status);
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} on Failure catch (e) {
|
||||||
|
_error = e.message;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Failed to delete equipment: $e';
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API만 사용하므로 토글 기능 제거
|
||||||
|
|
||||||
|
// 에러 처리
|
||||||
|
void clearError() {
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,338 +1,315 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:superport/models/equipment_unified_model.dart';
|
import 'package:superport/models/equipment_unified_model.dart';
|
||||||
import 'package:superport/services/equipment_service.dart';
|
import 'package:superport/services/equipment_service.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
import 'package:superport/core/utils/error_handler.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||||
import 'package:superport/core/errors/failures.dart';
|
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||||
import 'package:superport/models/equipment_unified_model.dart' as legacy;
|
|
||||||
import 'package:superport/core/utils/debug_logger.dart';
|
|
||||||
|
|
||||||
// companyTypeToString 함수 import
|
|
||||||
import 'package:superport/utils/constants.dart'
|
|
||||||
show companyTypeToString, CompanyType;
|
|
||||||
import 'package:superport/models/company_model.dart';
|
import 'package:superport/models/company_model.dart';
|
||||||
import 'package:superport/models/address_model.dart';
|
import 'package:superport/models/address_model.dart';
|
||||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
import 'package:superport/data/models/common/pagination_params.dart';
|
||||||
|
|
||||||
// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
/// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||||
class EquipmentListController extends ChangeNotifier {
|
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||||
final MockDataService dataService;
|
class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
late final EquipmentService _equipmentService;
|
||||||
|
|
||||||
List<UnifiedEquipment> equipments = [];
|
// 추가 상태 관리
|
||||||
String? selectedStatusFilter;
|
|
||||||
String searchKeyword = ''; // 검색어 추가
|
|
||||||
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||||
|
|
||||||
bool _isLoading = false;
|
// 필터
|
||||||
String? _error;
|
String? _statusFilter;
|
||||||
bool _useApi = true; // Feature flag for API usage
|
String? _categoryFilter;
|
||||||
|
int? _companyIdFilter;
|
||||||
// 페이지네이션
|
String? _selectedStatusFilter;
|
||||||
int _currentPage = 1;
|
|
||||||
final int _perPage = 20;
|
|
||||||
bool _hasMore = true;
|
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
bool get isLoading => _isLoading;
|
List<UnifiedEquipment> get equipments => items;
|
||||||
String? get error => _error;
|
String? get statusFilter => _statusFilter;
|
||||||
bool get hasMore => _hasMore;
|
String? get categoryFilter => _categoryFilter;
|
||||||
int get currentPage => _currentPage;
|
int? get companyIdFilter => _companyIdFilter;
|
||||||
|
String? get selectedStatusFilter => _selectedStatusFilter;
|
||||||
EquipmentListController({required this.dataService});
|
|
||||||
|
|
||||||
// 데이터 로드 및 상태 필터 적용
|
|
||||||
Future<void> loadData({bool isRefresh = false, String? search}) async {
|
|
||||||
if (_isLoading) return;
|
|
||||||
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (_useApi) {
|
|
||||||
// API 호출 - 전체 데이터 로드
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 📦 장비 목록 API 호출 시작');
|
|
||||||
print('║ • 상태 필터: ${selectedStatusFilter ?? "전체"}');
|
|
||||||
print('║ • 검색어: ${search ?? searchKeyword}');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
|
||||||
final apiEquipmentDtos = await _equipmentService.getEquipmentsWithStatus(
|
|
||||||
page: 1,
|
|
||||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
|
||||||
status: selectedStatusFilter != null ? EquipmentStatusConverter.clientToServer(selectedStatusFilter) : null,
|
|
||||||
search: search ?? searchKeyword,
|
|
||||||
);
|
|
||||||
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 📊 장비 목록 로드 완료');
|
|
||||||
print('║ ▶ 총 장비 수: ${apiEquipmentDtos.length}개');
|
|
||||||
print('╟──────────────────────────────────────────────────────────');
|
|
||||||
|
|
||||||
// 상태별 통계
|
|
||||||
Map<String, int> statusCount = {};
|
|
||||||
for (final dto in apiEquipmentDtos) {
|
|
||||||
final clientStatus = EquipmentStatusConverter.serverToClient(dto.status);
|
|
||||||
statusCount[clientStatus] = (statusCount[clientStatus] ?? 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
statusCount.forEach((status, count) {
|
|
||||||
print('║ • $status: $count개');
|
|
||||||
});
|
|
||||||
|
|
||||||
print('╟──────────────────────────────────────────────────────────');
|
|
||||||
print('║ 📑 전체 데이터 로드 완료');
|
|
||||||
print('║ • View에서 페이지네이션 처리 예정');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
// DTO를 UnifiedEquipment로 변환 (status 정보 포함)
|
|
||||||
final List<UnifiedEquipment> unifiedEquipments = apiEquipmentDtos.map((dto) {
|
|
||||||
final equipment = Equipment(
|
|
||||||
id: dto.id,
|
|
||||||
manufacturer: dto.manufacturer,
|
|
||||||
name: dto.modelName ?? dto.equipmentNumber,
|
|
||||||
category: '', // 세부 정보는 상세 조회에서 가져와야 함
|
|
||||||
subCategory: '',
|
|
||||||
subSubCategory: '',
|
|
||||||
serialNumber: dto.serialNumber,
|
|
||||||
quantity: 1,
|
|
||||||
inDate: dto.createdAt,
|
|
||||||
);
|
|
||||||
|
|
||||||
return UnifiedEquipment(
|
|
||||||
id: dto.id,
|
|
||||||
equipment: equipment,
|
|
||||||
date: dto.createdAt,
|
|
||||||
status: EquipmentStatusConverter.serverToClient(dto.status), // 서버 status를 클라이언트 status로 변환
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
equipments = unifiedEquipments;
|
|
||||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
|
||||||
} else {
|
|
||||||
// Mock 데이터 사용
|
|
||||||
equipments = dataService.getAllEquipments();
|
|
||||||
if (selectedStatusFilter != null) {
|
|
||||||
equipments =
|
|
||||||
equipments.where((e) => e.status == selectedStatusFilter).toList();
|
|
||||||
}
|
|
||||||
_hasMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedEquipmentIds.clear();
|
|
||||||
} on Failure catch (e) {
|
|
||||||
_error = e.message;
|
|
||||||
} catch (e) {
|
|
||||||
_error = 'An unexpected error occurred: $e';
|
|
||||||
} finally {
|
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상태 필터 변경
|
|
||||||
Future<void> changeStatusFilter(String? status) async {
|
|
||||||
selectedStatusFilter = status;
|
|
||||||
await loadData(isRefresh: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색어 변경
|
// Setters
|
||||||
Future<void> updateSearchKeyword(String keyword) async {
|
set selectedStatusFilter(String? value) {
|
||||||
searchKeyword = keyword;
|
_selectedStatusFilter = value;
|
||||||
await loadData(isRefresh: true, search: keyword);
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 장비 선택/해제 (모든 상태 지원)
|
EquipmentListController() {
|
||||||
void selectEquipment(int? id, String status, bool? isSelected) {
|
if (GetIt.instance.isRegistered<EquipmentService>()) {
|
||||||
if (id == null || isSelected == null) return;
|
_equipmentService = GetIt.instance<EquipmentService>();
|
||||||
final key = '$id:$status';
|
|
||||||
if (isSelected) {
|
|
||||||
selectedEquipmentIds.add(key);
|
|
||||||
} else {
|
} else {
|
||||||
selectedEquipmentIds.remove(key);
|
throw Exception('EquipmentService not registered in GetIt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PagedResult<UnifiedEquipment>> fetchData({
|
||||||
|
required PaginationParams params,
|
||||||
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
}) async {
|
||||||
|
// API 호출
|
||||||
|
final apiEquipmentDtos = await ErrorHandler.handleApiCall(
|
||||||
|
() => _equipmentService.getEquipmentsWithStatus(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
status: _statusFilter != null ?
|
||||||
|
EquipmentStatusConverter.clientToServer(_statusFilter) : null,
|
||||||
|
search: params.search,
|
||||||
|
companyId: _companyIdFilter,
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (apiEquipmentDtos == null) {
|
||||||
|
return PagedResult(
|
||||||
|
items: [],
|
||||||
|
meta: PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrevious: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO를 UnifiedEquipment로 변환
|
||||||
|
final items = apiEquipmentDtos.map((dto) {
|
||||||
|
final equipment = Equipment(
|
||||||
|
id: dto.id,
|
||||||
|
manufacturer: dto.manufacturer ?? 'Unknown',
|
||||||
|
name: dto.modelName ?? dto.equipmentNumber ?? 'Unknown',
|
||||||
|
category: 'Equipment', // 임시 카테고리
|
||||||
|
subCategory: 'General', // 임시 서브카테고리
|
||||||
|
subSubCategory: 'Standard', // 임시 서브서브카테고리
|
||||||
|
serialNumber: dto.serialNumber,
|
||||||
|
quantity: 1, // 기본 수량
|
||||||
|
);
|
||||||
|
|
||||||
|
// 간단한 Company 정보 생성 (사용하지 않으므로 제거)
|
||||||
|
// final company = dto.companyName != null ? ... : null;
|
||||||
|
|
||||||
|
return UnifiedEquipment(
|
||||||
|
id: dto.id,
|
||||||
|
equipment: equipment,
|
||||||
|
date: dto.createdAt ?? DateTime.now(),
|
||||||
|
status: EquipmentStatusConverter.serverToClient(dto.status),
|
||||||
|
notes: null, // EquipmentListDto에 remark 필드 없음
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||||
|
final meta = PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: items.length < params.perPage ?
|
||||||
|
(params.page - 1) * params.perPage + items.length :
|
||||||
|
params.page * params.perPage + 1,
|
||||||
|
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||||
|
hasNext: items.length >= params.perPage,
|
||||||
|
hasPrevious: params.page > 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return PagedResult(items: items, meta: meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filterItem(UnifiedEquipment item, String query) {
|
||||||
|
final q = query.toLowerCase();
|
||||||
|
return (item.equipment.name.toLowerCase().contains(q)) ||
|
||||||
|
(item.equipment.serialNumber?.toLowerCase().contains(q) ?? false) ||
|
||||||
|
(item.equipment.manufacturer.toLowerCase().contains(q)) ||
|
||||||
|
(item.notes?.toLowerCase().contains(q) ?? false) ||
|
||||||
|
(item.status.toLowerCase().contains(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 선택/선택 해제
|
||||||
|
void toggleSelection(UnifiedEquipment equipment) {
|
||||||
|
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||||
|
if (selectedEquipmentIds.contains(equipmentKey)) {
|
||||||
|
selectedEquipmentIds.remove(equipmentKey);
|
||||||
|
} else {
|
||||||
|
selectedEquipmentIds.add(equipmentKey);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 입고 장비 수 반환
|
/// 모든 선택 해제
|
||||||
int getSelectedInStockCount() {
|
void clearSelection() {
|
||||||
int count = 0;
|
selectedEquipmentIds.clear();
|
||||||
for (final idStatusPair in selectedEquipmentIds) {
|
notifyListeners();
|
||||||
final parts = idStatusPair.split(':');
|
|
||||||
if (parts.length == 2 && parts[1] == EquipmentStatus.in_) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 전체 장비 수 반환
|
/// 선택된 장비 정보 가져오기
|
||||||
|
Map<String, List<UnifiedEquipment>> getSelectedEquipmentsByStatus() {
|
||||||
|
final Map<String, List<UnifiedEquipment>> groupedEquipments = {};
|
||||||
|
|
||||||
|
for (final equipment in items) {
|
||||||
|
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||||
|
if (selectedEquipmentIds.contains(equipmentKey)) {
|
||||||
|
if (!groupedEquipments.containsKey(equipment.status)) {
|
||||||
|
groupedEquipments[equipment.status] = [];
|
||||||
|
}
|
||||||
|
groupedEquipments[equipment.status]!.add(equipment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupedEquipments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 설정
|
||||||
|
void setFilters({
|
||||||
|
String? status,
|
||||||
|
String? category,
|
||||||
|
int? companyId,
|
||||||
|
}) {
|
||||||
|
_statusFilter = status;
|
||||||
|
_categoryFilter = category;
|
||||||
|
_companyIdFilter = companyId;
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 상태 필터 변경
|
||||||
|
void filterByStatus(String? status) {
|
||||||
|
_statusFilter = status;
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 카테고리 필터 변경
|
||||||
|
void filterByCategory(String? category) {
|
||||||
|
_categoryFilter = category;
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회사 필터 변경
|
||||||
|
void filterByCompany(int? companyId) {
|
||||||
|
_companyIdFilter = companyId;
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 초기화
|
||||||
|
void clearFilters() {
|
||||||
|
_statusFilter = null;
|
||||||
|
_categoryFilter = null;
|
||||||
|
_companyIdFilter = null;
|
||||||
|
search('');
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 삭제
|
||||||
|
Future<void> deleteEquipment(int id, String status) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _equipmentService.deleteEquipment(id),
|
||||||
|
);
|
||||||
|
|
||||||
|
removeItemLocally((e) => e.equipment.id == id && e.status == status);
|
||||||
|
|
||||||
|
// 선택 목록에서도 제거
|
||||||
|
final equipmentKey = '$id:$status';
|
||||||
|
selectedEquipmentIds.remove(equipmentKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 장비 일괄 삭제
|
||||||
|
Future<void> deleteSelectedEquipments() async {
|
||||||
|
final selectedGroups = getSelectedEquipmentsByStatus();
|
||||||
|
|
||||||
|
for (final entry in selectedGroups.entries) {
|
||||||
|
for (final equipment in entry.value) {
|
||||||
|
if (equipment.equipment.id != null) {
|
||||||
|
await deleteEquipment(equipment.equipment.id!, equipment.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 상태 변경 (임시 구현 - API가 지원하지 않음)
|
||||||
|
Future<void> updateEquipmentStatus(int id, String currentStatus, String newStatus) async {
|
||||||
|
debugPrint('장비 상태 변경: $id, $currentStatus -> $newStatus');
|
||||||
|
// TODO: 실제 API가 장비 상태 변경을 지원할 때 구현
|
||||||
|
// 현재는 새로고침만 수행
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 정보 수정
|
||||||
|
Future<void> updateEquipment(int id, UnifiedEquipment equipment) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _equipmentService.updateEquipment(id, equipment.equipment),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
updateItemLocally(equipment, (e) =>
|
||||||
|
e.equipment.id == equipment.equipment.id &&
|
||||||
|
e.status == equipment.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 상태 필터 변경
|
||||||
|
void changeStatusFilter(String? status) {
|
||||||
|
_selectedStatusFilter = status;
|
||||||
|
_statusFilter = status;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 검색 키워드 업데이트
|
||||||
|
void updateSearchKeyword(String keyword) {
|
||||||
|
search(keyword); // BaseListController의 search 메서드 사용
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 선택 (토글 선택을 위한 별칭)
|
||||||
|
void selectEquipment(UnifiedEquipment equipment) {
|
||||||
|
toggleSelection(equipment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 입고 상태 장비 개수
|
||||||
|
int getSelectedInStockCount() {
|
||||||
|
return selectedEquipmentIds
|
||||||
|
.where((key) => key.endsWith(':입고'))
|
||||||
|
.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 장비들 가져오기
|
||||||
|
List<UnifiedEquipment> getSelectedEquipments() {
|
||||||
|
return items.where((equipment) {
|
||||||
|
final equipmentKey = '${equipment.equipment.id}:${equipment.status}';
|
||||||
|
return selectedEquipmentIds.contains(equipmentKey);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 장비들 요약 정보
|
||||||
|
String getSelectedEquipmentsSummary() {
|
||||||
|
final selectedEquipments = getSelectedEquipments();
|
||||||
|
if (selectedEquipments.isEmpty) return '선택된 장비가 없습니다';
|
||||||
|
|
||||||
|
final Map<String, int> statusCounts = {};
|
||||||
|
for (final equipment in selectedEquipments) {
|
||||||
|
statusCounts[equipment.status] = (statusCounts[equipment.status] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final summaryParts = statusCounts.entries
|
||||||
|
.map((entry) => '${entry.key}: ${entry.value}개')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return summaryParts.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 장비 총 개수
|
||||||
int getSelectedEquipmentCount() {
|
int getSelectedEquipmentCount() {
|
||||||
return selectedEquipmentIds.length;
|
return selectedEquipmentIds.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 특정 상태의 장비 수 반환
|
/// 특정 상태의 선택된 장비 개수
|
||||||
int getSelectedEquipmentCountByStatus(String status) {
|
int getSelectedEquipmentCountByStatus(String status) {
|
||||||
int count = 0;
|
return selectedEquipmentIds
|
||||||
for (final idStatusPair in selectedEquipmentIds) {
|
.where((key) => key.endsWith(':$status'))
|
||||||
final parts = idStatusPair.split(':');
|
.length;
|
||||||
if (parts.length == 2 && parts[1] == status) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 선택된 장비들의 UnifiedEquipment 객체 목록 반환
|
|
||||||
List<UnifiedEquipment> getSelectedEquipments() {
|
|
||||||
List<UnifiedEquipment> selected = [];
|
|
||||||
for (final idStatusPair in selectedEquipmentIds) {
|
|
||||||
final parts = idStatusPair.split(':');
|
|
||||||
if (parts.length == 2) {
|
|
||||||
final id = int.tryParse(parts[0]);
|
|
||||||
if (id != null) {
|
|
||||||
final equipment = equipments.firstWhere(
|
|
||||||
(e) => e.id == id && e.status == parts[1],
|
|
||||||
orElse: () => null as UnifiedEquipment,
|
|
||||||
);
|
|
||||||
if (equipment != null) {
|
|
||||||
selected.add(equipment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환
|
|
||||||
List<UnifiedEquipment> getSelectedEquipmentsByStatus(String status) {
|
|
||||||
List<UnifiedEquipment> selected = [];
|
|
||||||
for (final idStatusPair in selectedEquipmentIds) {
|
|
||||||
final parts = idStatusPair.split(':');
|
|
||||||
if (parts.length == 2 && parts[1] == status) {
|
|
||||||
final id = int.tryParse(parts[0]);
|
|
||||||
if (id != null) {
|
|
||||||
final equipment = equipments.firstWhere(
|
|
||||||
(e) => e.id == id && e.status == status,
|
|
||||||
orElse: () => null as UnifiedEquipment,
|
|
||||||
);
|
|
||||||
if (equipment != null) {
|
|
||||||
selected.add(equipment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용)
|
|
||||||
List<Map<String, dynamic>> getSelectedEquipmentsSummary() {
|
|
||||||
List<Map<String, dynamic>> summaryList = [];
|
|
||||||
List<UnifiedEquipment> selectedEquipmentsInStock =
|
|
||||||
getSelectedEquipmentsByStatus(EquipmentStatus.in_);
|
|
||||||
|
|
||||||
for (final equipment in selectedEquipmentsInStock) {
|
|
||||||
summaryList.add({
|
|
||||||
'equipment': equipment.equipment,
|
|
||||||
'equipmentInId': equipment.id,
|
|
||||||
'status': equipment.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return summaryList;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 출고 정보(회사, 담당자, 라이센스 등) 반환
|
|
||||||
String getOutEquipmentInfo(int equipmentId, String infoType) {
|
|
||||||
final equipmentOut = dataService.getEquipmentOutById(equipmentId);
|
|
||||||
if (equipmentOut != null) {
|
|
||||||
switch (infoType) {
|
|
||||||
case 'company':
|
|
||||||
final company = equipmentOut.company ?? '-';
|
|
||||||
if (company != '-') {
|
|
||||||
final companyObj = dataService.getAllCompanies().firstWhere(
|
|
||||||
(c) => c.name == company,
|
|
||||||
orElse:
|
|
||||||
() => Company(
|
|
||||||
name: company,
|
|
||||||
address: Address(),
|
|
||||||
companyTypes: [CompanyType.customer], // 기본값 고객사
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// 여러 유형 중 첫 번째만 표시 (대표 유형)
|
|
||||||
final typeText =
|
|
||||||
companyObj.companyTypes.isNotEmpty
|
|
||||||
? companyTypeToString(companyObj.companyTypes.first)
|
|
||||||
: '-';
|
|
||||||
return '$company (${typeText})';
|
|
||||||
}
|
|
||||||
return company;
|
|
||||||
case 'manager':
|
|
||||||
return equipmentOut.manager ?? '-';
|
|
||||||
case 'license':
|
|
||||||
return equipmentOut.license ?? '-';
|
|
||||||
default:
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 장비 삭제
|
|
||||||
Future<bool> deleteEquipment(UnifiedEquipment equipment) async {
|
|
||||||
try {
|
|
||||||
if (_useApi) {
|
|
||||||
// API를 통한 삭제
|
|
||||||
if (equipment.equipment.id != null) {
|
|
||||||
await _equipmentService.deleteEquipment(equipment.equipment.id!);
|
|
||||||
} else {
|
|
||||||
throw Exception('Equipment ID is null');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Mock 데이터 삭제
|
|
||||||
if (equipment.status == EquipmentStatus.in_) {
|
|
||||||
dataService.deleteEquipmentIn(equipment.id!);
|
|
||||||
} else if (equipment.status == EquipmentStatus.out) {
|
|
||||||
dataService.deleteEquipmentOut(equipment.id!);
|
|
||||||
} else if (equipment.status == EquipmentStatus.rent) {
|
|
||||||
// TODO: 대여 상태 삭제 구현
|
|
||||||
throw UnimplementedError('Rent status deletion not implemented');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 로컬 리스트에서도 제거
|
|
||||||
equipments.removeWhere((e) => e.id == equipment.id && e.status == equipment.status);
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} on Failure catch (e) {
|
|
||||||
_error = e.message;
|
|
||||||
notifyListeners();
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
_error = 'Failed to delete equipment: $e';
|
|
||||||
notifyListeners();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 사용 여부 토글 (테스트용)
|
|
||||||
void toggleApiUsage() {
|
|
||||||
_useApi = !_useApi;
|
|
||||||
loadData(isRefresh: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 에러 처리
|
|
||||||
void clearError() {
|
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import 'package:superport/models/equipment_unified_model.dart';
|
|||||||
import 'package:superport/models/company_model.dart';
|
import 'package:superport/models/company_model.dart';
|
||||||
import 'package:superport/models/company_branch_info.dart';
|
import 'package:superport/models/company_branch_info.dart';
|
||||||
import 'package:superport/models/address_model.dart';
|
import 'package:superport/models/address_model.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/services/equipment_service.dart';
|
import 'package:superport/services/equipment_service.dart';
|
||||||
import 'package:superport/services/company_service.dart';
|
import 'package:superport/services/company_service.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
@@ -14,7 +13,8 @@ import 'package:superport/utils/constants.dart';
|
|||||||
///
|
///
|
||||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||||
class EquipmentOutFormController extends ChangeNotifier {
|
class EquipmentOutFormController extends ChangeNotifier {
|
||||||
final MockDataService dataService;
|
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||||
|
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||||
int? equipmentOutId;
|
int? equipmentOutId;
|
||||||
|
|
||||||
// 편집 모드 여부
|
// 편집 모드 여부
|
||||||
@@ -62,7 +62,6 @@ class EquipmentOutFormController extends ChangeNotifier {
|
|||||||
final TextEditingController remarkController = TextEditingController();
|
final TextEditingController remarkController = TextEditingController();
|
||||||
|
|
||||||
EquipmentOutFormController({
|
EquipmentOutFormController({
|
||||||
required this.dataService,
|
|
||||||
this.equipmentOutId,
|
this.equipmentOutId,
|
||||||
}) {
|
}) {
|
||||||
isEditMode = equipmentOutId != null;
|
isEditMode = equipmentOutId != null;
|
||||||
@@ -77,22 +76,32 @@ class EquipmentOutFormController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 드롭다운 데이터 로드
|
// 드롭다운 데이터 로드
|
||||||
void loadDropdownData() {
|
Future<void> loadDropdownData() async {
|
||||||
// 회사 목록 로드 (출고처 가능한 회사만)
|
try {
|
||||||
companies = dataService.getAllCompanies()
|
// API를 통해 회사 목록 로드
|
||||||
.where((c) => c.companyTypes.contains(CompanyType.customer))
|
final allCompanies = await _companyService.getCompanies();
|
||||||
.map((c) => CompanyBranchInfo(
|
companies = allCompanies
|
||||||
id: c.id,
|
.where((c) => c.companyTypes.contains(CompanyType.customer))
|
||||||
name: c.name,
|
.map((c) => CompanyBranchInfo(
|
||||||
originalName: c.name,
|
id: c.id,
|
||||||
isMainCompany: true,
|
name: c.name,
|
||||||
companyId: c.id,
|
originalName: c.name,
|
||||||
|
isMainCompany: true,
|
||||||
|
companyId: c.id,
|
||||||
branchId: null,
|
branchId: null,
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// 라이선스 목록 로드
|
// TODO: 라이선스 목록도 API로 로드
|
||||||
licenses = dataService.getAllLicenses().map((l) => l.name).toList();
|
licenses = []; // 임시로 빈 목록
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('드롭다운 데이터 로드 실패: $e');
|
||||||
|
companies = [];
|
||||||
|
licenses = [];
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 장비로 초기화
|
// 선택된 장비로 초기화
|
||||||
@@ -109,23 +118,10 @@ class EquipmentOutFormController extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock 데이터에서 회사별 담당자 목록 가져오기
|
// TODO: API를 통해 회사별 담당자 목록 로드
|
||||||
final company = dataService.getAllCompanies().firstWhere(
|
// 현재는 임시로 빈 목록 사용
|
||||||
(c) => c.name == selectedCompanies[index],
|
hasManagersPerCompany[index] = false;
|
||||||
orElse: () => Company(
|
filteredManagersPerCompany[index] = [];
|
||||||
name: '',
|
|
||||||
companyTypes: [],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (company.name.isNotEmpty && company.contactName != null && company.contactName!.isNotEmpty) {
|
|
||||||
// 회사의 담당자 정보
|
|
||||||
hasManagersPerCompany[index] = true;
|
|
||||||
filteredManagersPerCompany[index] = [company.contactName!];
|
|
||||||
} else {
|
|
||||||
hasManagersPerCompany[index] = false;
|
|
||||||
filteredManagersPerCompany[index] = ['없음'];
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
|||||||
// import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
// import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
||||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
// import 'package:flutter_localizations/flutter_localizations.dart';
|
// import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
// import 'package:superport/screens/equipment/widgets/autocomplete_text_field.dart';
|
// import 'package:superport/screens/equipment/widgets/autocomplete_text_field.dart';
|
||||||
@@ -181,7 +180,6 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = EquipmentInFormController(
|
_controller = EquipmentInFormController(
|
||||||
dataService: MockDataService(),
|
|
||||||
equipmentInId: widget.equipmentInId,
|
equipmentInId: widget.equipmentInId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
484
lib/screens/equipment/equipment_in_form_lookup_example.dart
Normal file
484
lib/screens/equipment/equipment_in_form_lookup_example.dart
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../services/lookup_service.dart';
|
||||||
|
import '../../data/models/lookups/lookup_data.dart';
|
||||||
|
import '../common/theme_shadcn.dart';
|
||||||
|
import '../common/components/shadcn_components.dart';
|
||||||
|
|
||||||
|
/// LookupService를 활용한 장비 입고 폼 예시
|
||||||
|
/// 전역 캐싱된 Lookup 데이터를 활용하여 드롭다운 구성
|
||||||
|
class EquipmentInFormLookupExample extends StatefulWidget {
|
||||||
|
const EquipmentInFormLookupExample({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EquipmentInFormLookupExample> createState() => _EquipmentInFormLookupExampleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EquipmentInFormLookupExampleState extends State<EquipmentInFormLookupExample> {
|
||||||
|
late final LookupService _lookupService;
|
||||||
|
|
||||||
|
// 선택된 값들
|
||||||
|
String? _selectedEquipmentType;
|
||||||
|
String? _selectedEquipmentStatus;
|
||||||
|
String? _selectedManufacturer;
|
||||||
|
String? _selectedLicenseType;
|
||||||
|
|
||||||
|
// 텍스트 컨트롤러
|
||||||
|
final _serialNumberController = TextEditingController();
|
||||||
|
final _quantityController = TextEditingController();
|
||||||
|
final _descriptionController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_lookupService = GetIt.instance<LookupService>();
|
||||||
|
_loadLookupDataIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필요시 Lookup 데이터 로드 (캐시가 없을 경우)
|
||||||
|
Future<void> _loadLookupDataIfNeeded() async {
|
||||||
|
if (!_lookupService.hasData) {
|
||||||
|
await _lookupService.loadAllLookups();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {}); // UI 업데이트
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_serialNumberController.dispose();
|
||||||
|
_quantityController.dispose();
|
||||||
|
_descriptionController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: ShadcnTheme.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('장비 입고 (Lookup 활용 예시)'),
|
||||||
|
backgroundColor: ShadcnTheme.card,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: ChangeNotifierProvider.value(
|
||||||
|
value: _lookupService,
|
||||||
|
child: Consumer<LookupService>(
|
||||||
|
builder: (context, lookupService, child) {
|
||||||
|
if (lookupService.isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lookupService.error != null) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline,
|
||||||
|
size: 64,
|
||||||
|
color: ShadcnTheme.destructive,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Lookup 데이터 로드 실패',
|
||||||
|
style: ShadcnTheme.headingH4,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
lookupService.error!,
|
||||||
|
style: ShadcnTheme.bodyMuted,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ShadcnButton(
|
||||||
|
text: '다시 시도',
|
||||||
|
onPressed: () => lookupService.loadAllLookups(forceRefresh: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 800),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 안내 메시지
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ShadcnTheme.primary.withValues(alpha: 0.1),
|
||||||
|
border: Border.all(
|
||||||
|
color: ShadcnTheme.primary.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline,
|
||||||
|
color: ShadcnTheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'이 화면은 /lookups API를 통해 캐싱된 전역 데이터를 활용합니다.\n'
|
||||||
|
'드롭다운 데이터는 앱 시작 시 한 번만 로드되어 모든 화면에서 재사용됩니다.',
|
||||||
|
style: ShadcnTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 폼 카드
|
||||||
|
ShadcnCard(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('장비 정보', style: ShadcnTheme.headingH4),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 장비 타입 드롭다운
|
||||||
|
_buildDropdownField(
|
||||||
|
label: '장비 타입',
|
||||||
|
value: _selectedEquipmentType,
|
||||||
|
items: lookupService.equipmentTypes,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedEquipmentType = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 장비 상태 드롭다운
|
||||||
|
_buildDropdownField(
|
||||||
|
label: '장비 상태',
|
||||||
|
value: _selectedEquipmentStatus,
|
||||||
|
items: lookupService.equipmentStatuses,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedEquipmentStatus = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 제조사 드롭다운
|
||||||
|
_buildDropdownField(
|
||||||
|
label: '제조사',
|
||||||
|
value: _selectedManufacturer,
|
||||||
|
items: lookupService.manufacturers,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedManufacturer = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 시리얼 번호 입력
|
||||||
|
_buildTextField(
|
||||||
|
label: '시리얼 번호',
|
||||||
|
controller: _serialNumberController,
|
||||||
|
hintText: 'SN-2025-001',
|
||||||
|
),
|
||||||
|
|
||||||
|
// 수량 입력
|
||||||
|
_buildTextField(
|
||||||
|
label: '수량',
|
||||||
|
controller: _quantityController,
|
||||||
|
hintText: '1',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 라이선스 타입 드롭다운 (옵션)
|
||||||
|
_buildDropdownField(
|
||||||
|
label: '라이선스 타입 (선택)',
|
||||||
|
value: _selectedLicenseType,
|
||||||
|
items: lookupService.licenseTypes,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedLicenseType = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isOptional: true,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 비고 입력
|
||||||
|
_buildTextField(
|
||||||
|
label: '비고',
|
||||||
|
controller: _descriptionController,
|
||||||
|
hintText: '추가 정보를 입력하세요',
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// 버튼 그룹
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
ShadcnButton(
|
||||||
|
text: '취소',
|
||||||
|
variant: ShadcnButtonVariant.secondary,
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ShadcnButton(
|
||||||
|
text: '저장',
|
||||||
|
onPressed: _handleSubmit,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 캐시 정보 표시
|
||||||
|
_buildCacheInfoCard(lookupService),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 드롭다운 필드 빌더
|
||||||
|
Widget _buildDropdownField({
|
||||||
|
required String label,
|
||||||
|
required String? value,
|
||||||
|
required List<LookupItem> items,
|
||||||
|
required ValueChanged<String?> onChanged,
|
||||||
|
bool isOptional = false,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(label, style: ShadcnTheme.bodyMedium),
|
||||||
|
if (isOptional) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('(선택)', style: ShadcnTheme.bodyMuted),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: ShadcnTheme.border),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: '선택하세요',
|
||||||
|
hintStyle: ShadcnTheme.bodyMuted,
|
||||||
|
),
|
||||||
|
items: items.map((item) => DropdownMenuItem(
|
||||||
|
value: item.code ?? '',
|
||||||
|
child: Text(item.name ?? ''),
|
||||||
|
)).toList(),
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 텍스트 필드 빌더
|
||||||
|
Widget _buildTextField({
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
String? hintText,
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
int maxLines = 1,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label, style: ShadcnTheme.bodyMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
maxLines: maxLines,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
hintStyle: ShadcnTheme.bodyMuted,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: BorderSide(color: ShadcnTheme.border),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: BorderSide(color: ShadcnTheme.border),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: BorderSide(color: ShadcnTheme.primary, width: 2),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 캐시 정보 카드
|
||||||
|
Widget _buildCacheInfoCard(LookupService lookupService) {
|
||||||
|
return ShadcnCard(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.storage, size: 20, color: ShadcnTheme.muted),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('Lookup 캐시 정보', style: ShadcnTheme.bodyMedium),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildCacheItem('장비 타입', lookupService.equipmentTypes.length),
|
||||||
|
_buildCacheItem('장비 상태', lookupService.equipmentStatuses.length),
|
||||||
|
_buildCacheItem('제조사', lookupService.manufacturers.length),
|
||||||
|
_buildCacheItem('라이선스 타입', lookupService.licenseTypes.length),
|
||||||
|
_buildCacheItem('사용자 역할', lookupService.userRoles.length),
|
||||||
|
_buildCacheItem('회사 상태', lookupService.companyStatuses.length),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'캐시 상태: ${lookupService.isCacheValid ? "유효" : "만료"}',
|
||||||
|
style: ShadcnTheme.bodySmall.copyWith(
|
||||||
|
color: lookupService.isCacheValid
|
||||||
|
? ShadcnTheme.success
|
||||||
|
: ShadcnTheme.warning,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => lookupService.loadAllLookups(forceRefresh: true),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.refresh, size: 16, color: ShadcnTheme.primary),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('캐시 새로고침',
|
||||||
|
style: TextStyle(color: ShadcnTheme.primary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCacheItem(String label, int count) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(label, style: ShadcnTheme.bodySmall),
|
||||||
|
Text('$count개',
|
||||||
|
style: ShadcnTheme.bodySmall.copyWith(
|
||||||
|
color: ShadcnTheme.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 폼 제출 처리
|
||||||
|
void _handleSubmit() {
|
||||||
|
// 유효성 검증
|
||||||
|
if (_selectedEquipmentType == null) {
|
||||||
|
_showSnackBar('장비 타입을 선택하세요', isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_selectedEquipmentStatus == null) {
|
||||||
|
_showSnackBar('장비 상태를 선택하세요', isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_serialNumberController.text.isEmpty) {
|
||||||
|
_showSnackBar('시리얼 번호를 입력하세요', isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 값 정보 표시
|
||||||
|
final selectedType = _lookupService.findByCode(
|
||||||
|
_lookupService.equipmentTypes,
|
||||||
|
_selectedEquipmentType!,
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedStatus = _lookupService.findByCode(
|
||||||
|
_lookupService.equipmentStatuses,
|
||||||
|
_selectedEquipmentStatus!,
|
||||||
|
);
|
||||||
|
|
||||||
|
final message = '''
|
||||||
|
장비 입고 정보:
|
||||||
|
- 타입: ${selectedType?.name ?? _selectedEquipmentType}
|
||||||
|
- 상태: ${selectedStatus?.name ?? _selectedEquipmentStatus}
|
||||||
|
- 시리얼: ${_serialNumberController.text}
|
||||||
|
- 수량: ${_quantityController.text.isEmpty ? "1" : _quantityController.text}
|
||||||
|
''';
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('입고 정보 확인'),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('확인'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSnackBar(String message, {bool isError = false}) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: isError ? ShadcnTheme.destructive : ShadcnTheme.primary,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import 'package:superport/screens/common/widgets/standard_data_table.dart' as st
|
|||||||
import 'package:superport/screens/common/widgets/standard_states.dart';
|
import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||||
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
|
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/models/equipment_unified_model.dart';
|
import 'package:superport/models/equipment_unified_model.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
import 'package:superport/utils/equipment_display_helper.dart';
|
import 'package:superport/utils/equipment_display_helper.dart';
|
||||||
@@ -42,7 +41,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = EquipmentListController(dataService: MockDataService());
|
_controller = EquipmentListController();
|
||||||
_setInitialFilter();
|
_setInitialFilter();
|
||||||
|
|
||||||
// API 호출을 위해 Future로 변경
|
// API 호출을 위해 Future로 변경
|
||||||
@@ -116,7 +115,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
|||||||
}
|
}
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
});
|
});
|
||||||
await _controller.changeStatusFilter(_controller.selectedStatusFilter);
|
_controller.changeStatusFilter(_controller.selectedStatusFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 검색 실행
|
/// 검색 실행
|
||||||
@@ -125,13 +124,26 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
|||||||
_appliedSearchKeyword = _searchController.text;
|
_appliedSearchKeyword = _searchController.text;
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
});
|
});
|
||||||
await _controller.updateSearchKeyword(_searchController.text);
|
_controller.updateSearchKeyword(_searchController.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 장비 선택/해제
|
/// 장비 선택/해제
|
||||||
void _onEquipmentSelected(int? id, String status, bool? isSelected) {
|
void _onEquipmentSelected(int? id, String status, bool? isSelected) {
|
||||||
|
if (id == null) return;
|
||||||
|
|
||||||
|
// UnifiedEquipment를 찾아서 선택/해제
|
||||||
|
UnifiedEquipment? equipment;
|
||||||
|
try {
|
||||||
|
equipment = _controller.items.firstWhere(
|
||||||
|
(e) => e.equipment.id == id && e.status == status,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// 해당하는 장비를 찾지 못함
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_controller.selectEquipment(id, status, isSelected);
|
_controller.selectEquipment(equipment!);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +152,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
final equipments = _getFilteredEquipments();
|
final equipments = _getFilteredEquipments();
|
||||||
for (final equipment in equipments) {
|
for (final equipment in equipments) {
|
||||||
_controller.selectEquipment(equipment.id, equipment.status, value);
|
_controller.selectEquipment(equipment);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -234,7 +246,7 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary();
|
final selectedEquipments = _controller.getSelectedEquipments();
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -245,12 +257,12 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('선택한 ${selectedEquipmentsSummary.length}개 장비를 폐기하시겠습니까?'),
|
Text('선택한 ${selectedEquipments.length}개 장비를 폐기하시겠습니까?'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text('폐기할 장비 목록:', style: TextStyle(fontWeight: FontWeight.bold)),
|
const Text('폐기할 장비 목록:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
...selectedEquipmentsSummary.map((equipmentData) {
|
...selectedEquipments.map((unifiedEquipment) {
|
||||||
final equipment = equipmentData['equipment'] as Equipment;
|
final equipment = unifiedEquipment.equipment;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -328,26 +340,15 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Controller를 통한 삭제 처리
|
// Controller를 통한 삭제 처리
|
||||||
final success = await _controller.deleteEquipment(equipment);
|
await _controller.deleteEquipment(equipment.equipment.id!, equipment.status);
|
||||||
|
|
||||||
// 로딩 다이얼로그 닫기
|
// 로딩 다이얼로그 닫기
|
||||||
if (mounted) Navigator.pop(context);
|
if (mounted) Navigator.pop(context);
|
||||||
|
|
||||||
if (success) {
|
if (mounted) {
|
||||||
if (mounted) {
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
const SnackBar(content: Text('장비가 삭제되었습니다.')),
|
||||||
const SnackBar(content: Text('장비가 삭제되었습니다.')),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(_controller.error ?? '삭제 중 오류가 발생했습니다.'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:superport/models/company_branch_info.dart';
|
|||||||
import 'package:superport/models/address_model.dart';
|
import 'package:superport/models/address_model.dart';
|
||||||
import 'package:superport/screens/common/custom_widgets.dart';
|
import 'package:superport/screens/common/custom_widgets.dart';
|
||||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/screens/equipment/controllers/equipment_out_form_controller.dart';
|
import 'package:superport/screens/equipment/controllers/equipment_out_form_controller.dart';
|
||||||
import 'package:superport/screens/equipment/widgets/equipment_summary_card.dart';
|
import 'package:superport/screens/equipment/widgets/equipment_summary_card.dart';
|
||||||
import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart';
|
import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart';
|
||||||
@@ -37,7 +36,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = EquipmentOutFormController(dataService: MockDataService());
|
_controller = EquipmentOutFormController();
|
||||||
_controller.isEditMode = widget.equipmentOutId != null;
|
_controller.isEditMode = widget.equipmentOutId != null;
|
||||||
_controller.equipmentOutId = widget.equipmentOutId;
|
_controller.equipmentOutId = widget.equipmentOutId;
|
||||||
_controller.selectedEquipment = widget.selectedEquipment;
|
_controller.selectedEquipment = widget.selectedEquipment;
|
||||||
@@ -550,9 +549,9 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
|||||||
Branch? branch;
|
Branch? branch;
|
||||||
|
|
||||||
if (companyInfo.companyId != null) {
|
if (companyInfo.companyId != null) {
|
||||||
company = controller.dataService.getCompanyById(
|
// TODO: 실제 CompanyService를 통해 회사 정보 가져오기
|
||||||
companyInfo.companyId!,
|
// company = await _companyService.getCompanyById(companyInfo.companyId!);
|
||||||
);
|
company = null; // 임시로 null 처리
|
||||||
if (!companyInfo.isMainCompany &&
|
if (!companyInfo.isMainCompany &&
|
||||||
companyInfo.branchId != null &&
|
companyInfo.branchId != null &&
|
||||||
company != null) {
|
company != null) {
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:superport/models/license_model.dart';
|
import 'package:superport/models/license_model.dart';
|
||||||
import 'package:superport/services/license_service.dart';
|
import 'package:superport/services/license_service.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
|
|
||||||
// 라이센스 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
// 라이센스 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||||
class LicenseFormController extends ChangeNotifier {
|
class LicenseFormController extends ChangeNotifier {
|
||||||
final bool useApi;
|
final LicenseService _licenseService = GetIt.instance<LicenseService>();
|
||||||
final MockDataService? mockDataService;
|
|
||||||
late final LicenseService _licenseService;
|
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
bool _isEditMode = false;
|
bool _isEditMode = false;
|
||||||
@@ -59,15 +56,9 @@ class LicenseFormController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LicenseFormController({
|
LicenseFormController({
|
||||||
this.useApi = false,
|
|
||||||
MockDataService? dataService,
|
|
||||||
int? licenseId,
|
int? licenseId,
|
||||||
bool isExtension = false,
|
bool isExtension = false,
|
||||||
}) : mockDataService = dataService ?? MockDataService() {
|
}) {
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
|
||||||
_licenseService = GetIt.instance<LicenseService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (licenseId != null && !isExtension) {
|
if (licenseId != null && !isExtension) {
|
||||||
_licenseId = licenseId;
|
_licenseId = licenseId;
|
||||||
_isEditMode = true;
|
_isEditMode = true;
|
||||||
@@ -122,13 +113,8 @@ class LicenseFormController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
debugPrint('📝 API에서 라이센스 로드 중...');
|
||||||
debugPrint('📝 API에서 라이센스 로드 중...');
|
_originalLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||||
_originalLicense = await _licenseService.getLicenseById(_licenseId!);
|
|
||||||
} else {
|
|
||||||
debugPrint('📝 Mock에서 라이센스 로드 중...');
|
|
||||||
_originalLicense = mockDataService?.getLicenseById(_licenseId!);
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('📝 로드된 라이센스: $_originalLicense');
|
debugPrint('📝 로드된 라이센스: $_originalLicense');
|
||||||
|
|
||||||
@@ -182,14 +168,8 @@ class LicenseFormController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
License? sourceLicense;
|
debugPrint('📝 API에서 라이센스 로드 중 (연장용)...');
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
final sourceLicense = await _licenseService.getLicenseById(_licenseId!);
|
||||||
debugPrint('📝 API에서 라이센스 로드 중 (연장용)...');
|
|
||||||
sourceLicense = await _licenseService.getLicenseById(_licenseId!);
|
|
||||||
} else {
|
|
||||||
debugPrint('📝 Mock에서 라이센스 로드 중 (연장용)...');
|
|
||||||
sourceLicense = mockDataService?.getLicenseById(_licenseId!);
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('📝 로드된 소스 라이센스: $sourceLicense');
|
debugPrint('📝 로드된 소스 라이센스: $sourceLicense');
|
||||||
|
|
||||||
@@ -263,18 +243,10 @@ class LicenseFormController extends ChangeNotifier {
|
|||||||
remark: '${_durationMonths}개월,${_visitCycle},방문',
|
remark: '${_durationMonths}개월,${_visitCycle},방문',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
if (_isEditMode) {
|
||||||
if (_isEditMode) {
|
await _licenseService.updateLicense(license);
|
||||||
await _licenseService.updateLicense(license);
|
|
||||||
} else {
|
|
||||||
await _licenseService.createLicense(license);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (_isEditMode) {
|
await _licenseService.createLicense(license);
|
||||||
mockDataService?.updateLicense(license);
|
|
||||||
} else {
|
|
||||||
mockDataService?.addLicense(license);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:superport/core/errors/failures.dart';
|
||||||
|
import 'package:superport/models/license_model.dart';
|
||||||
|
import 'package:superport/services/license_service.dart';
|
||||||
|
|
||||||
|
// 라이센스 상태 필터
|
||||||
|
enum LicenseStatusFilter {
|
||||||
|
all,
|
||||||
|
active,
|
||||||
|
inactive,
|
||||||
|
expiringSoon, // 30일 이내
|
||||||
|
expired,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||||
|
class LicenseListController extends ChangeNotifier {
|
||||||
|
final LicenseService _licenseService = GetIt.instance<LicenseService>();
|
||||||
|
|
||||||
|
List<License> _licenses = [];
|
||||||
|
List<License> _filteredLicenses = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
String _searchQuery = '';
|
||||||
|
int _currentPage = 1;
|
||||||
|
final int _pageSize = 20;
|
||||||
|
bool _hasMore = true;
|
||||||
|
int _total = 0;
|
||||||
|
|
||||||
|
// 필터 옵션
|
||||||
|
int? _selectedCompanyId;
|
||||||
|
bool? _isActive;
|
||||||
|
String? _licenseType;
|
||||||
|
LicenseStatusFilter _statusFilter = LicenseStatusFilter.all;
|
||||||
|
String _sortBy = 'expiry_date';
|
||||||
|
String _sortOrder = 'asc';
|
||||||
|
|
||||||
|
// 선택된 라이선스 관리
|
||||||
|
final Set<int> _selectedLicenseIds = {};
|
||||||
|
|
||||||
|
// 통계 데이터
|
||||||
|
Map<String, int> _statistics = {
|
||||||
|
'total': 0,
|
||||||
|
'active': 0,
|
||||||
|
'inactive': 0,
|
||||||
|
'expiringSoon': 0,
|
||||||
|
'expired': 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 검색 디바운스를 위한 타이머
|
||||||
|
Timer? _debounceTimer;
|
||||||
|
|
||||||
|
LicenseListController();
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
List<License> get licenses => _filteredLicenses;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
String? get error => _error;
|
||||||
|
String get searchQuery => _searchQuery;
|
||||||
|
int get currentPage => _currentPage;
|
||||||
|
bool get hasMore => _hasMore;
|
||||||
|
int get total => _total;
|
||||||
|
int? get selectedCompanyId => _selectedCompanyId;
|
||||||
|
bool? get isActive => _isActive;
|
||||||
|
String? get licenseType => _licenseType;
|
||||||
|
LicenseStatusFilter get statusFilter => _statusFilter;
|
||||||
|
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||||
|
Map<String, int> get statistics => _statistics;
|
||||||
|
|
||||||
|
// 선택된 라이선스 개수
|
||||||
|
int get selectedCount => _selectedLicenseIds.length;
|
||||||
|
|
||||||
|
// 전체 선택 여부 확인
|
||||||
|
bool get isAllSelected =>
|
||||||
|
_filteredLicenses.isNotEmpty &&
|
||||||
|
_filteredLicenses.where((l) => l.id != null)
|
||||||
|
.every((l) => _selectedLicenseIds.contains(l.id));
|
||||||
|
|
||||||
|
// 데이터 로드
|
||||||
|
Future<void> loadData({bool isInitialLoad = true}) async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API 사용 - 전체 데이터 로드
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 🔧 유지보수 목록 API 호출 시작');
|
||||||
|
print('║ • 회사 필터: ${_selectedCompanyId ?? "전체"}');
|
||||||
|
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||||
|
print('║ • 라이센스 타입: ${_licenseType ?? "전체"}');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
||||||
|
final fetchedLicenses = await _licenseService.getLicenses(
|
||||||
|
page: 1,
|
||||||
|
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||||
|
isActive: _isActive,
|
||||||
|
companyId: _selectedCompanyId,
|
||||||
|
licenseType: _licenseType,
|
||||||
|
);
|
||||||
|
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 📊 유지보수 목록 로드 완료');
|
||||||
|
print('║ ▶ 총 라이센스 수: ${fetchedLicenses.length}개');
|
||||||
|
print('╟──────────────────────────────────────────────────────────');
|
||||||
|
|
||||||
|
// 상태별 통계
|
||||||
|
int activeCount = 0;
|
||||||
|
int expiringSoonCount = 0;
|
||||||
|
int expiredCount = 0;
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
for (final license in fetchedLicenses) {
|
||||||
|
if (license.expiryDate != null) {
|
||||||
|
final daysUntil = license.expiryDate!.difference(now).inDays;
|
||||||
|
if (daysUntil < 0) {
|
||||||
|
expiredCount++;
|
||||||
|
} else if (daysUntil <= 30) {
|
||||||
|
expiringSoonCount++;
|
||||||
|
} else {
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print('║ • 활성: $activeCount개');
|
||||||
|
print('║ • 만료 임박 (30일 이내): $expiringSoonCount개');
|
||||||
|
print('║ • 만료됨: $expiredCount개');
|
||||||
|
|
||||||
|
print('╟──────────────────────────────────────────────────────────');
|
||||||
|
print('║ 📑 전체 데이터 로드 완료');
|
||||||
|
print('║ • View에서 페이지네이션 처리 예정');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
_licenses = fetchedLicenses;
|
||||||
|
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||||
|
_total = fetchedLicenses.length;
|
||||||
|
|
||||||
|
debugPrint('📑 _applySearchFilter 호출 전: _licenses=${_licenses.length}개');
|
||||||
|
_applySearchFilter();
|
||||||
|
_applyStatusFilter();
|
||||||
|
await _updateStatistics();
|
||||||
|
debugPrint('📑 _applySearchFilter 호출 후: _filteredLicenses=${_filteredLicenses.length}개');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ loadData 에러 발생: $e');
|
||||||
|
_error = e.toString();
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
debugPrint('📑 loadData 종료: _filteredLicenses=${_filteredLicenses.length}개');
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다음 페이지 로드
|
||||||
|
Future<void> loadNextPage() async {
|
||||||
|
if (!_hasMore || _isLoading) return;
|
||||||
|
_currentPage++;
|
||||||
|
await loadData(isInitialLoad: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 (디바운싱 적용)
|
||||||
|
void search(String query) {
|
||||||
|
_searchQuery = query;
|
||||||
|
|
||||||
|
// 기존 타이머 취소
|
||||||
|
_debounceTimer?.cancel();
|
||||||
|
|
||||||
|
// API 검색은 디바운싱 적용 (300ms)
|
||||||
|
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 필터 적용
|
||||||
|
void _applySearchFilter() {
|
||||||
|
debugPrint('🔎 _applySearchFilter 시작: _searchQuery="$_searchQuery", _licenses=${_licenses.length}개');
|
||||||
|
|
||||||
|
if (_searchQuery.isEmpty) {
|
||||||
|
_filteredLicenses = List.from(_licenses);
|
||||||
|
debugPrint('🔎 검색어 없음: 전체 복사 ${_filteredLicenses.length}개');
|
||||||
|
} else {
|
||||||
|
_filteredLicenses = _licenses.where((license) {
|
||||||
|
final productName = license.productName?.toLowerCase() ?? '';
|
||||||
|
final licenseKey = license.licenseKey.toLowerCase();
|
||||||
|
final vendor = license.vendor?.toLowerCase() ?? '';
|
||||||
|
final companyName = license.companyName?.toLowerCase() ?? '';
|
||||||
|
final searchLower = _searchQuery.toLowerCase();
|
||||||
|
|
||||||
|
return productName.contains(searchLower) ||
|
||||||
|
licenseKey.contains(searchLower) ||
|
||||||
|
vendor.contains(searchLower) ||
|
||||||
|
companyName.contains(searchLower);
|
||||||
|
}).toList();
|
||||||
|
debugPrint('🔎 검색 필터링 완료: ${_filteredLicenses.length}개');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태 필터 적용
|
||||||
|
void _applyStatusFilter() {
|
||||||
|
if (_statusFilter == LicenseStatusFilter.all) return;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
_filteredLicenses = _filteredLicenses.where((license) {
|
||||||
|
switch (_statusFilter) {
|
||||||
|
case LicenseStatusFilter.active:
|
||||||
|
return license.isActive;
|
||||||
|
case LicenseStatusFilter.inactive:
|
||||||
|
return !license.isActive;
|
||||||
|
case LicenseStatusFilter.expiringSoon:
|
||||||
|
if (license.expiryDate != null) {
|
||||||
|
final days = license.expiryDate!.difference(now).inDays;
|
||||||
|
return days > 0 && days <= 30;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
case LicenseStatusFilter.expired:
|
||||||
|
if (license.expiryDate != null) {
|
||||||
|
return license.expiryDate!.isBefore(now);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
case LicenseStatusFilter.all:
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터 설정
|
||||||
|
void setFilters({
|
||||||
|
int? companyId,
|
||||||
|
bool? isActive,
|
||||||
|
String? licenseType,
|
||||||
|
}) {
|
||||||
|
_selectedCompanyId = companyId;
|
||||||
|
_isActive = isActive;
|
||||||
|
_licenseType = licenseType;
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터 초기화
|
||||||
|
void clearFilters() {
|
||||||
|
_selectedCompanyId = null;
|
||||||
|
_isActive = null;
|
||||||
|
_licenseType = null;
|
||||||
|
_searchQuery = '';
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이센스 삭제
|
||||||
|
Future<void> deleteLicense(int id) async {
|
||||||
|
try {
|
||||||
|
await _licenseService.deleteLicense(id);
|
||||||
|
|
||||||
|
// 목록에서 제거
|
||||||
|
_licenses.removeWhere((l) => l.id == id);
|
||||||
|
_applySearchFilter();
|
||||||
|
_total--;
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새로고침
|
||||||
|
Future<void> refresh() async {
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 만료 예정 라이선스 조회
|
||||||
|
Future<List<License>> getExpiringLicenses({int days = 30}) async {
|
||||||
|
try {
|
||||||
|
return await _licenseService.getExpiringLicenses(days: days);
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
notifyListeners();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태별 라이선스 개수 조회
|
||||||
|
Future<Map<String, int>> getLicenseStatusCounts() async {
|
||||||
|
try {
|
||||||
|
// API에서 상태별 개수 조회 (실제로는 별도 엔드포인트가 있다면 사용)
|
||||||
|
final activeCount = await _licenseService.getTotalLicenses(isActive: true);
|
||||||
|
final inactiveCount = await _licenseService.getTotalLicenses(isActive: false);
|
||||||
|
final expiringLicenses = await getExpiringLicenses(days: 30);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'active': activeCount,
|
||||||
|
'inactive': inactiveCount,
|
||||||
|
'expiring': expiringLicenses.length,
|
||||||
|
'total': activeCount + inactiveCount,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {'active': 0, 'inactive': 0, 'expiring': 0, 'total': 0};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 정렬 변경
|
||||||
|
void sortBy(String field, String order) {
|
||||||
|
_sortBy = field;
|
||||||
|
_sortOrder = order;
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태 필터 변경
|
||||||
|
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
||||||
|
_statusFilter = filter;
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이선스 선택/해제
|
||||||
|
void selectLicense(int? id, bool? isSelected) {
|
||||||
|
if (id == null) return;
|
||||||
|
|
||||||
|
if (isSelected == true) {
|
||||||
|
_selectedLicenseIds.add(id);
|
||||||
|
} else {
|
||||||
|
_selectedLicenseIds.remove(id);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 선택/해제
|
||||||
|
void selectAll(bool? isSelected) {
|
||||||
|
if (isSelected == true) {
|
||||||
|
// 현재 필터링된 라이선스 모두 선택
|
||||||
|
for (var license in _filteredLicenses) {
|
||||||
|
if (license.id != null) {
|
||||||
|
_selectedLicenseIds.add(license.id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 모두 해제
|
||||||
|
_selectedLicenseIds.clear();
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 라이선스 목록 반환
|
||||||
|
List<License> getSelectedLicenses() {
|
||||||
|
return _filteredLicenses
|
||||||
|
.where((l) => l.id != null && _selectedLicenseIds.contains(l.id))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 초기화
|
||||||
|
void clearSelection() {
|
||||||
|
_selectedLicenseIds.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이선스 할당
|
||||||
|
Future<bool> assignLicense(int licenseId, int userId) async {
|
||||||
|
try {
|
||||||
|
await _licenseService.assignLicense(licenseId, userId);
|
||||||
|
await loadData();
|
||||||
|
clearSelection();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이선스 할당 해제
|
||||||
|
Future<bool> unassignLicense(int licenseId) async {
|
||||||
|
try {
|
||||||
|
await _licenseService.unassignLicense(licenseId);
|
||||||
|
await loadData();
|
||||||
|
clearSelection();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택된 라이선스 일괄 삭제
|
||||||
|
Future<void> deleteSelectedLicenses() async {
|
||||||
|
if (_selectedLicenseIds.isEmpty) return;
|
||||||
|
|
||||||
|
final selectedIds = List<int>.from(_selectedLicenseIds);
|
||||||
|
int successCount = 0;
|
||||||
|
int failCount = 0;
|
||||||
|
|
||||||
|
for (var id in selectedIds) {
|
||||||
|
try {
|
||||||
|
await deleteLicense(id);
|
||||||
|
successCount++;
|
||||||
|
} catch (e) {
|
||||||
|
failCount++;
|
||||||
|
debugPrint('라이선스 $id 삭제 실패: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedLicenseIds.clear();
|
||||||
|
await loadData();
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
debugPrint('✅ $successCount개 라이선스 삭제 완료');
|
||||||
|
}
|
||||||
|
if (failCount > 0) {
|
||||||
|
debugPrint('❌ $failCount개 라이선스 삭제 실패');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 통계 업데이트
|
||||||
|
Future<void> _updateStatistics() async {
|
||||||
|
try {
|
||||||
|
final counts = await getLicenseStatusCounts();
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
int expiringSoonCount = 0;
|
||||||
|
int expiredCount = 0;
|
||||||
|
|
||||||
|
for (var license in _licenses) {
|
||||||
|
if (license.expiryDate != null) {
|
||||||
|
final days = license.expiryDate!.difference(now).inDays;
|
||||||
|
if (days <= 0) {
|
||||||
|
expiredCount++;
|
||||||
|
} else if (days <= 30) {
|
||||||
|
expiringSoonCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_statistics = {
|
||||||
|
'total': counts['total'] ?? 0,
|
||||||
|
'active': counts['active'] ?? 0,
|
||||||
|
'inactive': counts['inactive'] ?? 0,
|
||||||
|
'expiringSoon': expiringSoonCount,
|
||||||
|
'expired': expiredCount,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ 통계 업데이트 오류: $e');
|
||||||
|
// 오류 발생 시 기본값 사용
|
||||||
|
_statistics = {
|
||||||
|
'total': _licenses.length,
|
||||||
|
'active': 0,
|
||||||
|
'inactive': 0,
|
||||||
|
'expiringSoon': 0,
|
||||||
|
'expired': 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 만료일까지 남은 일수 계산
|
||||||
|
int? getDaysUntilExpiry(License license) {
|
||||||
|
if (license.expiryDate == null) return null;
|
||||||
|
return license.expiryDate!.difference(DateTime.now()).inDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_debounceTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,28 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:superport/core/errors/failures.dart';
|
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||||
|
import 'package:superport/core/constants/app_constants.dart';
|
||||||
|
import 'package:superport/core/utils/error_handler.dart';
|
||||||
import 'package:superport/models/license_model.dart';
|
import 'package:superport/models/license_model.dart';
|
||||||
import 'package:superport/services/license_service.dart';
|
import 'package:superport/services/license_service.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
import 'package:superport/data/models/common/pagination_params.dart';
|
||||||
|
|
||||||
// 라이센스 상태 필터
|
/// 라이센스 상태 필터
|
||||||
enum LicenseStatusFilter {
|
enum LicenseStatusFilter {
|
||||||
all,
|
all,
|
||||||
active,
|
active,
|
||||||
inactive,
|
inactive,
|
||||||
expiringSoon, // 30일 이내
|
expiringSoon, // ${AppConstants.licenseExpiryWarningDays}일 이내
|
||||||
expired,
|
expired,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
/// 라이센스 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||||
class LicenseListController extends ChangeNotifier {
|
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||||
final bool useApi;
|
class LicenseListController extends BaseListController<License> {
|
||||||
final MockDataService? mockDataService;
|
|
||||||
late final LicenseService _licenseService;
|
late final LicenseService _licenseService;
|
||||||
|
|
||||||
List<License> _licenses = [];
|
// 라이선스 특화 필터 상태
|
||||||
List<License> _filteredLicenses = [];
|
|
||||||
bool _isLoading = false;
|
|
||||||
String? _error;
|
|
||||||
String _searchQuery = '';
|
|
||||||
int _currentPage = 1;
|
|
||||||
final int _pageSize = 20;
|
|
||||||
bool _hasMore = true;
|
|
||||||
int _total = 0;
|
|
||||||
|
|
||||||
// 필터 옵션
|
|
||||||
int? _selectedCompanyId;
|
int? _selectedCompanyId;
|
||||||
bool? _isActive;
|
bool? _isActive;
|
||||||
String? _licenseType;
|
String? _licenseType;
|
||||||
@@ -54,207 +45,112 @@ class LicenseListController extends ChangeNotifier {
|
|||||||
// 검색 디바운스를 위한 타이머
|
// 검색 디바운스를 위한 타이머
|
||||||
Timer? _debounceTimer;
|
Timer? _debounceTimer;
|
||||||
|
|
||||||
LicenseListController({this.useApi = false, this.mockDataService}) {
|
// Getters for license-specific properties
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
List<License> get licenses => items;
|
||||||
_licenseService = GetIt.instance<LicenseService>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
List<License> get licenses => _filteredLicenses;
|
|
||||||
bool get isLoading => _isLoading;
|
|
||||||
String? get error => _error;
|
|
||||||
String get searchQuery => _searchQuery;
|
|
||||||
int get currentPage => _currentPage;
|
|
||||||
bool get hasMore => _hasMore;
|
|
||||||
int get total => _total;
|
|
||||||
int? get selectedCompanyId => _selectedCompanyId;
|
int? get selectedCompanyId => _selectedCompanyId;
|
||||||
bool? get isActive => _isActive;
|
bool? get isActive => _isActive;
|
||||||
String? get licenseType => _licenseType;
|
String? get licenseType => _licenseType;
|
||||||
LicenseStatusFilter get statusFilter => _statusFilter;
|
LicenseStatusFilter get statusFilter => _statusFilter;
|
||||||
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||||
Map<String, int> get statistics => _statistics;
|
Map<String, int> get statistics => _statistics;
|
||||||
|
|
||||||
// 선택된 라이선스 개수
|
|
||||||
int get selectedCount => _selectedLicenseIds.length;
|
int get selectedCount => _selectedLicenseIds.length;
|
||||||
|
|
||||||
// 전체 선택 여부 확인
|
// 전체 선택 여부 확인
|
||||||
bool get isAllSelected =>
|
bool get isAllSelected =>
|
||||||
_filteredLicenses.isNotEmpty &&
|
items.isNotEmpty &&
|
||||||
_filteredLicenses.where((l) => l.id != null)
|
items.where((l) => l.id != null)
|
||||||
.every((l) => _selectedLicenseIds.contains(l.id));
|
.every((l) => _selectedLicenseIds.contains(l.id));
|
||||||
|
|
||||||
// 데이터 로드
|
LicenseListController() {
|
||||||
Future<void> loadData({bool isInitialLoad = true}) async {
|
if (GetIt.instance.isRegistered<LicenseService>()) {
|
||||||
if (_isLoading) return;
|
_licenseService = GetIt.instance<LicenseService>();
|
||||||
|
} else {
|
||||||
_isLoading = true;
|
throw Exception('LicenseService not registered in GetIt');
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
|
||||||
// API 사용 - 전체 데이터 로드
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 🔧 유지보수 목록 API 호출 시작');
|
|
||||||
print('║ • 회사 필터: ${_selectedCompanyId ?? "전체"}');
|
|
||||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
|
||||||
print('║ • 라이센스 타입: ${_licenseType ?? "전체"}');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
|
||||||
final fetchedLicenses = await _licenseService.getLicenses(
|
|
||||||
page: 1,
|
|
||||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
|
||||||
isActive: _isActive,
|
|
||||||
companyId: _selectedCompanyId,
|
|
||||||
licenseType: _licenseType,
|
|
||||||
);
|
|
||||||
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 📊 유지보수 목록 로드 완료');
|
|
||||||
print('║ ▶ 총 라이센스 수: ${fetchedLicenses.length}개');
|
|
||||||
print('╟──────────────────────────────────────────────────────────');
|
|
||||||
|
|
||||||
// 상태별 통계
|
|
||||||
int activeCount = 0;
|
|
||||||
int expiringSoonCount = 0;
|
|
||||||
int expiredCount = 0;
|
|
||||||
final now = DateTime.now();
|
|
||||||
|
|
||||||
for (final license in fetchedLicenses) {
|
|
||||||
if (license.expiryDate != null) {
|
|
||||||
final daysUntil = license.expiryDate!.difference(now).inDays;
|
|
||||||
if (daysUntil < 0) {
|
|
||||||
expiredCount++;
|
|
||||||
} else if (daysUntil <= 30) {
|
|
||||||
expiringSoonCount++;
|
|
||||||
} else {
|
|
||||||
activeCount++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
activeCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('║ • 활성: $activeCount개');
|
|
||||||
print('║ • 만료 임박 (30일 이내): $expiringSoonCount개');
|
|
||||||
print('║ • 만료됨: $expiredCount개');
|
|
||||||
|
|
||||||
print('╟──────────────────────────────────────────────────────────');
|
|
||||||
print('║ 📑 전체 데이터 로드 완료');
|
|
||||||
print('║ • View에서 페이지네이션 처리 예정');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
_licenses = fetchedLicenses;
|
|
||||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
|
||||||
_total = fetchedLicenses.length;
|
|
||||||
} else {
|
|
||||||
// Mock 데이터 사용
|
|
||||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
|
||||||
|
|
||||||
// 필터링 적용
|
|
||||||
var filtered = allLicenses;
|
|
||||||
if (_selectedCompanyId != null) {
|
|
||||||
filtered = filtered.where((l) => l.companyId == _selectedCompanyId).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 페이지네이션 적용
|
|
||||||
final startIndex = (_currentPage - 1) * _pageSize;
|
|
||||||
final endIndex = startIndex + _pageSize;
|
|
||||||
|
|
||||||
if (startIndex < filtered.length) {
|
|
||||||
final pageLicenses = filtered.sublist(
|
|
||||||
startIndex,
|
|
||||||
endIndex > filtered.length ? filtered.length : endIndex,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isInitialLoad) {
|
|
||||||
_licenses = pageLicenses;
|
|
||||||
} else {
|
|
||||||
_licenses.addAll(pageLicenses);
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasMore = endIndex < filtered.length;
|
|
||||||
} else {
|
|
||||||
_hasMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_total = filtered.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('📑 _applySearchFilter 호출 전: _licenses=${_licenses.length}개');
|
|
||||||
_applySearchFilter();
|
|
||||||
_applyStatusFilter();
|
|
||||||
await _updateStatistics();
|
|
||||||
debugPrint('📑 _applySearchFilter 호출 후: _filteredLicenses=${_filteredLicenses.length}개');
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('❌ loadData 에러 발생: $e');
|
|
||||||
_error = e.toString();
|
|
||||||
} finally {
|
|
||||||
_isLoading = false;
|
|
||||||
debugPrint('📑 loadData 종료: _filteredLicenses=${_filteredLicenses.length}개');
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 다음 페이지 로드
|
@override
|
||||||
Future<void> loadNextPage() async {
|
Future<PagedResult<License>> fetchData({
|
||||||
if (!_hasMore || _isLoading) return;
|
required PaginationParams params,
|
||||||
_currentPage++;
|
Map<String, dynamic>? additionalFilters,
|
||||||
await loadData(isInitialLoad: false);
|
}) async {
|
||||||
|
// API 호출
|
||||||
|
final fetchedLicenses = await ErrorHandler.handleApiCall(
|
||||||
|
() => _licenseService.getLicenses(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
isActive: _isActive,
|
||||||
|
companyId: _selectedCompanyId,
|
||||||
|
licenseType: _licenseType,
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fetchedLicenses == null) {
|
||||||
|
return PagedResult(
|
||||||
|
items: [],
|
||||||
|
meta: PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrevious: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 통계 업데이트
|
||||||
|
await _updateStatistics(fetchedLicenses);
|
||||||
|
|
||||||
|
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||||
|
final meta = PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: fetchedLicenses.length < params.perPage ?
|
||||||
|
(params.page - 1) * params.perPage + fetchedLicenses.length :
|
||||||
|
params.page * params.perPage + 1,
|
||||||
|
totalPages: fetchedLicenses.length < params.perPage ? params.page : params.page + 1,
|
||||||
|
hasNext: fetchedLicenses.length >= params.perPage,
|
||||||
|
hasPrevious: params.page > 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return PagedResult(items: fetchedLicenses, meta: meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검색 (디바운싱 적용)
|
@override
|
||||||
|
bool filterItem(License item, String query) {
|
||||||
|
final q = query.toLowerCase();
|
||||||
|
return (item.productName?.toLowerCase().contains(q) ?? false) ||
|
||||||
|
(item.licenseKey.toLowerCase().contains(q)) ||
|
||||||
|
(item.vendor?.toLowerCase().contains(q) ?? false) ||
|
||||||
|
(item.companyName?.toLowerCase().contains(q) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BaseListController의 검색을 오버라이드하여 디바운싱 적용
|
||||||
|
@override
|
||||||
void search(String query) {
|
void search(String query) {
|
||||||
_searchQuery = query;
|
|
||||||
|
|
||||||
// 기존 타이머 취소
|
// 기존 타이머 취소
|
||||||
_debounceTimer?.cancel();
|
_debounceTimer?.cancel();
|
||||||
|
|
||||||
// Mock 데이터는 즉시 검색
|
// 디바운싱 적용 (300ms)
|
||||||
if (!useApi) {
|
_debounceTimer = Timer(AppConstants.licenseSearchDebounce, () {
|
||||||
_applySearchFilter();
|
super.search(query);
|
||||||
notifyListeners();
|
_applyStatusFilter();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 검색은 디바운싱 적용 (300ms)
|
|
||||||
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
|
||||||
loadData();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검색 필터 적용
|
|
||||||
void _applySearchFilter() {
|
|
||||||
debugPrint('🔎 _applySearchFilter 시작: _searchQuery="$_searchQuery", _licenses=${_licenses.length}개');
|
|
||||||
|
|
||||||
if (_searchQuery.isEmpty) {
|
|
||||||
_filteredLicenses = List.from(_licenses);
|
|
||||||
debugPrint('🔎 검색어 없음: 전체 복사 ${_filteredLicenses.length}개');
|
|
||||||
} else {
|
|
||||||
_filteredLicenses = _licenses.where((license) {
|
|
||||||
final productName = license.productName?.toLowerCase() ?? '';
|
|
||||||
final licenseKey = license.licenseKey.toLowerCase();
|
|
||||||
final vendor = license.vendor?.toLowerCase() ?? '';
|
|
||||||
final companyName = license.companyName?.toLowerCase() ?? '';
|
|
||||||
final searchLower = _searchQuery.toLowerCase();
|
|
||||||
|
|
||||||
return productName.contains(searchLower) ||
|
|
||||||
licenseKey.contains(searchLower) ||
|
|
||||||
vendor.contains(searchLower) ||
|
|
||||||
companyName.contains(searchLower);
|
|
||||||
}).toList();
|
|
||||||
debugPrint('🔎 검색 필터링 완료: ${_filteredLicenses.length}개');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상태 필터 적용
|
/// 상태 필터 적용 (BaseListController의 filtering과 추가로 동작)
|
||||||
void _applyStatusFilter() {
|
void _applyStatusFilter() {
|
||||||
if (_statusFilter == LicenseStatusFilter.all) return;
|
if (_statusFilter == LicenseStatusFilter.all) return;
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
_filteredLicenses = _filteredLicenses.where((license) {
|
final currentItems = List<License>.from(items);
|
||||||
|
|
||||||
|
// 상태 필터 적용
|
||||||
|
final filteredByStatus = currentItems.where((license) {
|
||||||
switch (_statusFilter) {
|
switch (_statusFilter) {
|
||||||
case LicenseStatusFilter.active:
|
case LicenseStatusFilter.active:
|
||||||
return license.isActive;
|
return license.isActive;
|
||||||
@@ -276,9 +172,13 @@ class LicenseListController extends ChangeNotifier {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
// 직접 필터링된 결과를 적용 (BaseListController의 private 필드에 접근할 수 없으므로)
|
||||||
|
// 대신 notifyListeners를 통해 UI 업데이트
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필터 설정
|
/// 필터 설정
|
||||||
void setFilters({
|
void setFilters({
|
||||||
int? companyId,
|
int? companyId,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
@@ -287,135 +187,48 @@ class LicenseListController extends ChangeNotifier {
|
|||||||
_selectedCompanyId = companyId;
|
_selectedCompanyId = companyId;
|
||||||
_isActive = isActive;
|
_isActive = isActive;
|
||||||
_licenseType = licenseType;
|
_licenseType = licenseType;
|
||||||
loadData();
|
loadData(isRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필터 초기화
|
/// 필터 초기화
|
||||||
void clearFilters() {
|
void clearFilters() {
|
||||||
_selectedCompanyId = null;
|
_selectedCompanyId = null;
|
||||||
_isActive = null;
|
_isActive = null;
|
||||||
_licenseType = null;
|
_licenseType = null;
|
||||||
_searchQuery = '';
|
_statusFilter = LicenseStatusFilter.all;
|
||||||
loadData();
|
search(''); // BaseListController의 search 호출
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라이센스 삭제
|
/// 상태 필터 변경
|
||||||
Future<void> deleteLicense(int id) async {
|
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
||||||
try {
|
_statusFilter = filter;
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
_applyStatusFilter();
|
||||||
await _licenseService.deleteLicense(id);
|
|
||||||
} else {
|
|
||||||
mockDataService?.deleteLicense(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 목록에서 제거
|
|
||||||
_licenses.removeWhere((l) => l.id == id);
|
|
||||||
_applySearchFilter();
|
|
||||||
_total--;
|
|
||||||
notifyListeners();
|
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 새로고침
|
/// 정렬 변경
|
||||||
Future<void> refresh() async {
|
|
||||||
await loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 만료 예정 라이선스 조회
|
|
||||||
Future<List<License>> getExpiringLicenses({int days = 30}) async {
|
|
||||||
try {
|
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
|
||||||
return await _licenseService.getExpiringLicenses(days: days);
|
|
||||||
} else {
|
|
||||||
// Mock 데이터에서 만료 예정 라이선스 필터링
|
|
||||||
final now = DateTime.now();
|
|
||||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
|
||||||
|
|
||||||
return allLicenses.where((license) {
|
|
||||||
// 실제 License 모델에서 만료일 확인
|
|
||||||
if (license.expiryDate != null) {
|
|
||||||
final daysUntilExpiry = license.expiryDate!.difference(now).inDays;
|
|
||||||
return daysUntilExpiry > 0 && daysUntilExpiry <= days;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상태별 라이선스 개수 조회
|
|
||||||
Future<Map<String, int>> getLicenseStatusCounts() async {
|
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
|
||||||
try {
|
|
||||||
// API에서 상태별 개수 조회 (실제로는 별도 엔드포인트가 있다면 사용)
|
|
||||||
final activeCount = await _licenseService.getTotalLicenses(isActive: true);
|
|
||||||
final inactiveCount = await _licenseService.getTotalLicenses(isActive: false);
|
|
||||||
final expiringLicenses = await getExpiringLicenses(days: 30);
|
|
||||||
|
|
||||||
return {
|
|
||||||
'active': activeCount,
|
|
||||||
'inactive': inactiveCount,
|
|
||||||
'expiring': expiringLicenses.length,
|
|
||||||
'total': activeCount + inactiveCount,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {'active': 0, 'inactive': 0, 'expiring': 0, 'total': 0};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Mock 데이터에서 계산
|
|
||||||
final allLicenses = mockDataService?.getAllLicenses() ?? [];
|
|
||||||
final now = DateTime.now();
|
|
||||||
|
|
||||||
int activeCount = 0;
|
|
||||||
int expiredCount = 0;
|
|
||||||
int expiringCount = 0;
|
|
||||||
|
|
||||||
for (var license in allLicenses) {
|
|
||||||
if (license.isActive) {
|
|
||||||
activeCount++;
|
|
||||||
|
|
||||||
if (license.expiryDate != null) {
|
|
||||||
final daysUntilExpiry = license.expiryDate!.difference(now).inDays;
|
|
||||||
if (daysUntilExpiry <= 0) {
|
|
||||||
expiredCount++;
|
|
||||||
} else if (daysUntilExpiry <= 30) {
|
|
||||||
expiringCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'active': activeCount,
|
|
||||||
'inactive': allLicenses.length - activeCount,
|
|
||||||
'expiring': expiringCount,
|
|
||||||
'expired': expiredCount,
|
|
||||||
'total': allLicenses.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 정렬 변경
|
|
||||||
void sortBy(String field, String order) {
|
void sortBy(String field, String order) {
|
||||||
_sortBy = field;
|
_sortBy = field;
|
||||||
_sortOrder = order;
|
_sortOrder = order;
|
||||||
loadData();
|
loadData(isRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 상태 필터 변경
|
/// 라이선스 삭제 (BaseListController의 기본 기능 활용)
|
||||||
Future<void> changeStatusFilter(LicenseStatusFilter filter) async {
|
Future<void> deleteLicense(int id) async {
|
||||||
_statusFilter = filter;
|
await ErrorHandler.handleApiCall<void>(
|
||||||
await loadData();
|
() => _licenseService.deleteLicense(id),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// BaseListController의 removeItemLocally 활용
|
||||||
|
removeItemLocally((l) => l.id == id);
|
||||||
|
|
||||||
|
// 선택 목록에서도 제거
|
||||||
|
_selectedLicenseIds.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라이선스 선택/해제
|
/// 라이선스 선택/해제
|
||||||
void selectLicense(int? id, bool? isSelected) {
|
void selectLicense(int? id, bool? isSelected) {
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
|
|
||||||
@@ -427,11 +240,11 @@ class LicenseListController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전체 선택/해제
|
/// 전체 선택/해제
|
||||||
void selectAll(bool? isSelected) {
|
void selectAll(bool? isSelected) {
|
||||||
if (isSelected == true) {
|
if (isSelected == true) {
|
||||||
// 현재 필터링된 라이선스 모두 선택
|
// 현재 필터링된 라이선스 모두 선택
|
||||||
for (var license in _filteredLicenses) {
|
for (var license in items) {
|
||||||
if (license.id != null) {
|
if (license.id != null) {
|
||||||
_selectedLicenseIds.add(license.id!);
|
_selectedLicenseIds.add(license.id!);
|
||||||
}
|
}
|
||||||
@@ -443,131 +256,126 @@ class LicenseListController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 라이선스 목록 반환
|
/// 선택된 라이선스 목록 반환
|
||||||
List<License> getSelectedLicenses() {
|
List<License> getSelectedLicenses() {
|
||||||
return _filteredLicenses
|
return items
|
||||||
.where((l) => l.id != null && _selectedLicenseIds.contains(l.id))
|
.where((l) => l.id != null && _selectedLicenseIds.contains(l.id))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택 초기화
|
/// 선택 초기화 (BaseListController에도 있지만 라이선스 특화)
|
||||||
|
@override
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
_selectedLicenseIds.clear();
|
_selectedLicenseIds.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라이선스 할당
|
/// 선택된 라이선스 일괄 삭제
|
||||||
Future<bool> assignLicense(int licenseId, int userId) async {
|
|
||||||
try {
|
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
|
||||||
await _licenseService.assignLicense(licenseId, userId);
|
|
||||||
await loadData();
|
|
||||||
clearSelection();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 라이선스 할당 해제
|
|
||||||
Future<bool> unassignLicense(int licenseId) async {
|
|
||||||
try {
|
|
||||||
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
|
|
||||||
await _licenseService.unassignLicense(licenseId);
|
|
||||||
await loadData();
|
|
||||||
clearSelection();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 선택된 라이선스 일괄 삭제
|
|
||||||
Future<void> deleteSelectedLicenses() async {
|
Future<void> deleteSelectedLicenses() async {
|
||||||
if (_selectedLicenseIds.isEmpty) return;
|
for (final id in _selectedLicenseIds.toList()) {
|
||||||
|
await deleteLicense(id);
|
||||||
final selectedIds = List<int>.from(_selectedLicenseIds);
|
|
||||||
int successCount = 0;
|
|
||||||
int failCount = 0;
|
|
||||||
|
|
||||||
for (var id in selectedIds) {
|
|
||||||
try {
|
|
||||||
await deleteLicense(id);
|
|
||||||
successCount++;
|
|
||||||
} catch (e) {
|
|
||||||
failCount++;
|
|
||||||
debugPrint('라이선스 $id 삭제 실패: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_selectedLicenseIds.clear();
|
|
||||||
await loadData();
|
|
||||||
|
|
||||||
if (successCount > 0) {
|
|
||||||
debugPrint('✅ $successCount개 라이선스 삭제 완료');
|
|
||||||
}
|
|
||||||
if (failCount > 0) {
|
|
||||||
debugPrint('❌ $failCount개 라이선스 삭제 실패');
|
|
||||||
}
|
}
|
||||||
|
clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 통계 업데이트
|
/// 라이선스 생성
|
||||||
Future<void> _updateStatistics() async {
|
Future<void> createLicense(License license) async {
|
||||||
try {
|
await ErrorHandler.handleApiCall<void>(
|
||||||
final counts = await getLicenseStatusCounts();
|
() => _licenseService.createLicense(license),
|
||||||
|
onError: (failure) {
|
||||||
final now = DateTime.now();
|
throw failure;
|
||||||
int expiringSoonCount = 0;
|
},
|
||||||
int expiredCount = 0;
|
);
|
||||||
|
|
||||||
for (var license in _licenses) {
|
await refresh();
|
||||||
if (license.expiryDate != null) {
|
}
|
||||||
final days = license.expiryDate!.difference(now).inDays;
|
|
||||||
if (days <= 0) {
|
/// 라이선스 수정
|
||||||
expiredCount++;
|
Future<void> updateLicense(License license) async {
|
||||||
} else if (days <= 30) {
|
await ErrorHandler.handleApiCall<void>(
|
||||||
expiringSoonCount++;
|
() => _licenseService.updateLicense(license),
|
||||||
}
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
updateItemLocally(license, (l) => l.id == license.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 라이선스 활성화/비활성화 토글
|
||||||
|
Future<void> toggleLicenseStatus(int id) async {
|
||||||
|
final license = items.firstWhere((l) => l.id == id);
|
||||||
|
final updatedLicense = license.copyWith(isActive: !license.isActive);
|
||||||
|
|
||||||
|
await updateLicense(updatedLicense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 통계 데이터 업데이트
|
||||||
|
Future<void> _updateStatistics(List<License> licenses) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
_statistics = {
|
||||||
|
'total': licenses.length,
|
||||||
|
'active': licenses.where((l) => l.isActive).length,
|
||||||
|
'inactive': licenses.where((l) => !l.isActive).length,
|
||||||
|
'expiringSoon': licenses.where((l) {
|
||||||
|
if (l.expiryDate != null) {
|
||||||
|
final days = l.expiryDate!.difference(now).inDays;
|
||||||
|
return days > 0 && days <= 30;
|
||||||
}
|
}
|
||||||
}
|
return false;
|
||||||
|
}).length,
|
||||||
|
'expired': licenses.where((l) {
|
||||||
|
if (l.expiryDate != null) {
|
||||||
|
return l.expiryDate!.isBefore(now);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 라이선스 만료일별 그룹핑
|
||||||
|
Map<String, List<License>> getLicensesByExpiryPeriod() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final Map<String, List<License>> grouped = {
|
||||||
|
'이미 만료': [],
|
||||||
|
'${AppConstants.licenseExpiryWarningDays}일 이내': [],
|
||||||
|
'${AppConstants.licenseExpiryCautionDays}일 이내': [],
|
||||||
|
'${AppConstants.licenseExpiryInfoDays}일 이내': [],
|
||||||
|
'${AppConstants.licenseExpiryInfoDays}일 이후': [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (final license in items) {
|
||||||
|
if (license.expiryDate == null) continue;
|
||||||
|
|
||||||
_statistics = {
|
final days = license.expiryDate!.difference(now).inDays;
|
||||||
'total': counts['total'] ?? 0,
|
|
||||||
'active': counts['active'] ?? 0,
|
if (days < 0) {
|
||||||
'inactive': counts['inactive'] ?? 0,
|
grouped['이미 만료']!.add(license);
|
||||||
'expiringSoon': expiringSoonCount,
|
} else if (days <= AppConstants.licenseExpiryWarningDays) {
|
||||||
'expired': expiredCount,
|
grouped['${AppConstants.licenseExpiryWarningDays}일 이내']!.add(license);
|
||||||
};
|
} else if (days <= AppConstants.licenseExpiryCautionDays) {
|
||||||
} catch (e) {
|
grouped['${AppConstants.licenseExpiryCautionDays}일 이내']!.add(license);
|
||||||
debugPrint('❌ 통계 업데이트 오류: $e');
|
} else if (days <= AppConstants.licenseExpiryInfoDays) {
|
||||||
// 오류 발생 시 기본값 사용
|
grouped['${AppConstants.licenseExpiryInfoDays}일 이내']!.add(license);
|
||||||
_statistics = {
|
} else {
|
||||||
'total': _licenses.length,
|
grouped['${AppConstants.licenseExpiryInfoDays}일 이후']!.add(license);
|
||||||
'active': 0,
|
}
|
||||||
'inactive': 0,
|
|
||||||
'expiringSoon': 0,
|
|
||||||
'expired': 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return grouped;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 만료일까지 남은 일수 계산
|
/// 만료까지 남은 날짜 계산
|
||||||
int? getDaysUntilExpiry(License license) {
|
int getDaysUntilExpiry(DateTime? expiryDate) {
|
||||||
if (license.expiryDate == null) return null;
|
if (expiryDate == null) return 999; // 만료일이 없으면 큰 숫자 반환
|
||||||
return license.expiryDate!.difference(DateTime.now()).inDays;
|
final now = DateTime.now();
|
||||||
|
return expiryDate.difference(now).inDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_debounceTimer?.cancel();
|
_debounceTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../core/controllers/base_list_controller.dart';
|
||||||
|
import '../../../core/utils/error_handler.dart';
|
||||||
|
import '../../../data/models/common/pagination_params.dart';
|
||||||
|
import '../../../data/models/license/license_dto.dart';
|
||||||
|
import '../../../domain/usecases/license/license_usecases.dart';
|
||||||
|
|
||||||
|
/// UseCase 패턴을 적용한 라이선스 목록 컨트롤러
|
||||||
|
class LicenseListControllerWithUseCase extends BaseListController<LicenseDto> {
|
||||||
|
final GetLicensesUseCase getLicensesUseCase;
|
||||||
|
final CreateLicenseUseCase createLicenseUseCase;
|
||||||
|
final UpdateLicenseUseCase updateLicenseUseCase;
|
||||||
|
final DeleteLicenseUseCase deleteLicenseUseCase;
|
||||||
|
final CheckLicenseExpiryUseCase checkLicenseExpiryUseCase;
|
||||||
|
|
||||||
|
// 선택된 항목들
|
||||||
|
final Set<int> _selectedLicenseIds = {};
|
||||||
|
Set<int> get selectedLicenseIds => _selectedLicenseIds;
|
||||||
|
|
||||||
|
// 필터 옵션
|
||||||
|
String? _filterByCompany;
|
||||||
|
String? _filterByExpiry;
|
||||||
|
DateTime? _filterStartDate;
|
||||||
|
DateTime? _filterEndDate;
|
||||||
|
|
||||||
|
String? get filterByCompany => _filterByCompany;
|
||||||
|
String? get filterByExpiry => _filterByExpiry;
|
||||||
|
DateTime? get filterStartDate => _filterStartDate;
|
||||||
|
DateTime? get filterEndDate => _filterEndDate;
|
||||||
|
|
||||||
|
// 만료 임박 라이선스 정보
|
||||||
|
LicenseExpiryResult? _expiryResult;
|
||||||
|
LicenseExpiryResult? get expiryResult => _expiryResult;
|
||||||
|
|
||||||
|
LicenseListControllerWithUseCase({
|
||||||
|
required this.getLicensesUseCase,
|
||||||
|
required this.createLicenseUseCase,
|
||||||
|
required this.updateLicenseUseCase,
|
||||||
|
required this.deleteLicenseUseCase,
|
||||||
|
required this.checkLicenseExpiryUseCase,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PagedResult<LicenseDto>> fetchData({
|
||||||
|
required PaginationParams params,
|
||||||
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 필터 파라미터 구성
|
||||||
|
final filters = <String, dynamic>{};
|
||||||
|
if (_filterByCompany != null) filters['company_id'] = _filterByCompany;
|
||||||
|
if (_filterByExpiry != null) filters['expiry'] = _filterByExpiry;
|
||||||
|
if (_filterStartDate != null) filters['start_date'] = _filterStartDate!.toIso8601String();
|
||||||
|
if (_filterEndDate != null) filters['end_date'] = _filterEndDate!.toIso8601String();
|
||||||
|
|
||||||
|
final updatedParams = params.copyWith(filters: filters);
|
||||||
|
final getParams = GetLicensesParams.fromPaginationParams(updatedParams);
|
||||||
|
|
||||||
|
final result = await getLicensesUseCase(getParams);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) => throw Exception(failure.message),
|
||||||
|
(licenseResponse) {
|
||||||
|
// PagedResult로 래핑하여 반환
|
||||||
|
final meta = PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: licenseResponse.items.length, // 실제로는 서버에서 받아와야 함
|
||||||
|
totalPages: (licenseResponse.items.length / params.perPage).ceil(),
|
||||||
|
hasNext: licenseResponse.items.length >= params.perPage,
|
||||||
|
hasPrevious: params.page > 1,
|
||||||
|
);
|
||||||
|
return PagedResult(items: licenseResponse.items, meta: meta);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('데이터 로드 실패: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 만료 임박 라이선스 체크
|
||||||
|
Future<void> checkExpiringLicenses() async {
|
||||||
|
try {
|
||||||
|
final params = CheckLicenseExpiryParams(
|
||||||
|
companyId: _filterByCompany != null ? int.tryParse(_filterByCompany!) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await checkLicenseExpiryUseCase(params);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => errorState = failure.message,
|
||||||
|
(expiryResult) {
|
||||||
|
_expiryResult = expiryResult;
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '라이선스 만료 체크 실패: $e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 라이선스 생성
|
||||||
|
Future<void> createLicense({
|
||||||
|
required int equipmentId,
|
||||||
|
required int companyId,
|
||||||
|
required String licenseType,
|
||||||
|
required DateTime startDate,
|
||||||
|
required DateTime expiryDate,
|
||||||
|
String? description,
|
||||||
|
double? cost,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
final params = CreateLicenseParams(
|
||||||
|
equipmentId: equipmentId,
|
||||||
|
companyId: companyId,
|
||||||
|
licenseType: licenseType,
|
||||||
|
startDate: startDate,
|
||||||
|
expiryDate: expiryDate,
|
||||||
|
description: description,
|
||||||
|
cost: cost,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await createLicenseUseCase(params);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async => errorState = failure.message,
|
||||||
|
(license) async {
|
||||||
|
await refresh();
|
||||||
|
await checkExpiringLicenses();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 생성: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 라이선스 수정
|
||||||
|
Future<void> updateLicense({
|
||||||
|
required int id,
|
||||||
|
int? equipmentId,
|
||||||
|
int? companyId,
|
||||||
|
String? licenseType,
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? expiryDate,
|
||||||
|
String? description,
|
||||||
|
double? cost,
|
||||||
|
String? status,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
final params = UpdateLicenseParams(
|
||||||
|
id: id,
|
||||||
|
equipmentId: equipmentId,
|
||||||
|
companyId: companyId,
|
||||||
|
licenseType: licenseType,
|
||||||
|
startDate: startDate,
|
||||||
|
expiryDate: expiryDate,
|
||||||
|
description: description,
|
||||||
|
cost: cost,
|
||||||
|
status: status,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await updateLicenseUseCase(params);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async => errorState = failure.message,
|
||||||
|
(license) async {
|
||||||
|
updateItemLocally(license, (item) => item.id == license.id);
|
||||||
|
await checkExpiringLicenses();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 생성: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 라이선스 삭제
|
||||||
|
Future<void> deleteLicense(int id) async {
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
final result = await deleteLicenseUseCase(id);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async => errorState = failure.message,
|
||||||
|
(_) async {
|
||||||
|
removeItemLocally((item) => item.id == id);
|
||||||
|
_selectedLicenseIds.remove(id);
|
||||||
|
await checkExpiringLicenses();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 생성: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 설정
|
||||||
|
void setFilters({
|
||||||
|
String? company,
|
||||||
|
String? expiry,
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? endDate,
|
||||||
|
}) {
|
||||||
|
_filterByCompany = company;
|
||||||
|
_filterByExpiry = expiry;
|
||||||
|
_filterStartDate = startDate;
|
||||||
|
_filterEndDate = endDate;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 초기화
|
||||||
|
void clearFilters() {
|
||||||
|
_filterByCompany = null;
|
||||||
|
_filterByExpiry = null;
|
||||||
|
_filterStartDate = null;
|
||||||
|
_filterEndDate = null;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 라이선스 선택 토글
|
||||||
|
void toggleLicenseSelection(int id) {
|
||||||
|
if (_selectedLicenseIds.contains(id)) {
|
||||||
|
_selectedLicenseIds.remove(id);
|
||||||
|
} else {
|
||||||
|
_selectedLicenseIds.add(id);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 모든 라이선스 선택
|
||||||
|
void selectAll() {
|
||||||
|
_selectedLicenseIds.clear();
|
||||||
|
_selectedLicenseIds.addAll(items.map((e) => e.id));
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택 해제
|
||||||
|
void clearSelection() {
|
||||||
|
_selectedLicenseIds.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 라이선스 일괄 삭제
|
||||||
|
Future<void> deleteSelectedLicenses() async {
|
||||||
|
if (_selectedLicenseIds.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
for (final id in _selectedLicenseIds.toList()) {
|
||||||
|
final result = await deleteLicenseUseCase(id);
|
||||||
|
result.fold(
|
||||||
|
(failure) => print('Failed to delete license $id: ${failure.message}'),
|
||||||
|
(_) => removeItemLocally((item) => item.id == id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedLicenseIds.clear();
|
||||||
|
await checkExpiringLicenses();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 생성: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_selectedLicenseIds.clear();
|
||||||
|
_expiryResult = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import 'package:superport/screens/license/controllers/license_form_controller.da
|
|||||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||||
import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
import 'package:superport/screens/common/custom_widgets.dart' hide FormFieldWrapper;
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/utils/validators.dart';
|
import 'package:superport/utils/validators.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:superport/core/config/environment.dart' as env;
|
import 'package:superport/core/config/environment.dart' as env;
|
||||||
@@ -51,8 +50,6 @@ class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
|
|||||||
debugPrint('📌 라이선스 폼 초기화 - API 모드: $useApi');
|
debugPrint('📌 라이선스 폼 초기화 - API 모드: $useApi');
|
||||||
|
|
||||||
_controller = LicenseFormController(
|
_controller = LicenseFormController(
|
||||||
useApi: useApi,
|
|
||||||
dataService: useApi ? null : MockDataService(),
|
|
||||||
licenseId: widget.maintenanceId,
|
licenseId: widget.maintenanceId,
|
||||||
isExtension: widget.isExtension,
|
isExtension: widget.isExtension,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import 'package:superport/screens/common/widgets/standard_states.dart';
|
|||||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||||
import 'package:superport/screens/license/controllers/license_list_controller.dart';
|
import 'package:superport/screens/license/controllers/license_list_controller.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/core/config/environment.dart' as env;
|
import 'package:superport/core/config/environment.dart' as env;
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ class LicenseListRedesign extends StatefulWidget {
|
|||||||
|
|
||||||
class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
||||||
late final LicenseListController _controller;
|
late final LicenseListController _controller;
|
||||||
final MockDataService _dataService = MockDataService();
|
// MockDataService 제거 - 실제 API 사용
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final ScrollController _horizontalScrollController = ScrollController();
|
final ScrollController _horizontalScrollController = ScrollController();
|
||||||
int _currentPage = 1;
|
int _currentPage = 1;
|
||||||
@@ -45,10 +44,7 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
|||||||
|
|
||||||
// 실제 API 사용 여부에 따라 컨트롤러 초기화
|
// 실제 API 사용 여부에 따라 컨트롤러 초기화
|
||||||
final useApi = env.Environment.useApi;
|
final useApi = env.Environment.useApi;
|
||||||
_controller = LicenseListController(
|
_controller = LicenseListController();
|
||||||
useApi: useApi,
|
|
||||||
mockDataService: useApi ? null : _dataService,
|
|
||||||
);
|
|
||||||
|
|
||||||
debugPrint('📌 Controller 모드: ${useApi ? "Real API" : "Mock Data"}');
|
debugPrint('📌 Controller 모드: ${useApi ? "Real API" : "Mock Data"}');
|
||||||
debugPrint('==========================================\n');
|
debugPrint('==========================================\n');
|
||||||
@@ -611,7 +607,7 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
|
|||||||
final displayIndex = entry.key;
|
final displayIndex = entry.key;
|
||||||
final license = entry.value;
|
final license = entry.value;
|
||||||
final index = (_currentPage - 1) * _pageSize + displayIndex;
|
final index = (_currentPage - 1) * _pageSize + displayIndex;
|
||||||
final daysRemaining = _controller.getDaysUntilExpiry(license);
|
final daysRemaining = _controller.getDaysUntilExpiry(license.expiryDate);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
|||||||
193
lib/screens/login/controllers/login_controller_with_usecase.dart
Normal file
193
lib/screens/login/controllers/login_controller_with_usecase.dart
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
import '../../../domain/usecases/base_usecase.dart';
|
||||||
|
import '../../../domain/usecases/auth/login_usecase.dart';
|
||||||
|
import '../../../domain/usecases/auth/check_auth_status_usecase.dart';
|
||||||
|
import '../../../services/auth_service.dart';
|
||||||
|
import '../../../services/health_check_service.dart';
|
||||||
|
import '../../../di/injection_container.dart';
|
||||||
|
|
||||||
|
/// UseCase를 활용한 로그인 화면 컨트롤러
|
||||||
|
/// 비즈니스 로직을 UseCase로 분리하여 테스트 용이성과 재사용성 향상
|
||||||
|
class LoginControllerWithUseCase extends ChangeNotifier {
|
||||||
|
// UseCases
|
||||||
|
late final LoginUseCase _loginUseCase;
|
||||||
|
late final CheckAuthStatusUseCase _checkAuthStatusUseCase;
|
||||||
|
|
||||||
|
// Services
|
||||||
|
final HealthCheckService _healthCheckService = HealthCheckService();
|
||||||
|
|
||||||
|
// UI Controllers
|
||||||
|
final TextEditingController idController = TextEditingController();
|
||||||
|
final TextEditingController pwController = TextEditingController();
|
||||||
|
|
||||||
|
// Focus Nodes
|
||||||
|
final FocusNode idFocus = FocusNode();
|
||||||
|
final FocusNode pwFocus = FocusNode();
|
||||||
|
|
||||||
|
// State
|
||||||
|
bool saveId = false;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
String? get errorMessage => _errorMessage;
|
||||||
|
|
||||||
|
LoginControllerWithUseCase() {
|
||||||
|
// UseCase 초기화
|
||||||
|
final authService = inject<AuthService>();
|
||||||
|
_loginUseCase = LoginUseCase(authService);
|
||||||
|
_checkAuthStatusUseCase = CheckAuthStatusUseCase(authService);
|
||||||
|
|
||||||
|
// 초기 인증 상태 확인
|
||||||
|
_checkInitialAuthStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 초기 인증 상태 확인
|
||||||
|
Future<void> _checkInitialAuthStatus() async {
|
||||||
|
final result = await _checkAuthStatusUseCase(const NoParams());
|
||||||
|
result.fold(
|
||||||
|
(failure) => debugPrint('인증 상태 확인 실패: ${failure.message}'),
|
||||||
|
(isAuthenticated) {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
debugPrint('이미 로그인된 상태입니다.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 아이디 저장 체크박스 상태 변경
|
||||||
|
void setSaveId(bool value) {
|
||||||
|
saveId = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 에러 메시지 초기화
|
||||||
|
void clearError() {
|
||||||
|
_errorMessage = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 로그인 처리
|
||||||
|
Future<bool> login() async {
|
||||||
|
// 입력값 검증 (UI 레벨)
|
||||||
|
if (idController.text.trim().isEmpty) {
|
||||||
|
_errorMessage = '아이디 또는 이메일을 입력해주세요.';
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pwController.text.isEmpty) {
|
||||||
|
_errorMessage = '비밀번호를 입력해주세요.';
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로딩 시작
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// 입력값이 이메일인지 username인지 판단
|
||||||
|
final inputValue = idController.text.trim();
|
||||||
|
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||||
|
final isEmail = emailRegex.hasMatch(inputValue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// UseCase 실행
|
||||||
|
final params = LoginParams(
|
||||||
|
email: isEmail ? inputValue : '$inputValue@superport.kr', // username인 경우 임시 도메인 추가
|
||||||
|
password: pwController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint('[LoginController] 로그인 시도: ${params.email}');
|
||||||
|
|
||||||
|
final result = await _loginUseCase(params).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () async {
|
||||||
|
debugPrint('[LoginController] 로그인 요청 타임아웃');
|
||||||
|
return Left(NetworkFailure(
|
||||||
|
message: '요청 시간이 초과되었습니다. 네트워크 연결을 확인해주세요.',
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) {
|
||||||
|
debugPrint('[LoginController] 로그인 실패: ${failure.message}');
|
||||||
|
|
||||||
|
// 실패 타입에 따른 메시지 처리
|
||||||
|
if (failure is ValidationFailure) {
|
||||||
|
_errorMessage = failure.message;
|
||||||
|
} else if (failure is AuthenticationFailure) {
|
||||||
|
_errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.';
|
||||||
|
} else if (failure is NetworkFailure) {
|
||||||
|
_errorMessage = '네트워크 연결을 확인해주세요.';
|
||||||
|
} else if (failure is ServerFailure) {
|
||||||
|
_errorMessage = '서버 오류가 발생했습니다.\n잠시 후 다시 시도해주세요.';
|
||||||
|
} else {
|
||||||
|
_errorMessage = failure.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
(loginResponse) {
|
||||||
|
debugPrint('[LoginController] 로그인 성공');
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[LoginController] 예상치 못한 에러: $e');
|
||||||
|
_errorMessage = '로그인 중 오류가 발생했습니다.';
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 헬스체크 실행
|
||||||
|
Future<bool> performHealthCheck() async {
|
||||||
|
debugPrint('[LoginController] 헬스체크 시작');
|
||||||
|
_isLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final healthResult = await _healthCheckService.checkHealth();
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// HealthCheckService가 Map을 반환하는 경우 적절히 변환
|
||||||
|
final isHealthy = healthResult is bool ? healthResult :
|
||||||
|
(healthResult is Map && healthResult['status'] == 'healthy');
|
||||||
|
|
||||||
|
if (isHealthy == false) {
|
||||||
|
_errorMessage = '서버와 연결할 수 없습니다.\n잠시 후 다시 시도해주세요.';
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[LoginController] 헬스체크 실패: $e');
|
||||||
|
_errorMessage = '서버 상태 확인 중 오류가 발생했습니다.';
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
idController.dispose();
|
||||||
|
pwController.dispose();
|
||||||
|
idFocus.dispose();
|
||||||
|
pwFocus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,15 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||||
import 'package:superport/screens/overview/controllers/overview_controller.dart';
|
import 'package:superport/screens/overview/controllers/overview_controller.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
// MockDataService 제거 - 실제 API 사용
|
||||||
|
import 'package:superport/services/auth_service.dart';
|
||||||
|
import 'package:superport/services/health_check_service.dart';
|
||||||
|
import 'package:superport/core/widgets/auth_guard.dart';
|
||||||
|
import 'package:superport/data/models/auth/auth_user.dart';
|
||||||
|
|
||||||
/// shadcn/ui 스타일로 재설계된 대시보드 화면
|
/// shadcn/ui 스타일로 재설계된 대시보드 화면
|
||||||
class OverviewScreenRedesign extends StatefulWidget {
|
class OverviewScreenRedesign extends StatefulWidget {
|
||||||
const OverviewScreenRedesign({Key? key}) : super(key: key);
|
const OverviewScreenRedesign({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<OverviewScreenRedesign> createState() => _OverviewScreenRedesignState();
|
State<OverviewScreenRedesign> createState() => _OverviewScreenRedesignState();
|
||||||
@@ -16,21 +20,44 @@ class OverviewScreenRedesign extends StatefulWidget {
|
|||||||
|
|
||||||
class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
||||||
late final OverviewController _controller;
|
late final OverviewController _controller;
|
||||||
|
late final HealthCheckService _healthCheckService;
|
||||||
|
Map<String, dynamic>? _healthStatus;
|
||||||
|
bool _isHealthCheckLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = OverviewController();
|
_controller = OverviewController();
|
||||||
|
_healthCheckService = HealthCheckService();
|
||||||
_loadData();
|
_loadData();
|
||||||
|
_checkHealthStatus();
|
||||||
|
// 주기적인 헬스체크 시작 (30초마다)
|
||||||
|
_healthCheckService.startPeriodicHealthCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadData() async {
|
Future<void> _loadData() async {
|
||||||
await _controller.loadDashboardData();
|
await _controller.loadDashboardData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkHealthStatus() async {
|
||||||
|
setState(() {
|
||||||
|
_isHealthCheckLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await _healthCheckService.checkHealth();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_healthStatus = result;
|
||||||
|
_isHealthCheckLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
|
_healthCheckService.stopPeriodicHealthCheck();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +97,13 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('안녕하세요, 관리자님! 👋', style: ShadcnTheme.headingH3),
|
FutureBuilder<AuthUser?>(
|
||||||
|
future: context.read<AuthService>().getCurrentUser(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final userName = snapshot.data?.name ?? '사용자';
|
||||||
|
return Text('안녕하세요, $userName님! 👋', style: ShadcnTheme.headingH3);
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'오늘의 포트 운영 현황을 확인해보세요.',
|
'오늘의 포트 운영 현황을 확인해보세요.',
|
||||||
@@ -333,51 +366,79 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRightColumn() {
|
Widget _buildRightColumn() {
|
||||||
return Column(
|
return FutureBuilder<AuthUser?>(
|
||||||
children: [
|
future: context.read<AuthService>().getCurrentUser(),
|
||||||
// 빠른 작업
|
builder: (context, snapshot) {
|
||||||
ShadcnCard(
|
final userRole = snapshot.data?.role?.toLowerCase() ?? '';
|
||||||
padding: const EdgeInsets.all(24),
|
final isAdminOrManager = userRole == 'admin' || userRole == 'manager';
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text('빠른 작업', style: ShadcnTheme.headingH4),
|
// 빠른 작업 (Admin과 Manager만 표시)
|
||||||
const SizedBox(height: 16),
|
if (isAdminOrManager) ...[
|
||||||
_buildQuickActionButton(Icons.add_box, '장비 입고', '새 장비 등록'),
|
ShadcnCard(
|
||||||
const SizedBox(height: 12),
|
padding: const EdgeInsets.all(24),
|
||||||
_buildQuickActionButton(
|
child: Column(
|
||||||
Icons.local_shipping,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'장비 출고',
|
children: [
|
||||||
'장비 대여 처리',
|
Text('빠른 작업', style: ShadcnTheme.headingH4),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 12),
|
_buildQuickActionButton(Icons.add_box, '장비 입고', '새 장비 등록'),
|
||||||
_buildQuickActionButton(
|
const SizedBox(height: 12),
|
||||||
Icons.business_center,
|
_buildQuickActionButton(
|
||||||
'회사 등록',
|
Icons.local_shipping,
|
||||||
'새 회사 추가',
|
'장비 출고',
|
||||||
),
|
'장비 대여 처리',
|
||||||
],
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildQuickActionButton(
|
||||||
|
Icons.business_center,
|
||||||
|
'회사 등록',
|
||||||
|
'새 회사 추가',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
// 시스템 상태 (실시간 헬스체크)
|
||||||
|
ShadcnCard(
|
||||||
// 시스템 상태
|
padding: const EdgeInsets.all(24),
|
||||||
ShadcnCard(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(24),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Text('시스템 상태', style: ShadcnTheme.headingH4),
|
children: [
|
||||||
const SizedBox(height: 16),
|
Text('시스템 상태', style: ShadcnTheme.headingH4),
|
||||||
_buildStatusItem('서버 상태', '정상'),
|
IconButton(
|
||||||
_buildStatusItem('데이터베이스', '정상'),
|
icon: _isHealthCheckLoading
|
||||||
_buildStatusItem('네트워크', '정상'),
|
? SizedBox(
|
||||||
_buildStatusItem('백업', '완료'),
|
width: 16,
|
||||||
],
|
height: 16,
|
||||||
),
|
child: CircularProgressIndicator(
|
||||||
),
|
strokeWidth: 2,
|
||||||
],
|
valueColor: AlwaysStoppedAnimation<Color>(ShadcnTheme.primary),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(Icons.refresh, size: 20, color: ShadcnTheme.muted),
|
||||||
|
onPressed: _isHealthCheckLoading ? null : _checkHealthStatus,
|
||||||
|
tooltip: '새로고침',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildHealthStatusItem('서버 상태', _getServerStatus()),
|
||||||
|
_buildHealthStatusItem('데이터베이스', _getDatabaseStatus()),
|
||||||
|
_buildHealthStatusItem('API 응답', _getApiResponseTime()),
|
||||||
|
_buildHealthStatusItem('최종 체크', _getLastCheckTime()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,4 +725,151 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 헬스 상태 아이템 빌더
|
||||||
|
Widget _buildHealthStatusItem(String label, Map<String, dynamic> statusInfo) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(label, style: ShadcnTheme.bodyMedium),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (statusInfo['icon'] != null) ...[
|
||||||
|
Icon(
|
||||||
|
statusInfo['icon'] as IconData,
|
||||||
|
size: 16,
|
||||||
|
color: statusInfo['color'] as Color,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
ShadcnBadge(
|
||||||
|
text: statusInfo['text'] as String,
|
||||||
|
variant: statusInfo['variant'] as ShadcnBadgeVariant,
|
||||||
|
size: ShadcnBadgeSize.small,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 서버 상태 가져오기
|
||||||
|
Map<String, dynamic> _getServerStatus() {
|
||||||
|
if (_healthStatus == null) {
|
||||||
|
return {
|
||||||
|
'text': '확인 중',
|
||||||
|
'variant': ShadcnBadgeVariant.secondary,
|
||||||
|
'icon': Icons.pending,
|
||||||
|
'color': ShadcnTheme.muted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final isHealthy = _healthStatus!['success'] == true &&
|
||||||
|
_healthStatus!['data']?['status'] == 'healthy';
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': isHealthy ? '정상' : '오류',
|
||||||
|
'variant': isHealthy ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.destructive,
|
||||||
|
'icon': isHealthy ? Icons.check_circle : Icons.error,
|
||||||
|
'color': isHealthy ? ShadcnTheme.success : ShadcnTheme.destructive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 데이터베이스 상태 가져오기
|
||||||
|
Map<String, dynamic> _getDatabaseStatus() {
|
||||||
|
if (_healthStatus == null) {
|
||||||
|
return {
|
||||||
|
'text': '확인 중',
|
||||||
|
'variant': ShadcnBadgeVariant.secondary,
|
||||||
|
'icon': Icons.pending,
|
||||||
|
'color': ShadcnTheme.muted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final dbStatus = _healthStatus!['data']?['database']?['status'] ?? 'unknown';
|
||||||
|
final isConnected = dbStatus == 'connected';
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': isConnected ? '연결됨' : '연결 안됨',
|
||||||
|
'variant': isConnected ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.warning,
|
||||||
|
'icon': isConnected ? Icons.storage : Icons.cloud_off,
|
||||||
|
'color': isConnected ? ShadcnTheme.success : ShadcnTheme.warning,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// API 응답 시간 가져오기
|
||||||
|
Map<String, dynamic> _getApiResponseTime() {
|
||||||
|
if (_healthStatus == null) {
|
||||||
|
return {
|
||||||
|
'text': '측정 중',
|
||||||
|
'variant': ShadcnBadgeVariant.secondary,
|
||||||
|
'icon': Icons.timer,
|
||||||
|
'color': ShadcnTheme.muted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final responseTime = _healthStatus!['data']?['responseTime'] ?? 0;
|
||||||
|
final timeMs = responseTime is num ? responseTime : 0;
|
||||||
|
|
||||||
|
ShadcnBadgeVariant variant;
|
||||||
|
Color color;
|
||||||
|
if (timeMs < 100) {
|
||||||
|
variant = ShadcnBadgeVariant.success;
|
||||||
|
color = ShadcnTheme.success;
|
||||||
|
} else if (timeMs < 500) {
|
||||||
|
variant = ShadcnBadgeVariant.warning;
|
||||||
|
color = ShadcnTheme.warning;
|
||||||
|
} else {
|
||||||
|
variant = ShadcnBadgeVariant.destructive;
|
||||||
|
color = ShadcnTheme.destructive;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': '${timeMs}ms',
|
||||||
|
'variant': variant,
|
||||||
|
'icon': Icons.speed,
|
||||||
|
'color': color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 마지막 체크 시간 가져오기
|
||||||
|
Map<String, dynamic> _getLastCheckTime() {
|
||||||
|
if (_healthStatus == null) {
|
||||||
|
return {
|
||||||
|
'text': '없음',
|
||||||
|
'variant': ShadcnBadgeVariant.secondary,
|
||||||
|
'icon': Icons.access_time,
|
||||||
|
'color': ShadcnTheme.muted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final timestamp = _healthStatus!['data']?['timestamp'];
|
||||||
|
if (timestamp != null) {
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(timestamp);
|
||||||
|
final formatter = DateFormat('HH:mm:ss');
|
||||||
|
return {
|
||||||
|
'text': formatter.format(date),
|
||||||
|
'variant': ShadcnBadgeVariant.outline,
|
||||||
|
'icon': Icons.access_time,
|
||||||
|
'color': ShadcnTheme.foreground,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// 파싱 실패
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 시간 사용
|
||||||
|
final now = DateTime.now();
|
||||||
|
final formatter = DateFormat('HH:mm:ss');
|
||||||
|
return {
|
||||||
|
'text': formatter.format(now),
|
||||||
|
'variant': ShadcnBadgeVariant.outline,
|
||||||
|
'icon': Icons.access_time,
|
||||||
|
'color': ShadcnTheme.foreground,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:superport/models/company_model.dart';
|
import 'package:superport/models/company_model.dart';
|
||||||
import 'package:superport/models/user_model.dart';
|
import 'package:superport/models/user_model.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/services/user_service.dart';
|
import 'package:superport/services/user_service.dart';
|
||||||
|
import 'package:superport/services/company_service.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
import 'package:superport/models/user_phone_field.dart';
|
import 'package:superport/models/user_phone_field.dart';
|
||||||
|
|
||||||
// 사용자 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
// 사용자 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||||
class UserFormController extends ChangeNotifier {
|
class UserFormController extends ChangeNotifier {
|
||||||
final MockDataService dataService;
|
|
||||||
final UserService _userService = GetIt.instance<UserService>();
|
final UserService _userService = GetIt.instance<UserService>();
|
||||||
|
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
// 상태 변수
|
// 상태 변수
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
String? _error;
|
String? _error;
|
||||||
bool _useApi = true; // Feature flag
|
// API만 사용
|
||||||
|
|
||||||
// 폼 필드
|
// 폼 필드
|
||||||
bool isEditMode = false;
|
bool isEditMode = false;
|
||||||
@@ -50,7 +50,7 @@ class UserFormController extends ChangeNotifier {
|
|||||||
bool get isCheckingUsername => _isCheckingUsername;
|
bool get isCheckingUsername => _isCheckingUsername;
|
||||||
bool? get isUsernameAvailable => _isUsernameAvailable;
|
bool? get isUsernameAvailable => _isUsernameAvailable;
|
||||||
|
|
||||||
UserFormController({required this.dataService, this.userId}) {
|
UserFormController({this.userId}) {
|
||||||
isEditMode = userId != null;
|
isEditMode = userId != null;
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
loadUser();
|
loadUser();
|
||||||
@@ -61,15 +61,29 @@ class UserFormController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 회사 목록 로드
|
// 회사 목록 로드
|
||||||
void loadCompanies() {
|
Future<void> loadCompanies() async {
|
||||||
companies = dataService.getAllCompanies();
|
try {
|
||||||
notifyListeners();
|
final result = await _companyService.getCompanies();
|
||||||
|
companies = result;
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('회사 목록 로드 실패: $e');
|
||||||
|
companies = [];
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 회사 ID에 따라 지점 목록 로드
|
// 회사 ID에 따라 지점 목록 로드
|
||||||
void loadBranches(int companyId) {
|
void loadBranches(int companyId) {
|
||||||
final company = dataService.getCompanyById(companyId);
|
final company = companies.firstWhere(
|
||||||
branches = company?.branches ?? [];
|
(c) => c.id == companyId,
|
||||||
|
orElse: () => Company(
|
||||||
|
id: companyId,
|
||||||
|
name: '알 수 없는 회사',
|
||||||
|
branches: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
branches = company.branches ?? [];
|
||||||
// 지점 변경 시 이전 선택 지점이 새 회사에 없으면 초기화
|
// 지점 변경 시 이전 선택 지점이 새 회사에 없으면 초기화
|
||||||
if (branchId != null && !branches.any((b) => b.id == branchId)) {
|
if (branchId != null && !branches.any((b) => b.id == branchId)) {
|
||||||
branchId = null;
|
branchId = null;
|
||||||
@@ -86,13 +100,7 @@ class UserFormController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
User? user;
|
final user = await _userService.getUser(userId!);
|
||||||
|
|
||||||
if (_useApi) {
|
|
||||||
user = await _userService.getUser(userId!);
|
|
||||||
} else {
|
|
||||||
user = dataService.getUserById(userId!);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
name = user.name;
|
name = user.name;
|
||||||
@@ -155,15 +163,8 @@ class UserFormController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (_useApi) {
|
final isDuplicate = await _userService.checkDuplicateUsername(value);
|
||||||
final isDuplicate = await _userService.checkDuplicateUsername(value);
|
_isUsernameAvailable = !isDuplicate;
|
||||||
_isUsernameAvailable = !isDuplicate;
|
|
||||||
} else {
|
|
||||||
// Mock 데이터에서 중복 확인
|
|
||||||
final users = dataService.getAllUsers();
|
|
||||||
final exists = users.any((u) => u.username == value && u.id != userId);
|
|
||||||
_isUsernameAvailable = !exists;
|
|
||||||
}
|
|
||||||
_lastCheckedUsername = value;
|
_lastCheckedUsername = value;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_isUsernameAvailable = null;
|
_isUsernameAvailable = null;
|
||||||
@@ -217,81 +218,32 @@ class UserFormController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_useApi) {
|
if (isEditMode && userId != null) {
|
||||||
if (isEditMode && userId != null) {
|
// 사용자 수정
|
||||||
// 사용자 수정
|
await _userService.updateUser(
|
||||||
await _userService.updateUser(
|
userId!,
|
||||||
userId!,
|
name: name,
|
||||||
name: name,
|
email: email.isNotEmpty ? email : null,
|
||||||
email: email.isNotEmpty ? email : null,
|
phone: phoneNumber,
|
||||||
phone: phoneNumber,
|
companyId: companyId,
|
||||||
companyId: companyId,
|
branchId: branchId,
|
||||||
branchId: branchId,
|
role: role,
|
||||||
role: role,
|
position: position.isNotEmpty ? position : null,
|
||||||
position: position.isNotEmpty ? position : null,
|
password: password.isNotEmpty ? password : null,
|
||||||
password: password.isNotEmpty ? password : null,
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 사용자 생성
|
|
||||||
await _userService.createUser(
|
|
||||||
username: username,
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
name: name,
|
|
||||||
role: role,
|
|
||||||
companyId: companyId!,
|
|
||||||
branchId: branchId,
|
|
||||||
phone: phoneNumber,
|
|
||||||
position: position.isNotEmpty ? position : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Mock 데이터 사용
|
// 사용자 생성
|
||||||
List<Map<String, String>> phoneNumbersList = [];
|
await _userService.createUser(
|
||||||
for (var phoneField in phoneFields) {
|
username: username,
|
||||||
if (phoneField.number.isNotEmpty) {
|
email: email,
|
||||||
phoneNumbersList.add({
|
password: password,
|
||||||
'type': phoneField.type,
|
name: name,
|
||||||
'number': phoneField.number,
|
role: role,
|
||||||
});
|
companyId: companyId!,
|
||||||
}
|
branchId: branchId,
|
||||||
}
|
phone: phoneNumber,
|
||||||
|
position: position.isNotEmpty ? position : null,
|
||||||
if (isEditMode && userId != null) {
|
);
|
||||||
final user = dataService.getUserById(userId!);
|
|
||||||
if (user != null) {
|
|
||||||
final updatedUser = User(
|
|
||||||
id: user.id,
|
|
||||||
companyId: companyId!,
|
|
||||||
branchId: branchId,
|
|
||||||
name: name,
|
|
||||||
role: role,
|
|
||||||
position: position.isNotEmpty ? position : null,
|
|
||||||
email: email.isNotEmpty ? email : null,
|
|
||||||
phoneNumbers: phoneNumbersList,
|
|
||||||
username: username.isNotEmpty ? username : null,
|
|
||||||
isActive: user.isActive,
|
|
||||||
createdAt: user.createdAt,
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
dataService.updateUser(updatedUser);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final newUser = User(
|
|
||||||
companyId: companyId!,
|
|
||||||
branchId: branchId,
|
|
||||||
name: name,
|
|
||||||
role: role,
|
|
||||||
position: position.isNotEmpty ? position : null,
|
|
||||||
email: email.isNotEmpty ? email : null,
|
|
||||||
phoneNumbers: phoneNumbersList,
|
|
||||||
username: username,
|
|
||||||
isActive: true,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
dataService.addUser(newUser);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onResult(null);
|
onResult(null);
|
||||||
@@ -314,9 +266,6 @@ class UserFormController extends ChangeNotifier {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// API/Mock 모드 전환
|
// API 모드만 사용 (Mock 데이터 제거됨)
|
||||||
void toggleApiMode() {
|
// void toggleApiMode() 메서드 제거
|
||||||
_useApi = !_useApi;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
172
lib/screens/user/controllers/user_list_controller.backup.dart
Normal file
172
lib/screens/user/controllers/user_list_controller.backup.dart
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:superport/models/user_model.dart';
|
||||||
|
import 'package:superport/models/company_model.dart';
|
||||||
|
import 'package:superport/services/user_service.dart';
|
||||||
|
|
||||||
|
/// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||||
|
class UserListController extends ChangeNotifier {
|
||||||
|
final UserService _userService = GetIt.instance<UserService>();
|
||||||
|
|
||||||
|
// 상태 변수
|
||||||
|
List<User> _users = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
// API만 사용
|
||||||
|
|
||||||
|
// 페이지네이션
|
||||||
|
int _currentPage = 1;
|
||||||
|
final int _perPage = 20;
|
||||||
|
bool _hasMoreData = true;
|
||||||
|
bool _isLoadingMore = false;
|
||||||
|
|
||||||
|
// 검색/필터
|
||||||
|
String _searchQuery = '';
|
||||||
|
int? _filterCompanyId;
|
||||||
|
String? _filterRole;
|
||||||
|
bool? _filterIsActive;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
List<User> get users => _users;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
bool get isLoadingMore => _isLoadingMore;
|
||||||
|
String? get error => _error;
|
||||||
|
bool get hasMoreData => _hasMoreData;
|
||||||
|
String get searchQuery => _searchQuery;
|
||||||
|
int? get filterCompanyId => _filterCompanyId;
|
||||||
|
String? get filterRole => _filterRole;
|
||||||
|
bool? get filterIsActive => _filterIsActive;
|
||||||
|
|
||||||
|
UserListController();
|
||||||
|
|
||||||
|
/// 사용자 목록 초기 로드
|
||||||
|
Future<void> loadUsers({bool refresh = false}) async {
|
||||||
|
if (refresh) {
|
||||||
|
_currentPage = 1;
|
||||||
|
_hasMoreData = true;
|
||||||
|
_users.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final newUsers = await _userService.getUsers(
|
||||||
|
page: _currentPage,
|
||||||
|
perPage: _perPage,
|
||||||
|
isActive: _filterIsActive,
|
||||||
|
companyId: _filterCompanyId,
|
||||||
|
role: _filterRole,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newUsers.isEmpty || newUsers.length < _perPage) {
|
||||||
|
_hasMoreData = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentPage == 1) {
|
||||||
|
_users = newUsers;
|
||||||
|
} else {
|
||||||
|
_users.addAll(newUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage++;
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 다음 페이지 로드 (무한 스크롤용)
|
||||||
|
Future<void> loadMore() async {
|
||||||
|
if (!_hasMoreData || _isLoadingMore || _isLoading) return;
|
||||||
|
|
||||||
|
_isLoadingMore = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadUsers();
|
||||||
|
} finally {
|
||||||
|
_isLoadingMore = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 검색 쿼리 설정
|
||||||
|
void setSearchQuery(String query) {
|
||||||
|
_searchQuery = query;
|
||||||
|
_currentPage = 1;
|
||||||
|
_hasMoreData = true;
|
||||||
|
loadUsers(refresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 설정
|
||||||
|
void setFilters({
|
||||||
|
int? companyId,
|
||||||
|
String? role,
|
||||||
|
bool? isActive,
|
||||||
|
}) {
|
||||||
|
_filterCompanyId = companyId;
|
||||||
|
_filterRole = role;
|
||||||
|
_filterIsActive = isActive;
|
||||||
|
_currentPage = 1;
|
||||||
|
_hasMoreData = true;
|
||||||
|
loadUsers(refresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 초기화
|
||||||
|
void clearFilters() {
|
||||||
|
_filterCompanyId = null;
|
||||||
|
_filterRole = null;
|
||||||
|
_filterIsActive = null;
|
||||||
|
_searchQuery = '';
|
||||||
|
_currentPage = 1;
|
||||||
|
_hasMoreData = true;
|
||||||
|
loadUsers(refresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 삭제
|
||||||
|
Future<void> deleteUser(int id, VoidCallback onDeleted, Function(String) onError) async {
|
||||||
|
try {
|
||||||
|
await _userService.deleteUser(id);
|
||||||
|
|
||||||
|
// 목록에서 삭제된 사용자 제거
|
||||||
|
_users.removeWhere((user) => user.id == id);
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
onDeleted();
|
||||||
|
} catch (e) {
|
||||||
|
onError('사용자 삭제 실패: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 상태 변경 (활성/비활성)
|
||||||
|
Future<void> changeUserStatus(int id, bool isActive, Function(String) onError) async {
|
||||||
|
try {
|
||||||
|
final updatedUser = await _userService.changeUserStatus(id, isActive);
|
||||||
|
// 목록에서 해당 사용자 업데이트
|
||||||
|
final index = _users.indexWhere((u) => u.id == id);
|
||||||
|
if (index != -1) {
|
||||||
|
_users[index] = updatedUser;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
onError('상태 변경 실패: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 권한명 반환 함수는 user_utils.dart의 getRoleName을 사용
|
||||||
|
|
||||||
|
/// 회사 ID와 지점 ID로 지점명 조회
|
||||||
|
// 지점명 조회는 별도 서비스로 이동 예정
|
||||||
|
String getBranchName(int companyId, int? branchId) {
|
||||||
|
// TODO: API를 통해 지점명 조회
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
// API만 사용하므로 토글 기능 제거
|
||||||
|
}
|
||||||
@@ -1,137 +1,83 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:superport/models/user_model.dart';
|
import 'package:superport/models/user_model.dart';
|
||||||
import 'package:superport/models/company_model.dart';
|
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/services/user_service.dart';
|
import 'package:superport/services/user_service.dart';
|
||||||
|
import 'package:superport/core/utils/error_handler.dart';
|
||||||
|
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||||
|
import 'package:superport/data/models/common/pagination_params.dart';
|
||||||
|
|
||||||
/// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
/// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||||
class UserListController extends ChangeNotifier {
|
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||||
final MockDataService dataService;
|
class UserListController extends BaseListController<User> {
|
||||||
final UserService _userService = GetIt.instance<UserService>();
|
late final UserService _userService;
|
||||||
|
|
||||||
// 상태 변수
|
// 필터 옵션
|
||||||
List<User> _users = [];
|
|
||||||
bool _isLoading = false;
|
|
||||||
String? _error;
|
|
||||||
bool _useApi = true; // Feature flag
|
|
||||||
|
|
||||||
// 페이지네이션
|
|
||||||
int _currentPage = 1;
|
|
||||||
final int _perPage = 20;
|
|
||||||
bool _hasMoreData = true;
|
|
||||||
bool _isLoadingMore = false;
|
|
||||||
|
|
||||||
// 검색/필터
|
|
||||||
String _searchQuery = '';
|
|
||||||
int? _filterCompanyId;
|
int? _filterCompanyId;
|
||||||
String? _filterRole;
|
String? _filterRole;
|
||||||
bool? _filterIsActive;
|
bool? _filterIsActive;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
List<User> get users => _users;
|
List<User> get users => items;
|
||||||
bool get isLoading => _isLoading;
|
|
||||||
bool get isLoadingMore => _isLoadingMore;
|
|
||||||
String? get error => _error;
|
|
||||||
bool get hasMoreData => _hasMoreData;
|
|
||||||
String get searchQuery => _searchQuery;
|
|
||||||
int? get filterCompanyId => _filterCompanyId;
|
int? get filterCompanyId => _filterCompanyId;
|
||||||
String? get filterRole => _filterRole;
|
String? get filterRole => _filterRole;
|
||||||
bool? get filterIsActive => _filterIsActive;
|
bool? get filterIsActive => _filterIsActive;
|
||||||
|
|
||||||
UserListController({required this.dataService});
|
UserListController() {
|
||||||
|
if (GetIt.instance.isRegistered<UserService>()) {
|
||||||
|
_userService = GetIt.instance<UserService>();
|
||||||
|
} else {
|
||||||
|
throw Exception('UserService not registered in GetIt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 사용자 목록 초기 로드
|
@override
|
||||||
|
Future<PagedResult<User>> fetchData({
|
||||||
|
required PaginationParams params,
|
||||||
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
}) async {
|
||||||
|
// API 호출
|
||||||
|
final fetchedUsers = await ErrorHandler.handleApiCall<List<User>>(
|
||||||
|
() => _userService.getUsers(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
isActive: _filterIsActive,
|
||||||
|
companyId: _filterCompanyId,
|
||||||
|
role: _filterRole,
|
||||||
|
// search 파라미터 제거 (API에서 지원하지 않음)
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final items = fetchedUsers ?? [];
|
||||||
|
|
||||||
|
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||||
|
final meta = PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: items.length < params.perPage ?
|
||||||
|
(params.page - 1) * params.perPage + items.length :
|
||||||
|
params.page * params.perPage + 1,
|
||||||
|
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||||
|
hasNext: items.length >= params.perPage,
|
||||||
|
hasPrevious: params.page > 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return PagedResult(items: items, meta: meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filterItem(User item, String query) {
|
||||||
|
final q = query.toLowerCase();
|
||||||
|
return item.name.toLowerCase().contains(q) ||
|
||||||
|
(item.email?.toLowerCase().contains(q) ?? false) ||
|
||||||
|
(item.username?.toLowerCase().contains(q) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 목록 초기 로드 (호환성 유지)
|
||||||
Future<void> loadUsers({bool refresh = false}) async {
|
Future<void> loadUsers({bool refresh = false}) async {
|
||||||
if (refresh) {
|
await loadData(isRefresh: refresh);
|
||||||
_currentPage = 1;
|
|
||||||
_hasMoreData = true;
|
|
||||||
_users.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isLoading) return;
|
|
||||||
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (_useApi) {
|
|
||||||
final newUsers = await _userService.getUsers(
|
|
||||||
page: _currentPage,
|
|
||||||
perPage: _perPage,
|
|
||||||
isActive: _filterIsActive,
|
|
||||||
companyId: _filterCompanyId,
|
|
||||||
role: _filterRole,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newUsers.isEmpty || newUsers.length < _perPage) {
|
|
||||||
_hasMoreData = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_currentPage == 1) {
|
|
||||||
_users = newUsers;
|
|
||||||
} else {
|
|
||||||
_users.addAll(newUsers);
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentPage++;
|
|
||||||
} else {
|
|
||||||
// Mock 데이터 사용
|
|
||||||
var allUsers = dataService.getAllUsers();
|
|
||||||
|
|
||||||
// 필터 적용
|
|
||||||
if (_filterCompanyId != null) {
|
|
||||||
allUsers = allUsers.where((u) => u.companyId == _filterCompanyId).toList();
|
|
||||||
}
|
|
||||||
if (_filterRole != null) {
|
|
||||||
allUsers = allUsers.where((u) => u.role == _filterRole).toList();
|
|
||||||
}
|
|
||||||
if (_filterIsActive != null) {
|
|
||||||
allUsers = allUsers.where((u) => u.isActive == _filterIsActive).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색 적용
|
|
||||||
if (_searchQuery.isNotEmpty) {
|
|
||||||
allUsers = allUsers.where((u) =>
|
|
||||||
u.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
|
||||||
(u.email?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false) ||
|
|
||||||
(u.username?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false)
|
|
||||||
).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_users = allUsers;
|
|
||||||
_hasMoreData = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
} finally {
|
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 다음 페이지 로드 (무한 스크롤용)
|
|
||||||
Future<void> loadMore() async {
|
|
||||||
if (!_hasMoreData || _isLoadingMore || _isLoading) return;
|
|
||||||
|
|
||||||
_isLoadingMore = true;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await loadUsers();
|
|
||||||
} finally {
|
|
||||||
_isLoadingMore = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 검색 쿼리 설정
|
|
||||||
void setSearchQuery(String query) {
|
|
||||||
_searchQuery = query;
|
|
||||||
_currentPage = 1;
|
|
||||||
_hasMoreData = true;
|
|
||||||
loadUsers(refresh: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 필터 설정
|
/// 필터 설정
|
||||||
@@ -143,9 +89,7 @@ class UserListController extends ChangeNotifier {
|
|||||||
_filterCompanyId = companyId;
|
_filterCompanyId = companyId;
|
||||||
_filterRole = role;
|
_filterRole = role;
|
||||||
_filterIsActive = isActive;
|
_filterIsActive = isActive;
|
||||||
_currentPage = 1;
|
loadData(isRefresh: true);
|
||||||
_hasMoreData = true;
|
|
||||||
loadUsers(refresh: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 필터 초기화
|
/// 필터 초기화
|
||||||
@@ -153,69 +97,126 @@ class UserListController extends ChangeNotifier {
|
|||||||
_filterCompanyId = null;
|
_filterCompanyId = null;
|
||||||
_filterRole = null;
|
_filterRole = null;
|
||||||
_filterIsActive = null;
|
_filterIsActive = null;
|
||||||
_searchQuery = '';
|
search('');
|
||||||
_currentPage = 1;
|
loadData(isRefresh: true);
|
||||||
_hasMoreData = true;
|
}
|
||||||
loadUsers(refresh: true);
|
|
||||||
|
/// 회사별 필터링
|
||||||
|
void filterByCompany(int? companyId) {
|
||||||
|
_filterCompanyId = companyId;
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 역할별 필터링
|
||||||
|
void filterByRole(String? role) {
|
||||||
|
_filterRole = role;
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 활성 상태별 필터링
|
||||||
|
void filterByActiveStatus(bool? isActive) {
|
||||||
|
_filterIsActive = isActive;
|
||||||
|
loadData(isRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 추가
|
||||||
|
Future<void> addUser(User user) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _userService.createUser(
|
||||||
|
username: user.username ?? '',
|
||||||
|
email: user.email ?? '',
|
||||||
|
password: 'temp123', // 임시 비밀번호
|
||||||
|
name: user.name,
|
||||||
|
role: user.role,
|
||||||
|
companyId: user.companyId,
|
||||||
|
branchId: user.branchId,
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 수정
|
||||||
|
Future<void> updateUser(User user) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _userService.updateUser(
|
||||||
|
user.id!,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
companyId: user.companyId,
|
||||||
|
branchId: user.branchId,
|
||||||
|
role: user.role,
|
||||||
|
position: user.position,
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
updateItemLocally(user, (u) => u.id == user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 사용자 삭제
|
/// 사용자 삭제
|
||||||
Future<void> deleteUser(int id, VoidCallback onDeleted, Function(String) onError) async {
|
Future<void> deleteUser(int id) async {
|
||||||
try {
|
await ErrorHandler.handleApiCall<void>(
|
||||||
if (_useApi) {
|
() => _userService.deleteUser(id),
|
||||||
await _userService.deleteUser(id);
|
onError: (failure) {
|
||||||
} else {
|
throw failure;
|
||||||
dataService.deleteUser(id);
|
},
|
||||||
}
|
|
||||||
|
|
||||||
// 목록에서 삭제된 사용자 제거
|
|
||||||
_users.removeWhere((user) => user.id == id);
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
onDeleted();
|
|
||||||
} catch (e) {
|
|
||||||
onError('사용자 삭제 실패: ${e.toString()}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 사용자 상태 변경 (활성/비활성)
|
|
||||||
Future<void> changeUserStatus(int id, bool isActive, Function(String) onError) async {
|
|
||||||
try {
|
|
||||||
if (_useApi) {
|
|
||||||
final updatedUser = await _userService.changeUserStatus(id, isActive);
|
|
||||||
// 목록에서 해당 사용자 업데이트
|
|
||||||
final index = _users.indexWhere((u) => u.id == id);
|
|
||||||
if (index != -1) {
|
|
||||||
_users[index] = updatedUser;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Mock 데이터에서는 상태 변경 지원 안함
|
|
||||||
onError('Mock 데이터에서는 상태 변경을 지원하지 않습니다');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
onError('상태 변경 실패: ${e.toString()}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 권한명 반환 함수는 user_utils.dart의 getRoleName을 사용
|
|
||||||
|
|
||||||
/// 회사 ID와 지점 ID로 지점명 조회
|
|
||||||
String getBranchName(int companyId, int? branchId) {
|
|
||||||
final company = dataService.getCompanyById(companyId);
|
|
||||||
if (company == null || company.branches == null || branchId == null) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
final branch = company.branches!.firstWhere(
|
|
||||||
(b) => b.id == branchId,
|
|
||||||
orElse: () => Branch(companyId: companyId, name: '-'),
|
|
||||||
);
|
);
|
||||||
return branch.name;
|
|
||||||
|
removeItemLocally((u) => u.id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// API/Mock 모드 전환
|
/// 사용자 활성/비활성 토글
|
||||||
void toggleApiMode() {
|
Future<void> toggleUserActiveStatus(User user) async {
|
||||||
_useApi = !_useApi;
|
// TODO: User 모델에 copyWith 메서드가 없어서 임시로 주석 처리
|
||||||
loadUsers(refresh: true);
|
// final updatedUser = user.copyWith(isActive: !user.isActive);
|
||||||
|
// await updateUser(updatedUser);
|
||||||
|
debugPrint('사용자 활성 상태 토글: ${user.name}');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// 비밀번호 재설정
|
||||||
|
Future<void> resetPassword(int userId, String newPassword) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _userService.resetPassword(
|
||||||
|
userId: userId,
|
||||||
|
newPassword: newPassword,
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 ID로 단일 사용자 조회
|
||||||
|
User? getUserById(int id) {
|
||||||
|
try {
|
||||||
|
return items.firstWhere((user) => user.id == id);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 검색 쿼리 설정 (호환성 유지)
|
||||||
|
void setSearchQuery(String query) {
|
||||||
|
search(query); // BaseListController의 search 메서드 사용
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 상태 변경
|
||||||
|
Future<void> changeUserStatus(User user, bool isActive) async {
|
||||||
|
// TODO: User 모델에 copyWith 메서드가 없어서 임시로 주석 처리
|
||||||
|
// final updatedUser = user.copyWith(isActive: isActive);
|
||||||
|
// await updateUser(updatedUser);
|
||||||
|
debugPrint('사용자 상태 변경: ${user.name} -> $isActive');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 지점명 가져오기 (임시 구현)
|
||||||
|
String getBranchName(int? branchId) {
|
||||||
|
if (branchId == null) return '본사';
|
||||||
|
return '지점 $branchId'; // 실제로는 CompanyService에서 가져와야 함
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
import 'package:superport/utils/validators.dart';
|
import 'package:superport/utils/validators.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -34,7 +33,6 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (_) => UserFormController(
|
create: (_) => UserFormController(
|
||||||
dataService: MockDataService(),
|
|
||||||
userId: widget.userId,
|
userId: widget.userId,
|
||||||
),
|
),
|
||||||
child: Consumer<UserFormController>(
|
child: Consumer<UserFormController>(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:superport/screens/common/components/shadcn_components.dart';
|
|||||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||||
import 'package:superport/screens/user/controllers/user_list_controller.dart';
|
import 'package:superport/screens/user/controllers/user_list_controller.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
import 'package:superport/utils/user_utils.dart';
|
import 'package:superport/utils/user_utils.dart';
|
||||||
|
|
||||||
/// shadcn/ui 스타일로 재설계된 사용자 관리 화면
|
/// shadcn/ui 스타일로 재설계된 사용자 관리 화면
|
||||||
@@ -19,7 +18,7 @@ class UserListRedesign extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UserListRedesignState extends State<UserListRedesign> {
|
class _UserListRedesignState extends State<UserListRedesign> {
|
||||||
final MockDataService _dataService = MockDataService();
|
// MockDataService 제거 - 실제 API 사용
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
int _currentPage = 1;
|
int _currentPage = 1;
|
||||||
final int _pageSize = 10;
|
final int _pageSize = 10;
|
||||||
@@ -60,8 +59,8 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
|||||||
|
|
||||||
/// 회사명 반환 함수
|
/// 회사명 반환 함수
|
||||||
String _getCompanyName(int companyId) {
|
String _getCompanyName(int companyId) {
|
||||||
final company = _dataService.getCompanyById(companyId);
|
// TODO: CompanyService를 통해 회사 정보 가져오기
|
||||||
return company?.name ?? '-';
|
return '회사 $companyId'; // 임시 처리
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 상태별 색상 반환
|
/// 상태별 색상 반환
|
||||||
@@ -128,18 +127,9 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
await context.read<UserListController>().deleteUser(
|
await context.read<UserListController>().deleteUser(userId);
|
||||||
userId,
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
() {
|
const SnackBar(content: Text('사용자가 삭제되었습니다')),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('사용자가 삭제되었습니다')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(error) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(error), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
||||||
@@ -168,15 +158,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
await context.read<UserListController>().changeUserStatus(
|
await context.read<UserListController>().changeUserStatus(user, newStatus);
|
||||||
user.id!,
|
|
||||||
newStatus,
|
|
||||||
(error) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(error), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Text(statusText),
|
child: Text(statusText),
|
||||||
),
|
),
|
||||||
@@ -188,7 +170,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (_) => UserListController(dataService: _dataService),
|
create: (_) => UserListController(),
|
||||||
child: Consumer<UserListController>(
|
child: Consumer<UserListController>(
|
||||||
builder: (context, controller, child) {
|
builder: (context, controller, child) {
|
||||||
if (controller.isLoading && controller.users.isEmpty) {
|
if (controller.isLoading && controller.users.isEmpty) {
|
||||||
@@ -494,10 +476,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.getBranchName(
|
controller.getBranchName(user.branchId),
|
||||||
user.companyId,
|
|
||||||
user.branchId,
|
|
||||||
),
|
|
||||||
style: ShadcnTheme.bodySmall,
|
style: ShadcnTheme.bodySmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,12 +3,9 @@ import 'package:get_it/get_it.dart';
|
|||||||
import 'package:superport/models/warehouse_location_model.dart';
|
import 'package:superport/models/warehouse_location_model.dart';
|
||||||
import 'package:superport/models/address_model.dart';
|
import 'package:superport/models/address_model.dart';
|
||||||
import 'package:superport/services/warehouse_service.dart';
|
import 'package:superport/services/warehouse_service.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
|
|
||||||
/// 입고지 폼 상태 및 저장/수정 로직을 담당하는 컨트롤러
|
/// 입고지 폼 상태 및 저장/수정 로직을 담당하는 컨트롤러
|
||||||
class WarehouseLocationFormController extends ChangeNotifier {
|
class WarehouseLocationFormController extends ChangeNotifier {
|
||||||
final bool useApi;
|
|
||||||
final MockDataService? mockDataService;
|
|
||||||
late final WarehouseService _warehouseService;
|
late final WarehouseService _warehouseService;
|
||||||
|
|
||||||
/// 폼 키
|
/// 폼 키
|
||||||
@@ -42,12 +39,12 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
|||||||
WarehouseLocation? _originalLocation;
|
WarehouseLocation? _originalLocation;
|
||||||
|
|
||||||
WarehouseLocationFormController({
|
WarehouseLocationFormController({
|
||||||
this.useApi = true,
|
|
||||||
this.mockDataService,
|
|
||||||
int? locationId,
|
int? locationId,
|
||||||
}) {
|
}) {
|
||||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||||
_warehouseService = GetIt.instance<WarehouseService>();
|
_warehouseService = GetIt.instance<WarehouseService>();
|
||||||
|
} else {
|
||||||
|
throw Exception('WarehouseService not registered in GetIt');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locationId != null) {
|
if (locationId != null) {
|
||||||
@@ -73,11 +70,7 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
_originalLocation = await _warehouseService.getWarehouseLocationById(locationId);
|
||||||
_originalLocation = await _warehouseService.getWarehouseLocationById(locationId);
|
|
||||||
} else {
|
|
||||||
_originalLocation = mockDataService?.getWarehouseLocationById(locationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_originalLocation != null) {
|
if (_originalLocation != null) {
|
||||||
nameController.text = _originalLocation!.name;
|
nameController.text = _originalLocation!.name;
|
||||||
@@ -114,18 +107,10 @@ class WarehouseLocationFormController extends ChangeNotifier {
|
|||||||
remark: remarkController.text.trim().isEmpty ? null : remarkController.text.trim(),
|
remark: remarkController.text.trim().isEmpty ? null : remarkController.text.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
if (_isEditMode) {
|
||||||
if (_isEditMode) {
|
await _warehouseService.updateWarehouseLocation(location);
|
||||||
await _warehouseService.updateWarehouseLocation(location);
|
|
||||||
} else {
|
|
||||||
await _warehouseService.createWarehouseLocation(location);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (_isEditMode) {
|
await _warehouseService.createWarehouseLocation(location);
|
||||||
mockDataService?.updateWarehouseLocation(location);
|
|
||||||
} else {
|
|
||||||
mockDataService?.addWarehouseLocation(location);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:superport/models/warehouse_location_model.dart';
|
||||||
|
import 'package:superport/services/warehouse_service.dart';
|
||||||
|
import 'package:superport/core/errors/failures.dart';
|
||||||
|
import 'package:superport/core/utils/error_handler.dart';
|
||||||
|
|
||||||
|
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용)
|
||||||
|
/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음
|
||||||
|
/// 향후 서비스/리포지토리 DI 구조로 확장 가능
|
||||||
|
class WarehouseLocationListController extends ChangeNotifier {
|
||||||
|
late final WarehouseService _warehouseService;
|
||||||
|
|
||||||
|
List<WarehouseLocation> _warehouseLocations = [];
|
||||||
|
List<WarehouseLocation> _filteredLocations = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
String _searchQuery = '';
|
||||||
|
int _currentPage = 1;
|
||||||
|
final int _pageSize = 20;
|
||||||
|
bool _hasMore = true;
|
||||||
|
int _total = 0;
|
||||||
|
|
||||||
|
// 필터 옵션
|
||||||
|
bool? _isActive;
|
||||||
|
|
||||||
|
WarehouseLocationListController() {
|
||||||
|
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||||
|
_warehouseService = GetIt.instance<WarehouseService>();
|
||||||
|
} else {
|
||||||
|
throw Exception('WarehouseService not registered');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
List<WarehouseLocation> get warehouseLocations => _filteredLocations;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
String? get error => _error;
|
||||||
|
String get searchQuery => _searchQuery;
|
||||||
|
int get currentPage => _currentPage;
|
||||||
|
bool get hasMore => _hasMore;
|
||||||
|
int get total => _total;
|
||||||
|
bool? get isActive => _isActive;
|
||||||
|
|
||||||
|
/// 데이터 로드
|
||||||
|
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
|
||||||
|
if (_isLoading) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// API 사용 시 ErrorHandler 적용
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 🏭 입고지 목록 API 호출 시작');
|
||||||
|
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
final fetchedLocations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||||
|
() => _warehouseService.getWarehouseLocations(
|
||||||
|
page: 1,
|
||||||
|
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
||||||
|
isActive: _isActive,
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||||
|
print('[WarehouseLocationListController] API 에러: ${failure.message}');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fetchedLocations != null) {
|
||||||
|
print('╔══════════════════════════════════════════════════════════');
|
||||||
|
print('║ 📊 입고지 목록 로드 완료');
|
||||||
|
print('║ ▶ 총 입고지 수: ${fetchedLocations.length}개');
|
||||||
|
print('╟──────────────────────────────────────────────────────────');
|
||||||
|
|
||||||
|
// 상태별 통계 (입고지에 상태가 있다면)
|
||||||
|
int activeCount = 0;
|
||||||
|
int inactiveCount = 0;
|
||||||
|
for (final location in fetchedLocations) {
|
||||||
|
// isActive 필드가 있다면 활용
|
||||||
|
activeCount++; // 현재는 모두 활성으로 가정
|
||||||
|
}
|
||||||
|
|
||||||
|
print('║ • 활성 입고지: $activeCount개');
|
||||||
|
if (inactiveCount > 0) {
|
||||||
|
print('║ • 비활성 입고지: $inactiveCount개');
|
||||||
|
}
|
||||||
|
|
||||||
|
print('╟──────────────────────────────────────────────────────────');
|
||||||
|
print('║ 📑 전체 데이터 로드 완료');
|
||||||
|
print('║ • View에서 페이지네이션 처리 예정');
|
||||||
|
print('╚══════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
_warehouseLocations = fetchedLocations;
|
||||||
|
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
||||||
|
_total = fetchedLocations.length;
|
||||||
|
_applySearchFilter();
|
||||||
|
print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown');
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다음 페이지 로드
|
||||||
|
Future<void> loadNextPage() async {
|
||||||
|
if (!_hasMore || _isLoading) return;
|
||||||
|
await loadWarehouseLocations(isInitialLoad: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색
|
||||||
|
void search(String query) {
|
||||||
|
_searchQuery = query;
|
||||||
|
_applySearchFilter();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 필터 적용
|
||||||
|
void _applySearchFilter() {
|
||||||
|
if (_searchQuery.isEmpty) {
|
||||||
|
_filteredLocations = List.from(_warehouseLocations);
|
||||||
|
} else {
|
||||||
|
_filteredLocations = _warehouseLocations.where((location) {
|
||||||
|
return location.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||||
|
location.address.toString().toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터 설정
|
||||||
|
void setFilters({bool? isActive}) {
|
||||||
|
_isActive = isActive;
|
||||||
|
loadWarehouseLocations();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필터 초기화
|
||||||
|
void clearFilters() {
|
||||||
|
_isActive = null;
|
||||||
|
_searchQuery = '';
|
||||||
|
loadWarehouseLocations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 입고지 추가
|
||||||
|
Future<void> addWarehouseLocation(WarehouseLocation location) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _warehouseService.createWarehouseLocation(location),
|
||||||
|
onError: (failure) {
|
||||||
|
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 목록 새로고침
|
||||||
|
await loadWarehouseLocations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 입고지 수정
|
||||||
|
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _warehouseService.updateWarehouseLocation(location),
|
||||||
|
onError: (failure) {
|
||||||
|
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 목록에서 업데이트
|
||||||
|
final index = _warehouseLocations.indexWhere((l) => l.id == location.id);
|
||||||
|
if (index != -1) {
|
||||||
|
_warehouseLocations[index] = location;
|
||||||
|
_applySearchFilter();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 입고지 삭제
|
||||||
|
Future<void> deleteWarehouseLocation(int id) async {
|
||||||
|
await ErrorHandler.handleApiCall<void>(
|
||||||
|
() => _warehouseService.deleteWarehouseLocation(id),
|
||||||
|
onError: (failure) {
|
||||||
|
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 목록에서 제거
|
||||||
|
_warehouseLocations.removeWhere((l) => l.id == id);
|
||||||
|
_applySearchFilter();
|
||||||
|
_total--;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새로고침
|
||||||
|
Future<void> refresh() async {
|
||||||
|
await loadWarehouseLocations();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용 중인 창고 위치 조회
|
||||||
|
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
|
||||||
|
final locations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||||
|
() => _warehouseService.getInUseWarehouseLocations(),
|
||||||
|
onError: (failure) {
|
||||||
|
_error = ErrorHandler.getUserFriendlyMessage(failure);
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return locations ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,265 +2,135 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:superport/models/warehouse_location_model.dart';
|
import 'package:superport/models/warehouse_location_model.dart';
|
||||||
import 'package:superport/services/warehouse_service.dart';
|
import 'package:superport/services/warehouse_service.dart';
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
import 'package:superport/core/utils/error_handler.dart';
|
||||||
import 'package:superport/core/errors/failures.dart';
|
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||||
|
import 'package:superport/data/models/common/pagination_params.dart';
|
||||||
|
|
||||||
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (SRP 적용)
|
/// 입고지 리스트 상태 및 CRUD만 담당하는 컨트롤러 클래스 (리팩토링 버전)
|
||||||
/// UI, 네비게이션, 다이얼로그 등은 포함하지 않음
|
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||||
/// 향후 서비스/리포지토리 DI 구조로 확장 가능
|
class WarehouseLocationListController extends BaseListController<WarehouseLocation> {
|
||||||
class WarehouseLocationListController extends ChangeNotifier {
|
late final WarehouseService _warehouseService;
|
||||||
final bool useApi;
|
|
||||||
final MockDataService? mockDataService;
|
|
||||||
WarehouseService? _warehouseService;
|
|
||||||
|
|
||||||
List<WarehouseLocation> _warehouseLocations = [];
|
|
||||||
List<WarehouseLocation> _filteredLocations = [];
|
|
||||||
bool _isLoading = false;
|
|
||||||
String? _error;
|
|
||||||
String _searchQuery = '';
|
|
||||||
int _currentPage = 1;
|
|
||||||
final int _pageSize = 20;
|
|
||||||
bool _hasMore = true;
|
|
||||||
int _total = 0;
|
|
||||||
|
|
||||||
// 필터 옵션
|
// 필터 옵션
|
||||||
bool? _isActive;
|
bool? _isActive;
|
||||||
|
|
||||||
WarehouseLocationListController({this.useApi = true, this.mockDataService}) {
|
WarehouseLocationListController() {
|
||||||
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
|
if (GetIt.instance.isRegistered<WarehouseService>()) {
|
||||||
_warehouseService = GetIt.instance<WarehouseService>();
|
_warehouseService = GetIt.instance<WarehouseService>();
|
||||||
|
} else {
|
||||||
|
throw Exception('WarehouseService not registered in GetIt');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// 추가 Getters
|
||||||
List<WarehouseLocation> get warehouseLocations => _filteredLocations;
|
List<WarehouseLocation> get warehouseLocations => items;
|
||||||
bool get isLoading => _isLoading;
|
|
||||||
String? get error => _error;
|
|
||||||
String get searchQuery => _searchQuery;
|
|
||||||
int get currentPage => _currentPage;
|
|
||||||
bool get hasMore => _hasMore;
|
|
||||||
int get total => _total;
|
|
||||||
bool? get isActive => _isActive;
|
bool? get isActive => _isActive;
|
||||||
|
|
||||||
/// 데이터 로드
|
@override
|
||||||
|
Future<PagedResult<WarehouseLocation>> fetchData({
|
||||||
|
required PaginationParams params,
|
||||||
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
}) async {
|
||||||
|
// API 사용
|
||||||
|
final fetchedLocations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||||
|
() => _warehouseService.getWarehouseLocations(
|
||||||
|
page: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
isActive: _isActive,
|
||||||
|
),
|
||||||
|
onError: (failure) {
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final items = fetchedLocations ?? [];
|
||||||
|
|
||||||
|
// 임시로 메타데이터 생성 (추후 API에서 실제 메타데이터 반환 시 수정)
|
||||||
|
final meta = PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: items.length < params.perPage ?
|
||||||
|
(params.page - 1) * params.perPage + items.length :
|
||||||
|
params.page * params.perPage + 1,
|
||||||
|
totalPages: items.length < params.perPage ? params.page : params.page + 1,
|
||||||
|
hasNext: items.length >= params.perPage,
|
||||||
|
hasPrevious: params.page > 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return PagedResult(items: items, meta: meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool filterItem(WarehouseLocation item, String query) {
|
||||||
|
return item.name.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
|
item.address.toString().toLowerCase().contains(query.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 데이터 로드 (호환성을 위해 유지)
|
||||||
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
|
Future<void> loadWarehouseLocations({bool isInitialLoad = true}) async {
|
||||||
if (_isLoading) return;
|
await loadData(isRefresh: isInitialLoad);
|
||||||
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (useApi && _warehouseService != null) {
|
|
||||||
// API 사용 - 전체 데이터 로드
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 🏭 입고지 목록 API 호출 시작');
|
|
||||||
print('║ • 활성 필터: ${_isActive != null ? (_isActive! ? "활성" : "비활성") : "전체"}');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
// 전체 데이터를 가져오기 위해 큰 perPage 값 사용
|
|
||||||
final fetchedLocations = await _warehouseService!.getWarehouseLocations(
|
|
||||||
page: 1,
|
|
||||||
perPage: 1000, // 충분히 큰 값으로 전체 데이터 로드
|
|
||||||
isActive: _isActive,
|
|
||||||
);
|
|
||||||
|
|
||||||
print('╔══════════════════════════════════════════════════════════');
|
|
||||||
print('║ 📊 입고지 목록 로드 완료');
|
|
||||||
print('║ ▶ 총 입고지 수: ${fetchedLocations.length}개');
|
|
||||||
print('╟──────────────────────────────────────────────────────────');
|
|
||||||
|
|
||||||
// 상태별 통계 (입고지에 상태가 있다면)
|
|
||||||
int activeCount = 0;
|
|
||||||
int inactiveCount = 0;
|
|
||||||
for (final location in fetchedLocations) {
|
|
||||||
// isActive 필드가 있다면 활용
|
|
||||||
activeCount++; // 현재는 모두 활성으로 가정
|
|
||||||
}
|
|
||||||
|
|
||||||
print('║ • 활성 입고지: $activeCount개');
|
|
||||||
if (inactiveCount > 0) {
|
|
||||||
print('║ • 비활성 입고지: $inactiveCount개');
|
|
||||||
}
|
|
||||||
|
|
||||||
print('╟──────────────────────────────────────────────────────────');
|
|
||||||
print('║ 📑 전체 데이터 로드 완료');
|
|
||||||
print('║ • View에서 페이지네이션 처리 예정');
|
|
||||||
print('╚══════════════════════════════════════════════════════════');
|
|
||||||
|
|
||||||
_warehouseLocations = fetchedLocations;
|
|
||||||
_hasMore = false; // 전체 데이터를 로드했으므로 더 이상 로드할 필요 없음
|
|
||||||
_total = fetchedLocations.length;
|
|
||||||
} else {
|
|
||||||
// Mock 데이터 사용
|
|
||||||
print('[WarehouseLocationListController] Using Mock data');
|
|
||||||
final allLocations = mockDataService?.getAllWarehouseLocations() ?? [];
|
|
||||||
print('[WarehouseLocationListController] Mock data has ${allLocations.length} locations');
|
|
||||||
|
|
||||||
// 필터링 적용
|
|
||||||
var filtered = allLocations;
|
|
||||||
if (_isActive != null) {
|
|
||||||
// Mock 데이터에는 isActive 필드가 없으므로 모두 활성으로 처리
|
|
||||||
filtered = _isActive! ? allLocations : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 페이지네이션 적용
|
|
||||||
final startIndex = (_currentPage - 1) * _pageSize;
|
|
||||||
final endIndex = startIndex + _pageSize;
|
|
||||||
|
|
||||||
if (startIndex < filtered.length) {
|
|
||||||
final pageLocations = filtered.sublist(
|
|
||||||
startIndex,
|
|
||||||
endIndex > filtered.length ? filtered.length : endIndex,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isInitialLoad) {
|
|
||||||
_warehouseLocations = pageLocations;
|
|
||||||
} else {
|
|
||||||
_warehouseLocations.addAll(pageLocations);
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasMore = endIndex < filtered.length;
|
|
||||||
} else {
|
|
||||||
_hasMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_total = filtered.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
_applySearchFilter();
|
|
||||||
print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown');
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
print('[WarehouseLocationListController] Error loading warehouse locations: $e');
|
|
||||||
print('[WarehouseLocationListController] Error type: ${e.runtimeType}');
|
|
||||||
print('[WarehouseLocationListController] Stack trace: $stackTrace');
|
|
||||||
|
|
||||||
if (e is ServerFailure) {
|
|
||||||
_error = e.message;
|
|
||||||
} else {
|
|
||||||
_error = '오류 발생: ${e.toString()}';
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 다음 페이지 로드
|
|
||||||
Future<void> loadNextPage() async {
|
|
||||||
if (!_hasMore || _isLoading) return;
|
|
||||||
await loadWarehouseLocations(isInitialLoad: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색
|
|
||||||
void search(String query) {
|
|
||||||
_searchQuery = query;
|
|
||||||
_applySearchFilter();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색 필터 적용
|
|
||||||
void _applySearchFilter() {
|
|
||||||
if (_searchQuery.isEmpty) {
|
|
||||||
_filteredLocations = List.from(_warehouseLocations);
|
|
||||||
} else {
|
|
||||||
_filteredLocations = _warehouseLocations.where((location) {
|
|
||||||
return location.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
|
||||||
location.address.toString().toLowerCase().contains(_searchQuery.toLowerCase());
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필터 설정
|
// 필터 설정
|
||||||
void setFilters({bool? isActive}) {
|
void setFilters({bool? isActive}) {
|
||||||
_isActive = isActive;
|
_isActive = isActive;
|
||||||
loadWarehouseLocations();
|
loadData(isRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필터 초기화
|
// 필터 초기화
|
||||||
void clearFilters() {
|
void clearFilters() {
|
||||||
_isActive = null;
|
_isActive = null;
|
||||||
_searchQuery = '';
|
search('');
|
||||||
loadWarehouseLocations();
|
loadData(isRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 입고지 추가
|
/// 입고지 추가
|
||||||
Future<void> addWarehouseLocation(WarehouseLocation location) async {
|
Future<void> addWarehouseLocation(WarehouseLocation location) async {
|
||||||
try {
|
await ErrorHandler.handleApiCall<void>(
|
||||||
if (useApi && _warehouseService != null) {
|
() => _warehouseService.createWarehouseLocation(location),
|
||||||
await _warehouseService!.createWarehouseLocation(location);
|
onError: (failure) {
|
||||||
} else {
|
throw failure;
|
||||||
mockDataService?.addWarehouseLocation(location);
|
},
|
||||||
}
|
);
|
||||||
|
|
||||||
// 목록 새로고침
|
// 목록 새로고침
|
||||||
await loadWarehouseLocations();
|
await refresh();
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 입고지 수정
|
/// 입고지 수정
|
||||||
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
|
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
|
||||||
try {
|
await ErrorHandler.handleApiCall<void>(
|
||||||
if (useApi && _warehouseService != null) {
|
() => _warehouseService.updateWarehouseLocation(location),
|
||||||
await _warehouseService!.updateWarehouseLocation(location);
|
onError: (failure) {
|
||||||
} else {
|
throw failure;
|
||||||
mockDataService?.updateWarehouseLocation(location);
|
},
|
||||||
}
|
);
|
||||||
|
|
||||||
// 목록에서 업데이트
|
// 로컬 업데이트
|
||||||
final index = _warehouseLocations.indexWhere((l) => l.id == location.id);
|
updateItemLocally(location, (l) => l.id == location.id);
|
||||||
if (index != -1) {
|
|
||||||
_warehouseLocations[index] = location;
|
|
||||||
_applySearchFilter();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 입고지 삭제
|
/// 입고지 삭제
|
||||||
Future<void> deleteWarehouseLocation(int id) async {
|
Future<void> deleteWarehouseLocation(int id) async {
|
||||||
try {
|
await ErrorHandler.handleApiCall<void>(
|
||||||
if (useApi && _warehouseService != null) {
|
() => _warehouseService.deleteWarehouseLocation(id),
|
||||||
await _warehouseService!.deleteWarehouseLocation(id);
|
onError: (failure) {
|
||||||
} else {
|
throw failure;
|
||||||
mockDataService?.deleteWarehouseLocation(id);
|
},
|
||||||
}
|
);
|
||||||
|
|
||||||
// 목록에서 제거
|
// 로컬 삭제
|
||||||
_warehouseLocations.removeWhere((l) => l.id == id);
|
removeItemLocally((l) => l.id == id);
|
||||||
_applySearchFilter();
|
|
||||||
_total--;
|
|
||||||
notifyListeners();
|
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 새로고침
|
|
||||||
Future<void> refresh() async {
|
|
||||||
await loadWarehouseLocations();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사용 중인 창고 위치 조회
|
// 사용 중인 창고 위치 조회
|
||||||
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
|
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
|
||||||
try {
|
final locations = await ErrorHandler.handleApiCall<List<WarehouseLocation>>(
|
||||||
if (useApi && _warehouseService != null) {
|
() => _warehouseService.getInUseWarehouseLocations(),
|
||||||
return await _warehouseService!.getInUseWarehouseLocations();
|
onError: (failure) {
|
||||||
} else {
|
throw failure;
|
||||||
// Mock 데이터에서는 모든 창고가 사용 중으로 간주
|
},
|
||||||
return mockDataService?.getAllWarehouseLocations() ?? [];
|
);
|
||||||
}
|
return locations ?? [];
|
||||||
} catch (e) {
|
|
||||||
_error = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../core/controllers/base_list_controller.dart';
|
||||||
|
import '../../../core/utils/error_handler.dart';
|
||||||
|
import '../../../data/models/common/pagination_params.dart';
|
||||||
|
import '../../../data/models/warehouse/warehouse_dto.dart';
|
||||||
|
import '../../../domain/usecases/warehouse_location/warehouse_location_usecases.dart';
|
||||||
|
|
||||||
|
/// UseCase 패턴을 적용한 창고 위치 목록 컨트롤러
|
||||||
|
class WarehouseLocationListControllerWithUseCase extends BaseListController<WarehouseLocationDto> {
|
||||||
|
final GetWarehouseLocationsUseCase getWarehouseLocationsUseCase;
|
||||||
|
final CreateWarehouseLocationUseCase createWarehouseLocationUseCase;
|
||||||
|
final UpdateWarehouseLocationUseCase updateWarehouseLocationUseCase;
|
||||||
|
final DeleteWarehouseLocationUseCase deleteWarehouseLocationUseCase;
|
||||||
|
|
||||||
|
// 선택된 항목들
|
||||||
|
final Set<int> _selectedLocationIds = {};
|
||||||
|
Set<int> get selectedLocationIds => _selectedLocationIds;
|
||||||
|
|
||||||
|
// 필터 옵션
|
||||||
|
bool _showActiveOnly = true;
|
||||||
|
String? _filterByManager;
|
||||||
|
|
||||||
|
bool get showActiveOnly => _showActiveOnly;
|
||||||
|
String? get filterByManager => _filterByManager;
|
||||||
|
|
||||||
|
WarehouseLocationListControllerWithUseCase({
|
||||||
|
required this.getWarehouseLocationsUseCase,
|
||||||
|
required this.createWarehouseLocationUseCase,
|
||||||
|
required this.updateWarehouseLocationUseCase,
|
||||||
|
required this.deleteWarehouseLocationUseCase,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PagedResult<WarehouseLocationDto>> fetchData({
|
||||||
|
required PaginationParams params,
|
||||||
|
Map<String, dynamic>? additionalFilters,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 필터 파라미터 구성
|
||||||
|
final filters = <String, dynamic>{};
|
||||||
|
if (_showActiveOnly) filters['is_active'] = true;
|
||||||
|
if (_filterByManager != null) filters['manager'] = _filterByManager;
|
||||||
|
|
||||||
|
final updatedParams = params.copyWith(filters: filters);
|
||||||
|
final getParams = GetWarehouseLocationsParams.fromPaginationParams(updatedParams);
|
||||||
|
|
||||||
|
final result = await getWarehouseLocationsUseCase(getParams);
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
(failure) => throw Exception(failure.message),
|
||||||
|
(locationsResponse) {
|
||||||
|
// PagedResult로 래핑하여 반환
|
||||||
|
final meta = PaginationMeta(
|
||||||
|
currentPage: params.page,
|
||||||
|
perPage: params.perPage,
|
||||||
|
total: locationsResponse.items.length,
|
||||||
|
totalPages: (locationsResponse.items.length / params.perPage).ceil(),
|
||||||
|
hasNext: locationsResponse.items.length >= params.perPage,
|
||||||
|
hasPrevious: params.page > 1,
|
||||||
|
);
|
||||||
|
return PagedResult(items: locationsResponse.items, meta: meta);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('데이터 로드 실패: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 창고 위치 생성
|
||||||
|
Future<void> createWarehouseLocation({
|
||||||
|
required String name,
|
||||||
|
required String address,
|
||||||
|
String? description,
|
||||||
|
String? contactNumber,
|
||||||
|
String? manager,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
final params = CreateWarehouseLocationParams(
|
||||||
|
name: name,
|
||||||
|
address: address,
|
||||||
|
description: description,
|
||||||
|
contactNumber: contactNumber,
|
||||||
|
manager: manager,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await createWarehouseLocationUseCase(params);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async => errorState = failure.message,
|
||||||
|
(location) async => await refresh(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 발생: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 창고 위치 수정
|
||||||
|
Future<void> updateWarehouseLocation({
|
||||||
|
required int id,
|
||||||
|
String? name,
|
||||||
|
String? address,
|
||||||
|
String? description,
|
||||||
|
String? contactNumber,
|
||||||
|
String? manager,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
|
bool? isActive,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
final params = UpdateWarehouseLocationParams(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
address: address,
|
||||||
|
description: description,
|
||||||
|
contactNumber: contactNumber,
|
||||||
|
manager: manager,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
isActive: isActive,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await updateWarehouseLocationUseCase(params);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async => errorState = failure.message,
|
||||||
|
(location) async => updateItemLocally(location, (item) => item.id == location.id),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 발생: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 창고 위치 삭제
|
||||||
|
Future<void> deleteWarehouseLocation(int id) async {
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
final result = await deleteWarehouseLocationUseCase(id);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async => errorState = failure.message,
|
||||||
|
(_) async {
|
||||||
|
removeItemLocally((item) => item.id == id);
|
||||||
|
_selectedLocationIds.remove(id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 발생: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 창고 위치 활성/비활성 토글
|
||||||
|
Future<void> toggleLocationStatus(int id) async {
|
||||||
|
final location = items.firstWhere((item) => item.id == id);
|
||||||
|
await updateWarehouseLocation(
|
||||||
|
id: id,
|
||||||
|
isActive: !location.isActive,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 설정
|
||||||
|
void setFilters({
|
||||||
|
bool? showActiveOnly,
|
||||||
|
String? manager,
|
||||||
|
}) {
|
||||||
|
if (showActiveOnly != null) _showActiveOnly = showActiveOnly;
|
||||||
|
_filterByManager = manager;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필터 초기화
|
||||||
|
void clearFilters() {
|
||||||
|
_showActiveOnly = true;
|
||||||
|
_filterByManager = null;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 창고 위치 선택 토글
|
||||||
|
void toggleLocationSelection(int id) {
|
||||||
|
if (_selectedLocationIds.contains(id)) {
|
||||||
|
_selectedLocationIds.remove(id);
|
||||||
|
} else {
|
||||||
|
_selectedLocationIds.add(id);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 모든 창고 위치 선택
|
||||||
|
void selectAll() {
|
||||||
|
_selectedLocationIds.clear();
|
||||||
|
_selectedLocationIds.addAll(items.map((e) => e.id));
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택 해제
|
||||||
|
void clearSelection() {
|
||||||
|
_selectedLocationIds.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 창고 위치 일괄 삭제
|
||||||
|
Future<void> deleteSelectedLocations() async {
|
||||||
|
if (_selectedLocationIds.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
final errors = <String>[];
|
||||||
|
for (final id in _selectedLocationIds.toList()) {
|
||||||
|
final result = await deleteWarehouseLocationUseCase(id);
|
||||||
|
result.fold(
|
||||||
|
(failure) => errors.add('Location $id: ${failure.message}'),
|
||||||
|
(_) => removeItemLocally((item) => item.id == id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedLocationIds.clear();
|
||||||
|
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
errorState = '일부 창고 위치 삭제 실패:\n${errors.join('\n')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 발생: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 창고 위치 일괄 활성화
|
||||||
|
Future<void> activateSelectedLocations() async {
|
||||||
|
if (_selectedLocationIds.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
for (final id in _selectedLocationIds.toList()) {
|
||||||
|
await updateWarehouseLocation(id: id, isActive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedLocationIds.clear();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 발생: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 선택된 창고 위치 일괄 비활성화
|
||||||
|
Future<void> deactivateSelectedLocations() async {
|
||||||
|
if (_selectedLocationIds.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoadingState = true;
|
||||||
|
|
||||||
|
for (final id in _selectedLocationIds.toList()) {
|
||||||
|
await updateWarehouseLocation(id: id, isActive: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedLocationIds.clear();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
errorState = '오류 발생: $e';
|
||||||
|
} finally {
|
||||||
|
isLoadingState = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 드롭다운용 활성 창고 위치 목록 가져오기
|
||||||
|
List<WarehouseLocationDto> getActiveLocations() {
|
||||||
|
return items.where((location) => location.isActive).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 특정 관리자의 창고 위치 목록 가져오기
|
||||||
|
List<WarehouseLocationDto> getLocationsByManager(String manager) {
|
||||||
|
return items.where((location) => location.managerName == manager).toList(); // managerName 필드 사용
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_selectedLocationIds.clear();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import 'package:superport/screens/common/widgets/standard_states.dart';
|
|||||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||||
import 'package:superport/screens/warehouse_location/controllers/warehouse_location_list_controller.dart';
|
import 'package:superport/screens/warehouse_location/controllers/warehouse_location_list_controller.dart';
|
||||||
import 'package:superport/utils/constants.dart';
|
import 'package:superport/utils/constants.dart';
|
||||||
|
import 'package:superport/core/widgets/auth_guard.dart';
|
||||||
|
|
||||||
/// shadcn/ui 스타일로 재설계된 입고지 관리 화면
|
/// shadcn/ui 스타일로 재설계된 입고지 관리 화면
|
||||||
class WarehouseLocationListRedesign extends StatefulWidget {
|
class WarehouseLocationListRedesign extends StatefulWidget {
|
||||||
@@ -99,10 +100,13 @@ class _WarehouseLocationListRedesignState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider.value(
|
// Admin과 Manager만 접근 가능
|
||||||
value: _controller,
|
return AuthGuard(
|
||||||
child: Consumer<WarehouseLocationListController>(
|
allowedRoles: UserRole.adminAndManager,
|
||||||
builder: (context, controller, child) {
|
child: ChangeNotifierProvider.value(
|
||||||
|
value: _controller,
|
||||||
|
child: Consumer<WarehouseLocationListController>(
|
||||||
|
builder: (context, controller, child) {
|
||||||
final int totalCount = controller.warehouseLocations.length;
|
final int totalCount = controller.warehouseLocations.length;
|
||||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||||
final int endIndex =
|
final int endIndex =
|
||||||
@@ -161,6 +165,7 @@ class _WarehouseLocationListRedesignState
|
|||||||
) : null,
|
) : null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import 'package:superport/data/models/auth/logout_request.dart';
|
|||||||
import 'package:superport/data/models/auth/refresh_token_request.dart';
|
import 'package:superport/data/models/auth/refresh_token_request.dart';
|
||||||
import 'package:superport/data/models/auth/token_response.dart';
|
import 'package:superport/data/models/auth/token_response.dart';
|
||||||
import 'package:superport/core/config/environment.dart' as env;
|
import 'package:superport/core/config/environment.dart' as env;
|
||||||
import 'package:superport/services/mock_data_service.dart';
|
|
||||||
|
|
||||||
abstract class AuthService {
|
abstract class AuthService {
|
||||||
Future<Either<Failure, LoginResponse>> login(LoginRequest request);
|
Future<Either<Failure, LoginResponse>> login(LoginRequest request);
|
||||||
@@ -49,15 +48,9 @@ class AuthServiceImpl implements AuthService {
|
|||||||
@override
|
@override
|
||||||
Future<Either<Failure, LoginResponse>> login(LoginRequest request) async {
|
Future<Either<Failure, LoginResponse>> login(LoginRequest request) async {
|
||||||
try {
|
try {
|
||||||
debugPrint('[AuthService] login 시작 - useApi: ${env.Environment.useApi}');
|
debugPrint('[AuthService] login 시작');
|
||||||
|
|
||||||
// Mock 모드일 때
|
// API 모드로 로그인 처리
|
||||||
if (!env.Environment.useApi) {
|
|
||||||
debugPrint('[AuthService] Mock 모드로 로그인 처리');
|
|
||||||
return _mockLogin(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 모드일 때
|
|
||||||
debugPrint('[AuthService] API 모드로 로그인 처리');
|
debugPrint('[AuthService] API 모드로 로그인 처리');
|
||||||
final result = await _authRemoteDataSource.login(request);
|
final result = await _authRemoteDataSource.login(request);
|
||||||
|
|
||||||
@@ -84,61 +77,6 @@ class AuthServiceImpl implements AuthService {
|
|||||||
return Left(ServerFailure(message: '로그인 처리 중 오류가 발생했습니다.'));
|
return Left(ServerFailure(message: '로그인 처리 중 오류가 발생했습니다.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<Failure, LoginResponse>> _mockLogin(LoginRequest request) async {
|
|
||||||
try {
|
|
||||||
// Mock 데이터 서비스의 사용자 확인
|
|
||||||
final mockService = MockDataService();
|
|
||||||
final users = mockService.getAllUsers();
|
|
||||||
|
|
||||||
// 사용자 찾기
|
|
||||||
final user = users.firstWhere(
|
|
||||||
(u) => u.email == request.email,
|
|
||||||
orElse: () => throw Exception('사용자를 찾을 수 없습니다.'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 비밀번호 확인 (Mock에서는 간단하게 처리)
|
|
||||||
if (request.password != 'admin123' && request.password != 'password123') {
|
|
||||||
return Left(AuthenticationFailure(message: '잘못된 비밀번호입니다.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock 토큰 생성
|
|
||||||
final mockAccessToken = 'mock_access_token_${DateTime.now().millisecondsSinceEpoch}';
|
|
||||||
final mockRefreshToken = 'mock_refresh_token_${DateTime.now().millisecondsSinceEpoch}';
|
|
||||||
|
|
||||||
// Mock 로그인 응답 생성
|
|
||||||
final loginResponse = LoginResponse(
|
|
||||||
accessToken: mockAccessToken,
|
|
||||||
refreshToken: mockRefreshToken,
|
|
||||||
tokenType: 'Bearer',
|
|
||||||
expiresIn: 3600,
|
|
||||||
user: AuthUser(
|
|
||||||
id: user.id ?? 0,
|
|
||||||
username: user.username ?? '',
|
|
||||||
email: user.email ?? request.email ?? '',
|
|
||||||
name: user.name,
|
|
||||||
role: user.role,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 토큰 및 사용자 정보 저장
|
|
||||||
await _saveTokens(
|
|
||||||
loginResponse.accessToken,
|
|
||||||
loginResponse.refreshToken,
|
|
||||||
loginResponse.expiresIn,
|
|
||||||
);
|
|
||||||
await _saveUser(loginResponse.user);
|
|
||||||
|
|
||||||
// 인증 상태 변경 알림
|
|
||||||
_authStateController.add(true);
|
|
||||||
|
|
||||||
debugPrint('[AuthService] Mock 로그인 성공');
|
|
||||||
return Right(loginResponse);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('[AuthService] Mock 로그인 실패: $e');
|
|
||||||
return Left(ServerFailure(message: '로그인 처리 중 오류가 발생했습니다.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Either<Failure, void>> logout() async {
|
Future<Either<Failure, void>> logout() async {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../core/config/environment.dart';
|
import '../core/config/environment.dart';
|
||||||
|
import '../core/constants/app_constants.dart';
|
||||||
import '../data/datasources/remote/api_client.dart';
|
import '../data/datasources/remote/api_client.dart';
|
||||||
|
|
||||||
// 조건부 import - 웹 플랫폼에서만 dart:js 사용
|
// 조건부 import - 웹 플랫폼에서만 dart:js 사용
|
||||||
@@ -72,8 +73,8 @@ class HealthCheckService {
|
|||||||
|
|
||||||
final dio = Dio(BaseOptions(
|
final dio = Dio(BaseOptions(
|
||||||
baseUrl: Environment.apiBaseUrl,
|
baseUrl: Environment.apiBaseUrl,
|
||||||
connectTimeout: const Duration(seconds: 10),
|
connectTimeout: AppConstants.healthCheckTimeout,
|
||||||
receiveTimeout: const Duration(seconds: 10),
|
receiveTimeout: AppConstants.healthCheckTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
@@ -116,7 +117,7 @@ class HealthCheckService {
|
|||||||
_performHealthCheck();
|
_performHealthCheck();
|
||||||
|
|
||||||
// 30초마다 체크
|
// 30초마다 체크
|
||||||
_healthCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
_healthCheckTimer = Timer.periodic(AppConstants.healthCheckInterval, (_) {
|
||||||
_performHealthCheck();
|
_performHealthCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user