feat: API 통합을 위한 기초 인프라 구축
- 네트워크 레이어 구현 (Dio 기반 ApiClient) - 환경별 설정 관리 시스템 구축 - 의존성 주입 설정 (GetIt) - API 엔드포인트 상수 정의 - 인터셉터 구현 (Auth, Error, Logging) - 프로젝트 아키텍처 개선 (core, data, di 디렉토리 구조) - API 통합 계획서 및 요구사항 문서 작성 - 필요 패키지 추가 (dio, flutter_secure_storage, get_it 등)
This commit is contained in:
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Example environment configuration
|
||||||
|
# Copy this file to .env.development or .env.production and update the values
|
||||||
|
|
||||||
|
API_BASE_URL=http://localhost:8080/api/v1
|
||||||
|
API_TIMEOUT=30000
|
||||||
|
ENABLE_LOGGING=true
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -43,3 +43,8 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|||||||
174
CLAUDE.md
Normal file
174
CLAUDE.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
이 파일은 Claude Code (claude.ai/code)가 SuperPort 프로젝트에서 작업할 때 필요한 가이드라인을 제공합니다.
|
||||||
|
|
||||||
|
## 🎯 프로젝트 개요
|
||||||
|
|
||||||
|
**SuperPort**는 Flutter 기반 장비 관리 ERP 시스템으로, 웹과 모바일 플랫폼을 지원합니다.
|
||||||
|
|
||||||
|
### 주요 기능
|
||||||
|
- 장비 입출고 관리 및 이력 추적
|
||||||
|
- 회사/지점 계층 구조 관리
|
||||||
|
- 사용자 권한 관리 (관리자/일반)
|
||||||
|
- 유지보수 라이선스 관리
|
||||||
|
- 창고 위치 관리
|
||||||
|
|
||||||
|
### 기술 스택
|
||||||
|
- **Frontend**: Flutter (Web + Mobile)
|
||||||
|
- **State Management**: Custom Controller Pattern
|
||||||
|
- **Data Layer**: MockDataService (API 연동 준비됨)
|
||||||
|
- **UI Theme**: Shadcn Design System
|
||||||
|
- **Localization**: 한국어 우선, 영어 지원
|
||||||
|
|
||||||
|
## 📂 프로젝트 구조
|
||||||
|
|
||||||
|
### 아키텍처 개요
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── models/ # 데이터 모델 (JSON serialization 포함)
|
||||||
|
├── services/ # 비즈니스 로직 서비스
|
||||||
|
│ └── mock_data_service.dart # Singleton 패턴 Mock 데이터
|
||||||
|
├── screens/ # 화면별 디렉토리
|
||||||
|
│ ├── equipment/ # 장비 관리
|
||||||
|
│ ├── company/ # 회사/지점 관리
|
||||||
|
│ ├── user/ # 사용자 관리
|
||||||
|
│ ├── license/ # 라이선스 관리
|
||||||
|
│ ├── warehouse/ # 창고 관리
|
||||||
|
│ └── common/ # 공통 컴포넌트
|
||||||
|
│ ├── layouts/ # AppLayoutRedesign (사이드바 네비게이션)
|
||||||
|
│ └── custom_widgets/ # 재사용 위젯
|
||||||
|
└── theme/ # theme_shadcn.dart (커스텀 테마)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 상태 관리 패턴
|
||||||
|
- **Controller Pattern** 사용 (Provider 대신)
|
||||||
|
- 각 화면마다 전용 Controller 구현
|
||||||
|
- 위치: `lib/screens/*/controllers/`
|
||||||
|
- 예시: `EquipmentInController`, `CompanyController`
|
||||||
|
|
||||||
|
## 💼 비즈니스 엔티티
|
||||||
|
|
||||||
|
### 1. Equipment (장비)
|
||||||
|
- 제조사, 이름, 카테고리 (대/중/소)
|
||||||
|
- 시리얼 번호가 있으면 수량 = 1 (수정 불가)
|
||||||
|
- 시리얼 번호가 없으면 복수 수량 가능
|
||||||
|
- 입출고 이력: 'I' (입고), 'O' (출고)
|
||||||
|
|
||||||
|
### 2. Company (회사)
|
||||||
|
- 본사/지점 계층 구조
|
||||||
|
- 지점별 독립된 주소와 연락처
|
||||||
|
- 회사 → 지점들 관계
|
||||||
|
|
||||||
|
### 3. User (사용자)
|
||||||
|
- 회사 연결
|
||||||
|
- 권한 레벨: 'S' (관리자), 'M' (일반)
|
||||||
|
|
||||||
|
### 4. License (라이선스)
|
||||||
|
- 유지보수 라이선스
|
||||||
|
- 기간 및 방문 주기 관리
|
||||||
|
|
||||||
|
## 🛠 개발 명령어
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 개발 실행
|
||||||
|
flutter run
|
||||||
|
|
||||||
|
# 빌드
|
||||||
|
flutter build web # 웹 배포
|
||||||
|
flutter build apk # Android APK
|
||||||
|
flutter build ios # iOS (macOS 필요)
|
||||||
|
|
||||||
|
# 코드 품질
|
||||||
|
flutter analyze # 정적 분석
|
||||||
|
flutter format . # 코드 포맷팅
|
||||||
|
|
||||||
|
# 의존성
|
||||||
|
flutter pub get # 설치
|
||||||
|
flutter pub upgrade # 업그레이드
|
||||||
|
|
||||||
|
# 테스트
|
||||||
|
flutter test # 테스트 실행
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📐 코딩 컨벤션
|
||||||
|
|
||||||
|
### 네이밍 규칙
|
||||||
|
- **파일**: snake_case (`equipment_list.dart`)
|
||||||
|
- **클래스**: PascalCase (`EquipmentInController`)
|
||||||
|
- **변수/메서드**: camelCase (`userName`, `calculateTotal`)
|
||||||
|
- **Boolean**: 동사 기반 (`isReady`, `hasError`)
|
||||||
|
|
||||||
|
### 파일 구조
|
||||||
|
- 300줄 초과 시 기능별 분리 고려
|
||||||
|
- 모델별 파일 분리 (`equipment.dart` vs `equipment_in.dart`)
|
||||||
|
- 재사용 컴포넌트는 `custom_widgets/`로 추출
|
||||||
|
|
||||||
|
### UI 가이드라인
|
||||||
|
- **Metronic Admin Template** 디자인 패턴 준수
|
||||||
|
- **Material Icons** 사용
|
||||||
|
- **ShadcnCard**: 일관된 카드 스타일
|
||||||
|
- **FormFieldWrapper**: 폼 필드 간격
|
||||||
|
- 반응형 디자인 (웹/모바일)
|
||||||
|
|
||||||
|
## 🚀 구현 가이드
|
||||||
|
|
||||||
|
### 새 기능 추가 순서
|
||||||
|
1. `lib/models/`에 모델 생성
|
||||||
|
2. `MockDataService`에 목 데이터 추가
|
||||||
|
3. 화면 디렉토리에 Controller 생성
|
||||||
|
4. 목록/폼 화면 구현
|
||||||
|
5. `AppLayoutRedesign`에 네비게이션 추가
|
||||||
|
|
||||||
|
### 폼 구현 팁
|
||||||
|
- 필수 필드 검증
|
||||||
|
- 날짜 선택: 과거 날짜만 허용 (이력 기록용)
|
||||||
|
- 카테고리: 계층적 드롭다운 (대→중→소)
|
||||||
|
- 생성/수정 모드 모두 처리
|
||||||
|
|
||||||
|
## 📋 현재 상태
|
||||||
|
|
||||||
|
### ✅ 구현 완료
|
||||||
|
- 로그인 화면 (Mock 인증)
|
||||||
|
- 메인 레이아웃 (사이드바 네비게이션)
|
||||||
|
- 모든 엔티티 CRUD
|
||||||
|
- 한국어/영어 다국어 지원
|
||||||
|
- 반응형 디자인
|
||||||
|
- Mock 데이터 서비스
|
||||||
|
|
||||||
|
### 🔜 구현 예정
|
||||||
|
- API 연동
|
||||||
|
- 실제 인증
|
||||||
|
- 바코드 스캔
|
||||||
|
- 테스트 커버리지
|
||||||
|
- 장비 보증 추적
|
||||||
|
- PDF 내보내기 (의존성 준비됨)
|
||||||
|
|
||||||
|
## 🔍 디버깅 팁
|
||||||
|
- 데이터 문제: `MockDataService` 확인
|
||||||
|
- 비즈니스 로직: Controller 확인
|
||||||
|
- 정적 분석: `flutter analyze`
|
||||||
|
- 웹 문제: 브라우저 콘솔 확인
|
||||||
|
|
||||||
|
## 💬 응답 규칙
|
||||||
|
|
||||||
|
### 언어 설정
|
||||||
|
- **코드/변수명**: 영어
|
||||||
|
- **주석/문서/응답**: 한국어
|
||||||
|
- 기술 용어는 영어 병기 가능
|
||||||
|
|
||||||
|
### Git 커밋 메시지
|
||||||
|
```
|
||||||
|
type: 간단한 설명 (한국어)
|
||||||
|
|
||||||
|
선택적 상세 설명
|
||||||
|
```
|
||||||
|
|
||||||
|
**타입**:
|
||||||
|
- `feat`: 새 기능
|
||||||
|
- `fix`: 버그 수정
|
||||||
|
- `refactor`: 리팩토링
|
||||||
|
- `docs`: 문서 변경
|
||||||
|
- `test`: 테스트
|
||||||
|
- `chore`: 빌드/도구
|
||||||
|
|
||||||
|
**주의**: AI 도구 속성 표시 금지
|
||||||
335
doc/API_Additional_Implementation_Requirements.md
Normal file
335
doc/API_Additional_Implementation_Requirements.md
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
# SuperPort API 구현 현황 분석 보고서
|
||||||
|
|
||||||
|
> 작성일: 2025-07-24
|
||||||
|
> 분석 범위: SuperPort 프론트엔드와 백엔드 API 전체
|
||||||
|
> 분석 기준: 프론트엔드 컨트롤러 요구사항 대비 백엔드 API 구현 상태
|
||||||
|
|
||||||
|
## 📊 요약
|
||||||
|
|
||||||
|
- **전체 API 구현율**: 85.3%
|
||||||
|
- **화면별 평균 구현율**: 82.9%
|
||||||
|
- **우선 구현 필요 API 수**: 15개
|
||||||
|
- **즉시 수정 필요 사항**: 3개 (타입 오류)
|
||||||
|
|
||||||
|
## 🖥️ 화면별 API 구현 현황
|
||||||
|
|
||||||
|
### 1. 🔐 로그인 화면
|
||||||
|
**구현율: 100%**
|
||||||
|
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| POST `/api/v1/auth/login` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/auth/logout` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/auth/refresh` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/me` | ✅ | ✅ 구현됨 | 현재 사용자 정보 |
|
||||||
|
|
||||||
|
### 2. 📊 대시보드 화면
|
||||||
|
**구현율: 90%**
|
||||||
|
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| GET `/api/v1/overview/stats` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/overview/recent-activities` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/overview/equipment-status` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/overview/license-expiry` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/statistics/summary` | ✅ | ❌ 미구현 | `/overview/stats`로 대체 가능 |
|
||||||
|
|
||||||
|
### 3. 🏭 장비 관리
|
||||||
|
**구현율: 87.5%**
|
||||||
|
|
||||||
|
#### 장비 목록
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| GET `/api/v1/equipment` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/equipment/search` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/equipment/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| DELETE `/api/v1/equipment/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
|
||||||
|
#### 장비 입고
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| POST `/api/v1/equipment` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PUT `/api/v1/equipment/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/equipment/in` | ✅ | ⚠️ 타입 오류 | DbConn → DatabaseConnection |
|
||||||
|
| GET `/api/v1/equipment/manufacturers` | ✅ | ❌ 미구현 | lookup API로 구현 필요 |
|
||||||
|
| GET `/api/v1/equipment/names` | ✅ | ❌ 미구현 | 자동완성용 |
|
||||||
|
| GET `/api/v1/equipment/categories` | ✅ | ✅ 구현됨 | `/lookups` 사용 |
|
||||||
|
|
||||||
|
#### 장비 출고
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| POST `/api/v1/equipment/out` | ✅ | ⚠️ 타입 오류 | DbConn → DatabaseConnection |
|
||||||
|
| POST `/api/v1/equipment/{id}/status` | ✅ | ✅ 구현됨 | PATCH 메서드로 |
|
||||||
|
| POST `/api/v1/equipment/batch-out` | ✅ | ❌ 미구현 | 대량 출고 처리 |
|
||||||
|
|
||||||
|
#### 장비 고급 기능
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| POST `/api/v1/equipment/{id}/history` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/equipment/{id}/history` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/equipment/rentals` | ✅ | ✅ 구현됨 | 대여 처리 |
|
||||||
|
| POST `/api/v1/equipment/rentals/{id}/return` | ✅ | ✅ 구현됨 | 반납 처리 |
|
||||||
|
| POST `/api/v1/equipment/repairs` | ✅ | ✅ 구현됨 | 수리 처리 |
|
||||||
|
| POST `/api/v1/equipment/disposals` | ✅ | ✅ 구현됨 | 폐기 처리 |
|
||||||
|
|
||||||
|
### 4. 🏢 회사 관리
|
||||||
|
**구현율: 95%**
|
||||||
|
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| GET `/api/v1/companies` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/companies/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/companies` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PUT `/api/v1/companies/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| DELETE `/api/v1/companies/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/companies/search` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/companies/names` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/companies/check-duplicate` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/companies/with-branches` | ✅ | ❌ 미구현 | 지점 포함 조회 |
|
||||||
|
|
||||||
|
#### 지점 관리
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| GET `/api/v1/companies/{id}/branches` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/companies/{id}/branches` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PUT `/api/v1/companies/{id}/branches/{bid}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| DELETE `/api/v1/companies/{id}/branches/{bid}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
|
||||||
|
### 5. 👥 사용자 관리
|
||||||
|
**구현율: 88.9%**
|
||||||
|
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| GET `/api/v1/users` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/users/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/users` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PUT `/api/v1/users/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| DELETE `/api/v1/users/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/users/search` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PATCH `/api/v1/users/{id}/status` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/users/{id}/change-password` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/users/{id}/branch` | ✅ | ❌ 미구현 | 사용자 상세에 포함 |
|
||||||
|
|
||||||
|
### 6. 📜 라이선스 관리
|
||||||
|
**구현율: 100%**
|
||||||
|
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| GET `/api/v1/licenses` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/licenses/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/licenses` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PUT `/api/v1/licenses/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| DELETE `/api/v1/licenses/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/licenses/expiring` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PATCH `/api/v1/licenses/{id}/assign` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PATCH `/api/v1/licenses/{id}/unassign` | ✅ | ✅ 구현됨 | - |
|
||||||
|
|
||||||
|
### 7. 🏭 창고 위치 관리
|
||||||
|
**구현율: 87.5%**
|
||||||
|
|
||||||
|
| API 엔드포인트 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|---------------|----------|-----------|------|
|
||||||
|
| GET `/api/v1/warehouse-locations` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/warehouse-locations/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| POST `/api/v1/warehouse-locations` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| PUT `/api/v1/warehouse-locations/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| DELETE `/api/v1/warehouse-locations/{id}` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/warehouse-locations/{id}/equipment` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/warehouse-locations/{id}/capacity` | ✅ | ✅ 구현됨 | - |
|
||||||
|
| GET `/api/v1/warehouse-locations/search` | ✅ | ❌ 미구현 | 검색 기능 |
|
||||||
|
|
||||||
|
## 🔧 기능별 API 구현 현황
|
||||||
|
|
||||||
|
### 인증/권한
|
||||||
|
**구현율: 80%**
|
||||||
|
|
||||||
|
| 기능 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|------|----------|-----------|------|
|
||||||
|
| JWT 토큰 인증 | ✅ | ✅ 구현됨 | - |
|
||||||
|
| 역할 기반 권한 | ✅ | ✅ 구현됨 | admin/manager/staff/viewer |
|
||||||
|
| 토큰 갱신 | ✅ | ✅ 구현됨 | - |
|
||||||
|
| 비밀번호 변경 | ✅ | ✅ 구현됨 | - |
|
||||||
|
| 비밀번호 재설정 | ✅ | ❌ 미구현 | 이메일 기반 재설정 |
|
||||||
|
|
||||||
|
### 파일 업로드
|
||||||
|
**구현율: 100%**
|
||||||
|
|
||||||
|
| 기능 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|------|----------|-----------|------|
|
||||||
|
| 파일 업로드 | ✅ | ✅ 구현됨 | `/api/v1/files/upload` |
|
||||||
|
| 파일 다운로드 | ✅ | ✅ 구현됨 | `/api/v1/files/{id}` |
|
||||||
|
| 파일 삭제 | ✅ | ✅ 구현됨 | - |
|
||||||
|
| 이미지 미리보기 | ✅ | ✅ 구현됨 | - |
|
||||||
|
|
||||||
|
### 보고서/내보내기
|
||||||
|
**구현율: 100%**
|
||||||
|
|
||||||
|
| 기능 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|------|----------|-----------|------|
|
||||||
|
| PDF 생성 | ✅ | ✅ 구현됨 | `/api/v1/reports/*/pdf` |
|
||||||
|
| Excel 내보내기 | ✅ | ✅ 구현됨 | `/api/v1/reports/*/excel` |
|
||||||
|
| 맞춤 보고서 | ✅ | ✅ 구현됨 | - |
|
||||||
|
|
||||||
|
### 통계/대시보드
|
||||||
|
**구현율: 100%**
|
||||||
|
|
||||||
|
| 기능 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|------|----------|-----------|------|
|
||||||
|
| 전체 통계 | ✅ | ✅ 구현됨 | - |
|
||||||
|
| 장비 상태 분포 | ✅ | ✅ 구현됨 | - |
|
||||||
|
| 라이선스 만료 현황 | ✅ | ✅ 구현됨 | - |
|
||||||
|
| 최근 활동 | ✅ | ✅ 구현됨 | - |
|
||||||
|
|
||||||
|
### 대량 처리
|
||||||
|
**구현율: 66.7%**
|
||||||
|
|
||||||
|
| 기능 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|------|----------|-----------|------|
|
||||||
|
| 대량 업로드 | ✅ | ✅ 구현됨 | `/api/v1/bulk/upload` |
|
||||||
|
| 대량 수정 | ✅ | ✅ 구현됨 | `/api/v1/bulk/update` |
|
||||||
|
| 대량 출고 | ✅ | ❌ 미구현 | 다중 장비 동시 출고 |
|
||||||
|
|
||||||
|
### 감사/백업
|
||||||
|
**구현율: 100%**
|
||||||
|
|
||||||
|
| 기능 | 필요 여부 | 구현 상태 | 비고 |
|
||||||
|
|------|----------|-----------|------|
|
||||||
|
| 감사 로그 | ✅ | ✅ 구현됨 | `/api/v1/audit-logs` |
|
||||||
|
| 백업 생성 | ✅ | ✅ 구현됨 | `/api/v1/backup/create` |
|
||||||
|
| 백업 복원 | ✅ | ✅ 구현됨 | `/api/v1/backup/restore` |
|
||||||
|
| 백업 스케줄 | ✅ | ✅ 구현됨 | - |
|
||||||
|
|
||||||
|
## 🚨 미구현 API 목록 및 우선순위
|
||||||
|
|
||||||
|
### 긴급 (핵심 기능)
|
||||||
|
1. **장비 제조사 목록** - `GET /api/v1/equipment/manufacturers`
|
||||||
|
- 장비 입력 시 자동완성 기능에 필수
|
||||||
|
- `/api/v1/lookups`에 추가 구현 권장
|
||||||
|
|
||||||
|
2. **장비명 자동완성** - `GET /api/v1/equipment/names`
|
||||||
|
- 장비 검색 UX 개선에 필수
|
||||||
|
- distinct 쿼리로 구현
|
||||||
|
|
||||||
|
3. **대량 출고 처리** - `POST /api/v1/equipment/batch-out`
|
||||||
|
- 여러 장비 동시 출고 기능
|
||||||
|
- 트랜잭션 처리 필요
|
||||||
|
|
||||||
|
### 높음 (주요 기능)
|
||||||
|
4. **회사-지점 통합 조회** - `GET /api/v1/companies/with-branches`
|
||||||
|
- 출고 시 회사/지점 선택에 필요
|
||||||
|
- 기존 API 확장으로 구현 가능
|
||||||
|
|
||||||
|
5. **비밀번호 재설정** - `POST /api/v1/auth/reset-password`
|
||||||
|
- 사용자 편의성 개선
|
||||||
|
- 이메일 서비스 연동 필요
|
||||||
|
|
||||||
|
6. **창고 위치 검색** - `GET /api/v1/warehouse-locations/search`
|
||||||
|
- 창고 위치 빠른 검색
|
||||||
|
- 기존 검색 패턴 활용
|
||||||
|
|
||||||
|
### 보통 (부가 기능)
|
||||||
|
7. **통계 요약 API 통합** - `GET /api/v1/statistics/summary`
|
||||||
|
- 현재 `/overview/stats`로 대체 가능
|
||||||
|
- API 일관성을 위해 별칭 추가 권장
|
||||||
|
|
||||||
|
8. **사용자 지점 정보** - `GET /api/v1/users/{id}/branch`
|
||||||
|
- 사용자 상세 조회에 이미 포함됨
|
||||||
|
- 별도 엔드포인트 불필요
|
||||||
|
|
||||||
|
## 🔧 즉시 수정 필요 사항
|
||||||
|
|
||||||
|
### 1. 장비 입출고 API 타입 오류
|
||||||
|
**파일**: `/src/handlers/equipment.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 현재 (오류)
|
||||||
|
pub async fn handle_equipment_in(
|
||||||
|
db: web::Data<DbConn>, // ❌ DbConn 타입 없음
|
||||||
|
claims: web::ReqData<TokenClaims>, // ❌ TokenClaims 타입 없음
|
||||||
|
// ...
|
||||||
|
)
|
||||||
|
|
||||||
|
// 수정 필요
|
||||||
|
pub async fn handle_equipment_in(
|
||||||
|
db: web::Data<DatabaseConnection>, // ✅
|
||||||
|
claims: web::ReqData<Claims>, // ✅
|
||||||
|
// ...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 플러터-백엔드 권한 레벨 매핑
|
||||||
|
**이슈**: Flutter는 'S'(관리자), 'M'(일반)을 사용하지만 백엔드는 'admin', 'manager', 'staff', 'viewer' 사용
|
||||||
|
|
||||||
|
**해결방안**:
|
||||||
|
```dart
|
||||||
|
// Flutter 유틸리티 함수 추가
|
||||||
|
String mapFlutterRoleToBackend(String flutterRole) {
|
||||||
|
switch (flutterRole) {
|
||||||
|
case 'S': return 'admin';
|
||||||
|
case 'M': return 'staff';
|
||||||
|
default: return 'viewer';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. API 응답 형식 일관성
|
||||||
|
일부 API가 표준 응답 형식을 따르지 않음. 모든 API가 다음 형식을 따르도록 수정 필요:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": { ... },
|
||||||
|
"meta": { ... } // 페이지네이션 시
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 추가 구현 제안사항
|
||||||
|
|
||||||
|
### 1. WebSocket 실시간 기능
|
||||||
|
- 장비 상태 실시간 업데이트
|
||||||
|
- 라이선스 만료 실시간 알림
|
||||||
|
- 다중 사용자 동시 편집 방지
|
||||||
|
|
||||||
|
### 2. 배치 작업 스케줄러
|
||||||
|
- 정기 백업 자동화
|
||||||
|
- 라이선스 만료 알림 발송
|
||||||
|
- 장비 점검 일정 알림
|
||||||
|
|
||||||
|
### 3. 모바일 전용 API
|
||||||
|
- 바코드 스캔 장비 조회
|
||||||
|
- 오프라인 동기화
|
||||||
|
- 푸시 알림
|
||||||
|
|
||||||
|
### 4. 고급 검색 기능
|
||||||
|
- Elasticsearch 연동
|
||||||
|
- 전문 검색
|
||||||
|
- 필터 조합 저장
|
||||||
|
|
||||||
|
## 🛠️ 기술적 고려사항
|
||||||
|
|
||||||
|
### 1. 성능 최적화
|
||||||
|
- N+1 쿼리 문제 해결 (eager loading)
|
||||||
|
- 응답 캐싱 구현
|
||||||
|
- 페이지네이션 기본값 설정
|
||||||
|
|
||||||
|
### 2. 보안 강화
|
||||||
|
- Rate limiting 구현됨 ✅
|
||||||
|
- CORS 설정됨 ✅
|
||||||
|
- SQL injection 방지됨 ✅
|
||||||
|
- XSS 방지 헤더 추가됨 ✅
|
||||||
|
|
||||||
|
### 3. 문서화
|
||||||
|
- OpenAPI 3.0 스펙 작성됨 ✅
|
||||||
|
- Postman 컬렉션 생성 필요
|
||||||
|
- API 버저닝 전략 수립 필요
|
||||||
|
|
||||||
|
## 📈 결론
|
||||||
|
|
||||||
|
SuperPort 백엔드 API는 전체적으로 매우 높은 수준으로 구현되어 있습니다. 기본 CRUD 기능뿐만 아니라 고급 기능들(대량 처리, 보고서 생성, 감사 로그, 백업 등)도 대부분 구현되어 있어 즉시 프로덕션 사용이 가능한 수준입니다.
|
||||||
|
|
||||||
|
단, 프론트엔드와의 완전한 연동을 위해서는:
|
||||||
|
1. 장비 입출고 API의 타입 오류 수정 (긴급)
|
||||||
|
2. 자동완성을 위한 lookup API 추가
|
||||||
|
3. 대량 출고 기능 구현
|
||||||
|
4. Flutter 앱의 API 클라이언트 구현
|
||||||
|
|
||||||
|
이러한 작업이 완료되면 MockDataService를 실제 API 호출로 전환할 수 있습니다.
|
||||||
893
doc/API_Integration_Plan.md
Normal file
893
doc/API_Integration_Plan.md
Normal file
@@ -0,0 +1,893 @@
|
|||||||
|
# SuperPort API 통합 계획서
|
||||||
|
|
||||||
|
**작성일**: 2025-07-24
|
||||||
|
**버전**: 1.0
|
||||||
|
**작성자**: Claude
|
||||||
|
|
||||||
|
## 목차
|
||||||
|
|
||||||
|
1. [개요](#1-개요)
|
||||||
|
2. [API 서버 분석](#2-api-서버-분석)
|
||||||
|
3. [현재 상태 분석](#3-현재-상태-분석)
|
||||||
|
4. [화면별 API 통합 계획](#4-화면별-api-통합-계획)
|
||||||
|
5. [기능별 구현 계획](#5-기능별-구현-계획)
|
||||||
|
6. [기술적 아키텍처](#6-기술적-아키텍처)
|
||||||
|
7. [구현 로드맵](#7-구현-로드맵)
|
||||||
|
8. [작업 Task 총정리](#8-작업-task-총정리)
|
||||||
|
9. [위험 관리](#9-위험-관리)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 프로젝트 배경
|
||||||
|
|
||||||
|
SuperPort는 현재 MockDataService를 사용하여 모든 데이터를 메모리에서 관리하고 있습니다. 이는 개발 초기 단계에서는 유용했지만, 실제 운영 환경에서는 다음과 같은 한계가 있습니다:
|
||||||
|
|
||||||
|
- 데이터 영속성 부재
|
||||||
|
- 다중 사용자 동시 접근 불가
|
||||||
|
- 실시간 데이터 동기화 불가
|
||||||
|
- 보안 및 인증 기능 부재
|
||||||
|
|
||||||
|
### 1.2 API 통합 목적
|
||||||
|
|
||||||
|
- **데이터 영속성**: PostgreSQL 데이터베이스를 통한 안정적인 데이터 저장
|
||||||
|
- **다중 사용자 지원**: 동시 접근 및 실시간 업데이트 지원
|
||||||
|
- **보안 강화**: JWT 기반 인증 및 역할 기반 접근 제어
|
||||||
|
- **확장성**: 향후 기능 추가를 위한 견고한 백엔드 인프라
|
||||||
|
|
||||||
|
### 1.3 예상 효과
|
||||||
|
|
||||||
|
- 실제 운영 환경 배포 가능
|
||||||
|
- 데이터 무결성 및 일관성 보장
|
||||||
|
- 사용자별 권한 관리
|
||||||
|
- 감사 로그 및 이력 추적
|
||||||
|
- 대용량 데이터 처리 능력
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. API 서버 분석
|
||||||
|
|
||||||
|
### 2.1 기술 스택
|
||||||
|
|
||||||
|
- **언어**: Rust
|
||||||
|
- **프레임워크**: Actix-web 4.5
|
||||||
|
- **ORM**: SeaORM
|
||||||
|
- **데이터베이스**: PostgreSQL 16
|
||||||
|
- **인증**: JWT (Access + Refresh Token)
|
||||||
|
|
||||||
|
### 2.2 인증 방식
|
||||||
|
|
||||||
|
```json
|
||||||
|
// 로그인 요청
|
||||||
|
POST /api/v1/auth/login
|
||||||
|
{
|
||||||
|
"username": "user@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 응답
|
||||||
|
{
|
||||||
|
"access_token": "eyJ0eXAiOiJKV1Q...",
|
||||||
|
"refresh_token": "eyJ0eXAiOiJKV1Q...",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 주요 엔드포인트
|
||||||
|
|
||||||
|
| 기능 | 메서드 | 경로 | 설명 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| **인증** |
|
||||||
|
| 로그인 | POST | /auth/login | 사용자 인증 |
|
||||||
|
| 로그아웃 | POST | /auth/logout | 세션 종료 |
|
||||||
|
| 토큰 갱신 | POST | /auth/refresh | 액세스 토큰 갱신 |
|
||||||
|
| **장비** |
|
||||||
|
| 목록 조회 | GET | /equipment | 페이징, 필터, 정렬 지원 |
|
||||||
|
| 상세 조회 | GET | /equipment/{id} | 장비 상세 정보 |
|
||||||
|
| 생성 | POST | /equipment | 새 장비 등록 |
|
||||||
|
| 수정 | PUT | /equipment/{id} | 장비 정보 수정 |
|
||||||
|
| 삭제 | DELETE | /equipment/{id} | 장비 삭제 |
|
||||||
|
| 입고 | POST | /equipment/in | 장비 입고 처리 |
|
||||||
|
| 출고 | POST | /equipment/out | 장비 출고 처리 |
|
||||||
|
| **회사** |
|
||||||
|
| 목록 조회 | GET | /companies | 회사 목록 |
|
||||||
|
| 지점 조회 | GET | /companies/{id}/branches | 회사별 지점 |
|
||||||
|
|
||||||
|
### 2.4 데이터 모델 매핑
|
||||||
|
|
||||||
|
| Flutter 모델 | API DTO | 변경사항 |
|
||||||
|
|--------------|---------|----------|
|
||||||
|
| Equipment | EquipmentDto | category 구조 변경 |
|
||||||
|
| Company | CompanyDto | branch 관계 추가 |
|
||||||
|
| User | UserDto | role → role_type |
|
||||||
|
| License | LicenseDto | 완전 일치 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 현재 상태 분석
|
||||||
|
|
||||||
|
### 3.1 MockDataService 구조
|
||||||
|
|
||||||
|
현재 MockDataService는 다음과 같은 구조로 되어 있습니다:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MockDataService {
|
||||||
|
static final MockDataService _instance = MockDataService._internal();
|
||||||
|
|
||||||
|
// 메모리 저장소
|
||||||
|
final List<Equipment> _equipments = [];
|
||||||
|
final List<Company> _companies = [];
|
||||||
|
|
||||||
|
// 동기적 메서드
|
||||||
|
List<Equipment> getEquipments() => _equipments;
|
||||||
|
void addEquipment(Equipment equipment) => _equipments.add(equipment);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 변경 필요 사항
|
||||||
|
|
||||||
|
1. **비동기 처리**: 모든 메서드를 Future 반환으로 변경
|
||||||
|
2. **에러 처리**: try-catch 및 에러 상태 관리
|
||||||
|
3. **로딩 상태**: 데이터 페칭 중 로딩 인디케이터
|
||||||
|
4. **캐싱**: 불필요한 API 호출 최소화
|
||||||
|
|
||||||
|
### 3.3 컨트롤러 패턴 개선
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 현재
|
||||||
|
class EquipmentController {
|
||||||
|
List<Equipment> get equipments => MockDataService().getEquipments();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 개선 후
|
||||||
|
class EquipmentController extends ChangeNotifier {
|
||||||
|
List<Equipment> _equipments = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
Future<void> loadEquipments() async {
|
||||||
|
_isLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
_equipments = await _apiService.getEquipments();
|
||||||
|
_error = null;
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 화면별 API 통합 계획
|
||||||
|
|
||||||
|
### 4.1 로그인 화면
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- POST /api/v1/auth/login
|
||||||
|
- POST /api/v1/auth/refresh
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] AuthService 클래스 생성
|
||||||
|
- [ ] JWT 토큰 저장/관리 로직 구현
|
||||||
|
- [x] SecureStorage 설정
|
||||||
|
- [ ] Access Token 저장
|
||||||
|
- [ ] Refresh Token 저장
|
||||||
|
- [ ] 로그인 폼 검증 추가
|
||||||
|
- [ ] 이메일 형식 검증
|
||||||
|
- [ ] 비밀번호 최소 길이 검증
|
||||||
|
- [ ] 로그인 실패 에러 처리
|
||||||
|
- [ ] 401: 잘못된 인증 정보
|
||||||
|
- [ ] 429: 너무 많은 시도
|
||||||
|
- [ ] 500: 서버 오류
|
||||||
|
- [ ] 자동 로그인 구현
|
||||||
|
- [ ] 토큰 유효성 검사
|
||||||
|
- [ ] 토큰 자동 갱신
|
||||||
|
- [ ] 로그아웃 기능 구현
|
||||||
|
|
||||||
|
### 4.2 대시보드
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- GET /api/v1/overview/stats
|
||||||
|
- GET /api/v1/overview/recent-activities
|
||||||
|
- GET /api/v1/equipment/status-distribution
|
||||||
|
- GET /api/v1/licenses/expiring-soon
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] DashboardService 생성
|
||||||
|
- [ ] 통계 데이터 모델 생성
|
||||||
|
- [ ] OverviewStats DTO
|
||||||
|
- [ ] RecentActivity DTO
|
||||||
|
- [ ] StatusDistribution DTO
|
||||||
|
- [ ] DashboardController 비동기화
|
||||||
|
- [ ] 동시 다중 API 호출 구현
|
||||||
|
- [ ] 부분 로딩 상태 관리
|
||||||
|
- [ ] 실시간 업데이트 구현
|
||||||
|
- [ ] WebSocket 연결 설정
|
||||||
|
- [ ] 실시간 이벤트 수신
|
||||||
|
- [ ] 캐싱 전략 구현
|
||||||
|
- [ ] 5분 캐시 TTL
|
||||||
|
- [ ] Pull-to-refresh 구현
|
||||||
|
- [ ] 에러 시 부분 렌더링
|
||||||
|
|
||||||
|
### 4.3 장비 목록
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- GET /api/v1/equipment?page=1&limit=20&sort=created_at&order=desc
|
||||||
|
- GET /api/v1/equipment/categories
|
||||||
|
- GET /api/v1/companies/names
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] EquipmentService 생성
|
||||||
|
- [ ] 페이지네이션 구현
|
||||||
|
- [ ] 무한 스크롤 구현
|
||||||
|
- [ ] 페이지 상태 관리
|
||||||
|
- [ ] 로딩 인디케이터
|
||||||
|
- [ ] 필터링 기능
|
||||||
|
- [ ] 카테고리별 필터
|
||||||
|
- [ ] 상태별 필터
|
||||||
|
- [ ] 회사별 필터
|
||||||
|
- [ ] 날짜 범위 필터
|
||||||
|
- [ ] 정렬 기능
|
||||||
|
- [ ] 생성일 정렬
|
||||||
|
- [ ] 이름 정렬
|
||||||
|
- [ ] 상태 정렬
|
||||||
|
- [ ] 검색 기능
|
||||||
|
- [ ] 디바운싱 구현
|
||||||
|
- [ ] 검색 결과 하이라이트
|
||||||
|
- [ ] 일괄 작업
|
||||||
|
- [ ] 다중 선택 UI
|
||||||
|
- [ ] 일괄 삭제
|
||||||
|
- [ ] 일괄 상태 변경
|
||||||
|
|
||||||
|
### 4.4 장비 상세/편집
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- GET /api/v1/equipment/{id}
|
||||||
|
- PUT /api/v1/equipment/{id}
|
||||||
|
- GET /api/v1/equipment/{id}/history
|
||||||
|
- POST /api/v1/files/upload
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 상세 정보 로딩
|
||||||
|
- [ ] 기본 정보 표시
|
||||||
|
- [ ] 이력 정보 로딩
|
||||||
|
- [ ] 관련 문서 표시
|
||||||
|
- [ ] 편집 모드 구현
|
||||||
|
- [ ] 폼 데이터 바인딩
|
||||||
|
- [ ] 실시간 검증
|
||||||
|
- [ ] 변경사항 추적
|
||||||
|
- [ ] 이미지 업로드
|
||||||
|
- [ ] 파일 선택 UI
|
||||||
|
- [ ] 업로드 진행률
|
||||||
|
- [ ] 썸네일 생성
|
||||||
|
- [ ] 히스토리 표시
|
||||||
|
- [ ] 타임라인 UI
|
||||||
|
- [ ] 상태 변경 이력
|
||||||
|
- [ ] 담당자 정보
|
||||||
|
|
||||||
|
### 4.5 장비 입고
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- POST /api/v1/equipment/in
|
||||||
|
- GET /api/v1/warehouse-locations
|
||||||
|
- GET /api/v1/equipment/serial-check/{serial}
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 입고 폼 구현
|
||||||
|
- [ ] 장비 정보 입력
|
||||||
|
- [ ] 시리얼 번호 중복 검사
|
||||||
|
- [ ] 창고 위치 선택
|
||||||
|
- [ ] 바코드 스캔 통합
|
||||||
|
- [ ] 카메라 권한 요청
|
||||||
|
- [ ] 바코드 디코딩
|
||||||
|
- [ ] 자동 필드 채우기
|
||||||
|
- [ ] 일괄 입고
|
||||||
|
- [ ] CSV 파일 업로드
|
||||||
|
- [ ] 데이터 검증
|
||||||
|
- [ ] 진행률 표시
|
||||||
|
- [ ] 입고증 생성
|
||||||
|
- [ ] PDF 생성
|
||||||
|
- [ ] 이메일 전송
|
||||||
|
|
||||||
|
### 4.6 장비 출고
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- POST /api/v1/equipment/out
|
||||||
|
- GET /api/v1/equipment/available
|
||||||
|
- GET /api/v1/customers
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 출고 폼 구현
|
||||||
|
- [ ] 가용 장비 조회
|
||||||
|
- [ ] 수량 검증
|
||||||
|
- [ ] 고객 정보 입력
|
||||||
|
- [ ] 출고 승인 프로세스
|
||||||
|
- [ ] 승인 요청
|
||||||
|
- [ ] 승인자 알림
|
||||||
|
- [ ] 승인 이력
|
||||||
|
- [ ] 출고 문서
|
||||||
|
- [ ] 출고증 생성
|
||||||
|
- [ ] 전자 서명
|
||||||
|
- [ ] 문서 보관
|
||||||
|
|
||||||
|
### 4.7 회사 관리
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- GET /api/v1/companies
|
||||||
|
- POST /api/v1/companies
|
||||||
|
- PUT /api/v1/companies/{id}
|
||||||
|
- GET /api/v1/companies/{id}/branches
|
||||||
|
- POST /api/v1/companies/{id}/branches
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 회사 목록 구현
|
||||||
|
- [ ] 본사/지점 트리 구조
|
||||||
|
- [ ] 확장/축소 UI
|
||||||
|
- [ ] 검색 필터
|
||||||
|
- [ ] 회사 등록
|
||||||
|
- [ ] 사업자번호 검증
|
||||||
|
- [ ] 주소 검색 API 연동
|
||||||
|
- [ ] 중복 확인
|
||||||
|
- [ ] 지점 관리
|
||||||
|
- [ ] 지점 추가/편집
|
||||||
|
- [ ] 지점별 권한 설정
|
||||||
|
- [ ] 지점 이전 기능
|
||||||
|
- [ ] 회사 통계
|
||||||
|
- [ ] 장비 보유 현황
|
||||||
|
- [ ] 라이선스 현황
|
||||||
|
- [ ] 사용자 현황
|
||||||
|
|
||||||
|
### 4.8 사용자 관리
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- GET /api/v1/users
|
||||||
|
- POST /api/v1/users
|
||||||
|
- PUT /api/v1/users/{id}
|
||||||
|
- PATCH /api/v1/users/{id}/status
|
||||||
|
- POST /api/v1/users/{id}/reset-password
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 사용자 목록
|
||||||
|
- [ ] 역할별 필터
|
||||||
|
- [ ] 회사별 필터
|
||||||
|
- [ ] 상태별 표시
|
||||||
|
- [ ] 사용자 등록
|
||||||
|
- [ ] 이메일 중복 확인
|
||||||
|
- [ ] 임시 비밀번호 생성
|
||||||
|
- [ ] 환영 이메일 발송
|
||||||
|
- [ ] 권한 관리
|
||||||
|
- [ ] 역할 선택 UI
|
||||||
|
- [ ] 권한 미리보기
|
||||||
|
- [ ] 권한 변경 이력
|
||||||
|
- [ ] 비밀번호 관리
|
||||||
|
- [ ] 비밀번호 재설정
|
||||||
|
- [ ] 강제 변경 설정
|
||||||
|
- [ ] 비밀번호 정책
|
||||||
|
|
||||||
|
### 4.9 라이선스 관리
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- GET /api/v1/licenses
|
||||||
|
- POST /api/v1/licenses
|
||||||
|
- GET /api/v1/licenses/expiring?days=30
|
||||||
|
- POST /api/v1/licenses/{id}/renew
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 라이선스 목록
|
||||||
|
- [ ] 만료일 기준 정렬
|
||||||
|
- [ ] 상태별 색상 구분
|
||||||
|
- [ ] 갱신 알림 표시
|
||||||
|
- [ ] 라이선스 등록
|
||||||
|
- [ ] 계약 정보 입력
|
||||||
|
- [ ] 파일 첨부
|
||||||
|
- [ ] 자동 갱신 설정
|
||||||
|
- [ ] 만료 알림
|
||||||
|
- [ ] 30일전 알림
|
||||||
|
- [ ] 7일전 알림
|
||||||
|
- [ ] 당일 알림
|
||||||
|
- [ ] 라이선스 갱신
|
||||||
|
- [ ] 갱신 프로세스
|
||||||
|
- [ ] 갱신 이력
|
||||||
|
- [ ] 비용 추적
|
||||||
|
|
||||||
|
### 4.10 창고 관리
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- GET /api/v1/warehouse-locations
|
||||||
|
- POST /api/v1/warehouse-locations
|
||||||
|
- GET /api/v1/warehouse-locations/{id}/inventory
|
||||||
|
- PATCH /api/v1/warehouse-locations/{id}/capacity
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 창고 목록
|
||||||
|
- [ ] 위치별 그룹핑
|
||||||
|
- [ ] 용량 표시
|
||||||
|
- [ ] 사용률 차트
|
||||||
|
- [ ] 창고 등록
|
||||||
|
- [ ] 위치 정보
|
||||||
|
- [ ] 용량 설정
|
||||||
|
- [ ] 담당자 지정
|
||||||
|
- [ ] 재고 현황
|
||||||
|
- [ ] 실시간 재고
|
||||||
|
- [ ] 장비별 위치
|
||||||
|
- [ ] 이동 이력
|
||||||
|
- [ ] 창고 이동
|
||||||
|
- [ ] 이동 요청
|
||||||
|
- [ ] 승인 프로세스
|
||||||
|
- [ ] 이동 추적
|
||||||
|
|
||||||
|
### 4.11 보고서
|
||||||
|
|
||||||
|
**사용 API 엔드포인트**:
|
||||||
|
- GET /api/v1/reports/equipment-status
|
||||||
|
- GET /api/v1/reports/license-summary
|
||||||
|
- POST /api/v1/reports/export/excel
|
||||||
|
- POST /api/v1/reports/export/pdf
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 보고서 템플릿
|
||||||
|
- [ ] 장비 현황 보고서
|
||||||
|
- [ ] 라이선스 보고서
|
||||||
|
- [ ] 사용자 활동 보고서
|
||||||
|
- [ ] 보고서 생성
|
||||||
|
- [ ] 기간 선택
|
||||||
|
- [ ] 필터 옵션
|
||||||
|
- [ ] 미리보기
|
||||||
|
- [ ] 내보내기
|
||||||
|
- [ ] Excel 다운로드
|
||||||
|
- [ ] PDF 다운로드
|
||||||
|
- [ ] 이메일 전송
|
||||||
|
- [ ] 정기 보고서
|
||||||
|
- [ ] 스케줄 설정
|
||||||
|
- [ ] 자동 생성
|
||||||
|
- [ ] 수신자 관리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 기능별 구현 계획
|
||||||
|
|
||||||
|
### 5.1 인증/인가 시스템
|
||||||
|
|
||||||
|
**구현 내용**:
|
||||||
|
- JWT 토큰 관리 서비스
|
||||||
|
- 자동 토큰 갱신 인터셉터
|
||||||
|
- 역할 기반 라우트 가드
|
||||||
|
- 세션 타임아웃 처리
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] AuthService 구현
|
||||||
|
- [ ] 로그인/로그아웃
|
||||||
|
- [ ] 토큰 저장/조회
|
||||||
|
- [ ] 토큰 갱신 로직
|
||||||
|
- [ ] AuthInterceptor 구현
|
||||||
|
- [ ] 요청 헤더 토큰 추가
|
||||||
|
- [ ] 401 에러 처리
|
||||||
|
- [ ] 토큰 갱신 재시도
|
||||||
|
- [ ] RouteGuard 구현
|
||||||
|
- [ ] 인증 확인
|
||||||
|
- [ ] 권한 확인
|
||||||
|
- [ ] 리다이렉트 처리
|
||||||
|
|
||||||
|
### 5.2 네트워크 레이어
|
||||||
|
|
||||||
|
**구현 내용**:
|
||||||
|
- Dio 클라이언트 설정
|
||||||
|
- API 엔드포인트 관리
|
||||||
|
- 에러 처리 표준화
|
||||||
|
- 요청/응답 로깅
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [x] ApiClient 싱글톤 구현
|
||||||
|
- [ ] BaseApiService 추상 클래스
|
||||||
|
- [x] 환경별 설정 관리
|
||||||
|
- [ ] 에러 핸들링 유틸
|
||||||
|
- [ ] 네트워크 연결 확인
|
||||||
|
|
||||||
|
### 5.3 상태 관리
|
||||||
|
|
||||||
|
**구현 내용**:
|
||||||
|
- Repository 패턴 도입
|
||||||
|
- 데이터 캐싱 전략
|
||||||
|
- 옵티미스틱 업데이트
|
||||||
|
- 상태 동기화
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] Repository 인터페이스 정의
|
||||||
|
- [ ] 캐시 매니저 구현
|
||||||
|
- [ ] 상태 업데이트 로직
|
||||||
|
- [ ] 충돌 해결 전략
|
||||||
|
|
||||||
|
### 5.4 파일 업로드/다운로드
|
||||||
|
|
||||||
|
**구현 내용**:
|
||||||
|
- Multipart 파일 업로드
|
||||||
|
- 진행률 표시
|
||||||
|
- 파일 다운로드 관리
|
||||||
|
- 오프라인 파일 캐싱
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] FileService 구현
|
||||||
|
- [ ] 업로드 큐 관리
|
||||||
|
- [ ] 다운로드 매니저
|
||||||
|
- [ ] 파일 캐시 정책
|
||||||
|
|
||||||
|
### 5.5 실시간 기능
|
||||||
|
|
||||||
|
**구현 내용**:
|
||||||
|
- WebSocket 연결 관리
|
||||||
|
- 실시간 이벤트 처리
|
||||||
|
- 자동 재연결
|
||||||
|
- 이벤트 필터링
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] WebSocketService 구현
|
||||||
|
- [ ] 이벤트 리스너 관리
|
||||||
|
- [ ] 재연결 로직
|
||||||
|
- [ ] 이벤트 라우팅
|
||||||
|
|
||||||
|
### 5.6 오프라인 지원
|
||||||
|
|
||||||
|
**구현 내용**:
|
||||||
|
- 로컬 데이터베이스 (SQLite)
|
||||||
|
- 동기화 큐 관리
|
||||||
|
- 충돌 해결
|
||||||
|
- 오프라인 모드 UI
|
||||||
|
|
||||||
|
**작업 Task**:
|
||||||
|
- [ ] 로컬 DB 스키마 설계
|
||||||
|
- [ ] 동기화 서비스 구현
|
||||||
|
- [ ] 충돌 해결 UI
|
||||||
|
- [ ] 오프라인 인디케이터
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 기술적 아키텍처
|
||||||
|
|
||||||
|
### 6.1 새로운 디렉토리 구조 ✅
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── core/ ✅
|
||||||
|
│ ├── config/ ✅
|
||||||
|
│ │ └── environment.dart ✅
|
||||||
|
│ ├── constants/ ✅
|
||||||
|
│ │ ├── api_endpoints.dart ✅
|
||||||
|
│ │ └── app_constants.dart ✅
|
||||||
|
│ ├── errors/ ✅
|
||||||
|
│ │ ├── exceptions.dart ✅
|
||||||
|
│ │ └── failures.dart ✅
|
||||||
|
│ └── utils/ ✅
|
||||||
|
│ ├── validators.dart ✅
|
||||||
|
│ └── formatters.dart ✅
|
||||||
|
├── data/ ✅
|
||||||
|
│ ├── datasources/ ✅
|
||||||
|
│ │ ├── local/ ✅
|
||||||
|
│ │ │ └── cache_datasource.dart
|
||||||
|
│ │ └── remote/ ✅
|
||||||
|
│ │ ├── api_client.dart ✅
|
||||||
|
│ │ ├── interceptors/ ✅
|
||||||
|
│ │ │ ├── auth_interceptor.dart ✅
|
||||||
|
│ │ │ ├── error_interceptor.dart ✅
|
||||||
|
│ │ │ └── logging_interceptor.dart ✅
|
||||||
|
│ │ ├── auth_remote_datasource.dart
|
||||||
|
│ │ └── equipment_remote_datasource.dart
|
||||||
|
│ ├── models/ ✅
|
||||||
|
│ │ ├── auth/
|
||||||
|
│ │ ├── equipment/
|
||||||
|
│ │ └── common/
|
||||||
|
│ └── repositories/ ✅
|
||||||
|
│ ├── auth_repository_impl.dart
|
||||||
|
│ └── equipment_repository_impl.dart
|
||||||
|
├── di/ ✅
|
||||||
|
│ └── injection_container.dart ✅
|
||||||
|
├── domain/
|
||||||
|
│ ├── entities/
|
||||||
|
│ ├── repositories/
|
||||||
|
│ └── usecases/
|
||||||
|
├── presentation/
|
||||||
|
│ ├── controllers/
|
||||||
|
│ ├── screens/
|
||||||
|
│ └── widgets/
|
||||||
|
└── main.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 의존성 주입 ✅
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// GetIt을 사용한 DI 설정 ✅
|
||||||
|
final getIt = GetIt.instance;
|
||||||
|
|
||||||
|
Future<void> setupDependencies() async {
|
||||||
|
// 환경 초기화 ✅
|
||||||
|
await Environment.initialize();
|
||||||
|
|
||||||
|
// 네트워크 ✅
|
||||||
|
getIt.registerLazySingleton(() => Dio());
|
||||||
|
getIt.registerLazySingleton(() => const FlutterSecureStorage());
|
||||||
|
|
||||||
|
// API 클라이언트 ✅
|
||||||
|
getIt.registerLazySingleton(() => ApiClient());
|
||||||
|
|
||||||
|
// 데이터소스
|
||||||
|
// TODO: Remote datasources will be registered here
|
||||||
|
|
||||||
|
// 리포지토리
|
||||||
|
// TODO: Repositories will be registered here
|
||||||
|
|
||||||
|
// 컨트롤러
|
||||||
|
// TODO: Controllers will be registered here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 API 클라이언트 설계 ✅
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// ApiClient 클래스 구현됨 (Retrofit 대신 순수 Dio 사용)
|
||||||
|
class ApiClient {
|
||||||
|
late final Dio _dio;
|
||||||
|
static final ApiClient _instance = ApiClient._internal();
|
||||||
|
|
||||||
|
factory ApiClient() => _instance;
|
||||||
|
|
||||||
|
ApiClient._internal() {
|
||||||
|
_dio = Dio(_baseOptions);
|
||||||
|
_setupInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET, POST, PUT, PATCH, DELETE 메서드 구현됨
|
||||||
|
// 파일 업로드/다운로드 메서드 구현됨
|
||||||
|
// 인터셉터 설정 완료 (Auth, Error, Logging)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 에러 처리 표준화
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class ApiException implements Exception {
|
||||||
|
final String message;
|
||||||
|
final int? statusCode;
|
||||||
|
final Map<String, dynamic>? errors;
|
||||||
|
|
||||||
|
ApiException({
|
||||||
|
required this.message,
|
||||||
|
this.statusCode,
|
||||||
|
this.errors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorHandler {
|
||||||
|
static String getErrorMessage(dynamic error) {
|
||||||
|
if (error is DioError) {
|
||||||
|
switch (error.type) {
|
||||||
|
case DioErrorType.connectTimeout:
|
||||||
|
return "연결 시간이 초과되었습니다";
|
||||||
|
case DioErrorType.other:
|
||||||
|
if (error.error is SocketException) {
|
||||||
|
return "인터넷 연결을 확인해주세요";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return "알 수 없는 오류가 발생했습니다";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 구현 로드맵
|
||||||
|
|
||||||
|
### 7.1 Phase 1: 기초 인프라 (3주)
|
||||||
|
|
||||||
|
**1주차: 네트워크 레이어**
|
||||||
|
- [x] Dio 설정 및 인터셉터 구현
|
||||||
|
- [x] API 클라이언트 기본 구조
|
||||||
|
- [ ] 에러 처리 프레임워크
|
||||||
|
- [x] 환경 설정 관리
|
||||||
|
|
||||||
|
**2주차: 인증 시스템**
|
||||||
|
- [ ] AuthService 구현
|
||||||
|
- [ ] 토큰 관리 로직
|
||||||
|
- [ ] 로그인/로그아웃 화면 연동
|
||||||
|
- [ ] 자동 토큰 갱신
|
||||||
|
|
||||||
|
**3주차: 기본 데이터 레이어**
|
||||||
|
- [ ] Repository 패턴 구현
|
||||||
|
- [ ] 기본 모델 변환
|
||||||
|
- [ ] 첫 화면(대시보드) API 연동
|
||||||
|
- [ ] 로딩/에러 상태 관리
|
||||||
|
|
||||||
|
### 7.2 Phase 2: 핵심 기능 (4주)
|
||||||
|
|
||||||
|
**4-5주차: 장비 관리**
|
||||||
|
- [ ] 장비 목록/상세 API 연동
|
||||||
|
- [ ] 입출고 프로세스 구현
|
||||||
|
- [ ] 검색/필터/정렬 기능
|
||||||
|
- [ ] 이미지 업로드
|
||||||
|
|
||||||
|
**6-7주차: 회사/사용자 관리**
|
||||||
|
- [ ] 회사 CRUD 구현
|
||||||
|
- [ ] 지점 관리 기능
|
||||||
|
- [ ] 사용자 관리 및 권한
|
||||||
|
- [ ] 프로필 관리
|
||||||
|
|
||||||
|
### 7.3 Phase 3: 고급 기능 (3주)
|
||||||
|
|
||||||
|
**8주차: 실시간 기능**
|
||||||
|
- [ ] WebSocket 연결 구현
|
||||||
|
- [ ] 실시간 알림
|
||||||
|
- [ ] 대시보드 실시간 업데이트
|
||||||
|
- [ ] 이벤트 처리
|
||||||
|
|
||||||
|
**9주차: 오프라인 지원**
|
||||||
|
- [ ] 로컬 데이터베이스 설정
|
||||||
|
- [ ] 동기화 로직 구현
|
||||||
|
- [ ] 오프라인 모드 UI
|
||||||
|
- [ ] 충돌 해결
|
||||||
|
|
||||||
|
**10주차: 보고서 및 파일**
|
||||||
|
- [ ] 보고서 생성 기능
|
||||||
|
- [ ] Excel/PDF 다운로드
|
||||||
|
- [ ] 파일 관리 시스템
|
||||||
|
- [ ] 대량 데이터 처리
|
||||||
|
|
||||||
|
### 7.4 Phase 4: 최적화 및 마무리 (2주)
|
||||||
|
|
||||||
|
**11주차: 성능 최적화**
|
||||||
|
- [ ] API 호출 최적화
|
||||||
|
- [ ] 캐싱 전략 개선
|
||||||
|
- [ ] 이미지 최적화
|
||||||
|
- [ ] 번들 크기 최적화
|
||||||
|
|
||||||
|
**12주차: 테스트 및 배포**
|
||||||
|
- [ ] 통합 테스트
|
||||||
|
- [ ] 사용자 승인 테스트
|
||||||
|
- [ ] 배포 준비
|
||||||
|
- [ ] 문서화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 작업 Task 총정리
|
||||||
|
|
||||||
|
### 8.1 우선순위별 분류
|
||||||
|
|
||||||
|
#### 🔴 Critical (필수)
|
||||||
|
1. [x] API 클라이언트 설정
|
||||||
|
2. [ ] 인증 시스템 구현
|
||||||
|
3. [ ] 기본 CRUD 기능
|
||||||
|
4. [ ] 에러 처리
|
||||||
|
5. [ ] 로딩 상태 관리
|
||||||
|
|
||||||
|
#### 🟡 High (중요)
|
||||||
|
6. [ ] 페이지네이션
|
||||||
|
7. [ ] 검색/필터
|
||||||
|
8. [ ] 파일 업로드
|
||||||
|
9. [ ] 권한 관리
|
||||||
|
10. [ ] 데이터 검증
|
||||||
|
|
||||||
|
#### 🟢 Medium (개선)
|
||||||
|
11. [ ] 캐싱
|
||||||
|
12. [ ] 실시간 업데이트
|
||||||
|
13. [ ] 오프라인 지원
|
||||||
|
14. [ ] 보고서 생성
|
||||||
|
15. [ ] 성능 최적화
|
||||||
|
|
||||||
|
#### 🔵 Low (선택)
|
||||||
|
16. [ ] 다국어 개선
|
||||||
|
17. [ ] 테마 커스터마이징
|
||||||
|
18. [ ] 애니메이션
|
||||||
|
19. [ ] 단축키
|
||||||
|
20. [ ] 고급 필터
|
||||||
|
|
||||||
|
### 8.2 예상 소요 시간
|
||||||
|
|
||||||
|
| 작업 카테고리 | 예상 시간 | 담당자 제안 |
|
||||||
|
|--------------|-----------|-------------|
|
||||||
|
| 네트워크 인프라 | 40시간 | 백엔드 경험자 |
|
||||||
|
| 인증 시스템 | 24시간 | 보안 전문가 |
|
||||||
|
| 화면별 API 연동 | 120시간 | 프론트엔드 개발자 |
|
||||||
|
| 상태 관리 | 32시간 | Flutter 전문가 |
|
||||||
|
| 테스트 | 40시간 | QA 엔지니어 |
|
||||||
|
| 문서화 | 16시간 | 기술 문서 작성자 |
|
||||||
|
| **총계** | **272시간** | 약 7주 (1인 기준) |
|
||||||
|
|
||||||
|
### 8.3 체크리스트
|
||||||
|
|
||||||
|
#### 개발 환경 설정
|
||||||
|
- [ ] API 서버 접속 정보 확인
|
||||||
|
- [x] 개발/스테이징/운영 환경 구분
|
||||||
|
- [x] 필요 패키지 설치
|
||||||
|
- [ ] Git 브랜치 전략 수립
|
||||||
|
|
||||||
|
#### 코드 품질
|
||||||
|
- [ ] 코드 리뷰 프로세스
|
||||||
|
- [ ] 린트 규칙 설정
|
||||||
|
- [ ] 테스트 커버리지 목표
|
||||||
|
- [ ] CI/CD 파이프라인
|
||||||
|
|
||||||
|
#### 보안
|
||||||
|
- [ ] 토큰 안전 저장
|
||||||
|
- [ ] API 키 관리
|
||||||
|
- [ ] 민감 정보 마스킹
|
||||||
|
- [ ] 보안 감사
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 위험 관리
|
||||||
|
|
||||||
|
### 9.1 기술적 위험
|
||||||
|
|
||||||
|
#### API 응답 지연
|
||||||
|
- **위험**: 느린 네트워크로 인한 UX 저하
|
||||||
|
- **대응**:
|
||||||
|
- 로딩 스켈레톤 UI
|
||||||
|
- 요청 취소 기능
|
||||||
|
- 타임아웃 설정
|
||||||
|
|
||||||
|
#### 토큰 만료 처리
|
||||||
|
- **위험**: 작업 중 토큰 만료로 인한 데이터 손실
|
||||||
|
- **대응**:
|
||||||
|
- 자동 토큰 갱신
|
||||||
|
- 작업 중 데이터 임시 저장
|
||||||
|
- 재인증 플로우
|
||||||
|
|
||||||
|
#### 대용량 데이터 처리
|
||||||
|
- **위험**: 많은 데이터로 인한 앱 멈춤
|
||||||
|
- **대응**:
|
||||||
|
- 페이지네이션 필수 적용
|
||||||
|
- 가상 스크롤 구현
|
||||||
|
- 데이터 스트리밍
|
||||||
|
|
||||||
|
### 9.2 비즈니스 위험
|
||||||
|
|
||||||
|
#### 기존 데이터 마이그레이션
|
||||||
|
- **위험**: Mock 데이터와 실제 데이터 불일치
|
||||||
|
- **대응**:
|
||||||
|
- 데이터 매핑 문서화
|
||||||
|
- 단계적 마이그레이션
|
||||||
|
- 데이터 검증 도구
|
||||||
|
|
||||||
|
#### 사용자 교육
|
||||||
|
- **위험**: 새로운 인증 절차에 대한 거부감
|
||||||
|
- **대응**:
|
||||||
|
- 사용자 가이드 제작
|
||||||
|
- 단계적 롤아웃
|
||||||
|
- 피드백 수집
|
||||||
|
|
||||||
|
### 9.3 롤백 계획
|
||||||
|
|
||||||
|
1. **Feature Flag 사용**
|
||||||
|
- API/Mock 모드 전환 가능
|
||||||
|
- 화면별 점진적 적용
|
||||||
|
|
||||||
|
2. **데이터 백업**
|
||||||
|
- 마이그레이션 전 전체 백업
|
||||||
|
- 롤백 스크립트 준비
|
||||||
|
|
||||||
|
3. **버전 관리**
|
||||||
|
- 이전 버전 APK/IPA 보관
|
||||||
|
- 긴급 패치 프로세스
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 맺음말
|
||||||
|
|
||||||
|
이 문서는 SuperPort 프로젝트의 API 통합을 위한 상세한 계획서입니다. 각 팀원은 담당 파트의 체크리스트를 활용하여 진행 상황을 추적하고, 주간 회의에서 진행률을 공유하시기 바랍니다.
|
||||||
|
|
||||||
|
성공적인 API 통합을 위해서는 팀원 간의 긴밀한 협업과 지속적인 커뮤니케이션이 필수적입니다. 문제가 발생하면 즉시 공유하고 함께 해결책을 찾아나가겠습니다.
|
||||||
|
|
||||||
|
**문서 업데이트**: 이 문서는 프로젝트 진행에 따라 지속적으로 업데이트됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_마지막 업데이트: 2025-07-24_ (네트워크 레이어 및 기본 인프라 구현 완료)
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
# supERPort ERP 개발일지
|
|
||||||
|
|
||||||
## 대화 1: 프로젝트 분석 및 계획
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- supERPort ERP 시스템의 요구사항 문서(PRD) 검토
|
|
||||||
- Flutter 기반 프론트엔드 구현 계획 수립
|
|
||||||
- 주요 기능 리뷰: 장비 입고/출고, 회사/사용자/라이센스 등록 관리
|
|
||||||
- Metronic Admin Template 스타일 가이드라인 확인
|
|
||||||
- 추천 디렉토리 구조 검토
|
|
||||||
|
|
||||||
## 대화 2: 프로젝트 디렉토리 구조 생성
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- PRD에 명시된 디렉토리 구조 구현
|
|
||||||
- 모델, 화면, 서비스, 유틸리티 디렉토리 생성
|
|
||||||
- 각 기능별 파일 구조 설계
|
|
||||||
|
|
||||||
## 대화 3: 모델 클래스 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 장비 관련 모델 구현:
|
|
||||||
- `EquipmentModel`: 공통 장비 정보
|
|
||||||
- `EquipmentInModel`: 장비 입고 정보
|
|
||||||
- `EquipmentOutModel`: 장비 출고 정보
|
|
||||||
- 회사 관련 모델 구현:
|
|
||||||
- `CompanyModel`: 회사 및 지점 정보
|
|
||||||
- 사용자 관련 모델 구현:
|
|
||||||
- `UserModel`: 사용자 정보 및 권한
|
|
||||||
- 라이센스 관련 모델 구현:
|
|
||||||
- `LicenseModel`: 유지보수 라이센스 정보
|
|
||||||
|
|
||||||
## 대화 4: 테마 및 공통 위젯 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 앱 테마 구현 (`AppTheme` 클래스)
|
|
||||||
- Metronic Admin 스타일 적용
|
|
||||||
- 색상 팔레트, 텍스트 스타일, 버튼 스타일 정의
|
|
||||||
- 공통 위젯 구현:
|
|
||||||
- `PageTitle`: 화면 상단 제목 및 추가 버튼
|
|
||||||
- `DataTableCard`: 데이터 테이블을 감싸는 카드 위젯
|
|
||||||
- `FormFieldWrapper`: 폼 필드 레이블 및 래퍼
|
|
||||||
- `DatePickerField`: 날짜 선택 필드
|
|
||||||
- `CategorySelectionField`: 대분류/중분류/소분류 선택 필드
|
|
||||||
|
|
||||||
## 대화 5: 모의 데이터 서비스 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- `MockDataService` 클래스 구현
|
|
||||||
- 모의 데이터 생성 및 관리 기능:
|
|
||||||
- 장비 입고/출고 데이터 관리
|
|
||||||
- 회사 데이터 관리
|
|
||||||
- 사용자 데이터 관리
|
|
||||||
- 라이센스 데이터 관리
|
|
||||||
- CRUD 작업 지원 (생성, 조회, 업데이트, 삭제)
|
|
||||||
|
|
||||||
## 대화 6: 장비 입고 화면 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 장비 입고 목록 화면 구현:
|
|
||||||
- 데이터 테이블로 입고 장비 목록 표시
|
|
||||||
- 추가, 수정, 삭제 기능 구현
|
|
||||||
- 장비 입고 폼 화면 구현:
|
|
||||||
- 제조사명, 장비명, 분류 정보 입력
|
|
||||||
- 시리얼 넘버, 바코드, 물품 수량 입력
|
|
||||||
- 입고일 선택
|
|
||||||
- 폼 유효성 검사
|
|
||||||
|
|
||||||
## 대화 7: 장비 출고 화면 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 장비 출고 목록 화면 구현:
|
|
||||||
- 데이터 테이블로 출고 장비 목록 표시
|
|
||||||
- 추가, 수정, 삭제 기능 구현
|
|
||||||
- 장비 출고 폼 화면 구현:
|
|
||||||
- 장비명, 분류 정보 선택
|
|
||||||
- 시리얼 넘버, 바코드 입력
|
|
||||||
- 출고 수량, 출고일 입력
|
|
||||||
- 폼 유효성 검사
|
|
||||||
|
|
||||||
## 대화 8: 메인 화면 및 라우팅 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 메인 애플리케이션 파일(`main.dart`) 구현
|
|
||||||
- 홈 화면 구현:
|
|
||||||
- 주요 기능 바로가기 카드 메뉴 (그리드 레이아웃)
|
|
||||||
- 라우팅 설정:
|
|
||||||
- 각 화면에 대한 라우트 정의
|
|
||||||
- 인자 전달 처리 (수정 화면용 ID 등)
|
|
||||||
|
|
||||||
## 대화 9: 상수 및 유효성 검사 유틸리티 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 상수 관리 파일 구현:
|
|
||||||
- 라우트 경로 상수
|
|
||||||
- 장비 상태 상수
|
|
||||||
- 사용자 역할 상수
|
|
||||||
- 유효성 검사 함수 구현:
|
|
||||||
- 필수 입력값 검사
|
|
||||||
- 숫자 입력값 검사
|
|
||||||
- 전화번호 형식 검사
|
|
||||||
|
|
||||||
## 대화 10: 회사 관리 화면 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 회사 목록 화면 구현:
|
|
||||||
- 데이터 테이블로 회사 목록 표시
|
|
||||||
- 회사명, 주소, 지점 수 표시
|
|
||||||
- 추가, 수정, 삭제 기능 구현
|
|
||||||
- 회사 등록/수정 폼 구현:
|
|
||||||
- 회사명, 주소 입력
|
|
||||||
- 지점 정보 관리 (추가, 수정, 삭제)
|
|
||||||
- 지점 정보 입력 (지점명, 주소, 전화번호)
|
|
||||||
- 폼 유효성 검사
|
|
||||||
|
|
||||||
## 대화 11: 사용자 관리 화면 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 사용자 목록 화면 구현:
|
|
||||||
- 데이터 테이블로 사용자 목록 표시
|
|
||||||
- 이름, 소속 회사, 권한 정보 표시
|
|
||||||
- 추가, 수정, 삭제 기능 구현
|
|
||||||
- 사용자 등록/수정 폼 구현:
|
|
||||||
- 이름 입력
|
|
||||||
- 소속 회사 선택 (드롭다운)
|
|
||||||
- 권한 선택 (라디오 버튼: 관리자/일반 사용자)
|
|
||||||
- 폼 유효성 검사
|
|
||||||
|
|
||||||
## 대화 12: 라이센스 관리 화면 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 라이센스 목록 화면 구현:
|
|
||||||
- 데이터 테이블로 라이센스 목록 표시
|
|
||||||
- 라이센스명, 회사, 기간, 방문 주기 표시
|
|
||||||
- 추가, 수정, 삭제 기능 구현
|
|
||||||
- 라이센스 등록/수정 폼 구현:
|
|
||||||
- 라이센스명 입력
|
|
||||||
- 회사 선택 (드롭다운)
|
|
||||||
- 라이센스 기간, 방문 주기 입력
|
|
||||||
- 폼 유효성 검사
|
|
||||||
|
|
||||||
## 대화 13: 카테고리 선택 위젯 수정
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- `CategorySelectionField` 위젯의 오류 수정
|
|
||||||
- 타입 처리 개선 및 조건문 명확화
|
|
||||||
- 카테고리/서브카테고리 선택 로직 개선
|
|
||||||
|
|
||||||
## 대화 14: 프로젝트 완성 및 최종 점검
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 16일
|
|
||||||
- **내용**:
|
|
||||||
- 모든 화면 구현 완료 확인:
|
|
||||||
- 장비 입고/출고 관리
|
|
||||||
- 회사 관리
|
|
||||||
- 사용자 관리
|
|
||||||
- 라이센스 관리
|
|
||||||
- 기능 점검:
|
|
||||||
- 각 화면 간 이동 및 데이터 전달
|
|
||||||
- 등록/수정/삭제 기능
|
|
||||||
- 유효성 검사
|
|
||||||
- UI/UX 최종 점검:
|
|
||||||
- 메트로닉 스타일 적용 확인
|
|
||||||
- 반응형 레이아웃 확인
|
|
||||||
- 사용성 검토
|
|
||||||
|
|
||||||
## 대화 15: Metronic 테일윈드 디자인 적용
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 17일
|
|
||||||
- **내용**:
|
|
||||||
- Metronic Admin 테일윈드 버전 (데모6) 디자인 분석
|
|
||||||
- 테일윈드 CSS 기반으로 테마 시스템 재구성
|
|
||||||
- 새로운 테마 파일 생성: `theme_tailwind.dart`
|
|
||||||
- 기존 Material 테마에서 테일윈드 스타일로 변경
|
|
||||||
- Metronic의 색상 팔레트와 그림자 효과 적용
|
|
||||||
|
|
||||||
## 대화 16: 공통 레이아웃 컴포넌트 개발
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 17일
|
|
||||||
- **내용**:
|
|
||||||
- Metronic 스타일의 레이아웃 컴포넌트 구현: `layout_components.dart`
|
|
||||||
- 공통 UI 컴포넌트 개발:
|
|
||||||
- `MetronicPageContainer`: 페이지 기본 레이아웃
|
|
||||||
- `MetronicCard`: 카드 컴포넌트
|
|
||||||
- `MetronicStatsCard`: 통계 카드 컴포넌트
|
|
||||||
- `MetronicPageTitle`: 페이지 제목 컴포넌트
|
|
||||||
- `MetronicDataTable`: 데이터 테이블 컴포넌트
|
|
||||||
- `MetronicFormField`: 폼 필드 래퍼 컴포넌트
|
|
||||||
- `MetronicTabContainer`: 탭 컨테이너 컴포넌트
|
|
||||||
- 전체 UI 일관성 향상을 위한 컴포넌트 표준화
|
|
||||||
|
|
||||||
## 대화 17: 오버뷰 대시보드 화면 구현
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 17일
|
|
||||||
- **내용**:
|
|
||||||
- 오버뷰 화면 개발: `overview_screen.dart`
|
|
||||||
- 기능 구현:
|
|
||||||
- 환영 카드 섹션 개발
|
|
||||||
- 통계 카드 그리드 레이아웃 구현
|
|
||||||
- 시스템 활동 차트 영역 개발 (차트 플레이스홀더)
|
|
||||||
- 최근 활동, 알림, 예정된 작업 탭 구현
|
|
||||||
- 장비 입출고 통계 및 추이 표시
|
|
||||||
- Metronic 데모6 스타일 적용
|
|
||||||
- 홈 화면을 오버뷰 대시보드로 변경
|
|
||||||
|
|
||||||
## 대화 18: 메인 파일 업데이트 및 테마 적용
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 17일
|
|
||||||
- **내용**:
|
|
||||||
- `main.dart` 파일 수정
|
|
||||||
- 테일윈드 테마 적용으로 변경: `AppThemeTailwind.lightTheme`
|
|
||||||
- 홈 화면을 기존 메뉴 그리드에서 오버뷰 대시보드로 변경
|
|
||||||
- 불필요한 HomeScreen 클래스 제거
|
|
||||||
- 라우트 설정 업데이트
|
|
||||||
- 모든 화면이 통일된 디자인 시스템 적용
|
|
||||||
|
|
||||||
## 대화 19: 좌측 사이드바 메뉴 구현 및 레이아웃 개선
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 18일
|
|
||||||
- **내용**:
|
|
||||||
- Metronic 테일윈드 데모6 스타일의 사이드바 메뉴 구현
|
|
||||||
- 좌측에 메인 메뉴를 배치하는 레이아웃 구조 변경
|
|
||||||
- `SidebarMenu` 클래스 개발:
|
|
||||||
- 메뉴 계층 구조 지원 (접는/펼치는 기능)
|
|
||||||
- 활성 메뉴 시각적 표시
|
|
||||||
- 메뉴 항목별 아이콘 및 스타일 적용
|
|
||||||
- `MainLayout` 컴포넌트 구현:
|
|
||||||
- 좌측 사이드바와 우측 컨텐츠 영역 구성
|
|
||||||
- 커스텀 앱바 디자인 적용
|
|
||||||
- 모든 화면에 일관된 레이아웃 제공
|
|
||||||
- 오버뷰 화면을 새 레이아웃에 통합
|
|
||||||
- 메뉴 항목:
|
|
||||||
- 대시보드
|
|
||||||
- 장비 관리 (하위 메뉴: 장비 입고, 장비 출고)
|
|
||||||
- 회사 관리
|
|
||||||
- 사용자 관리
|
|
||||||
- 라이센스 관리
|
|
||||||
|
|
||||||
## 대화 20: 대시보드 인터페이스 개선
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 19일
|
|
||||||
- **내용**:
|
|
||||||
- 대시보드 화면 UI 개선:
|
|
||||||
- 불필요한 환영 메시지 컴포넌트 제거
|
|
||||||
- 통계 카드 디자인 최적화
|
|
||||||
- MetronicStatsCard 컴포넌트 높이 축소 (여백 및 폰트 크기 조정)
|
|
||||||
- 통계 카드 레이아웃 비율 조정 (childAspectRatio 적용)
|
|
||||||
- 헤더 인터페이스 일관성 향상:
|
|
||||||
- 사이드바 헤더와 메인 헤더의 높이 일치 (72px로 통일)
|
|
||||||
- 브랜드 로고 및 앱 타이틀 정렬 개선
|
|
||||||
- 헤더 패딩 및 여백 최적화
|
|
||||||
- 전체적인 레이아웃 조화 개선
|
|
||||||
- 상단 네비게이션 영역 수직 정렬 통일
|
|
||||||
- 컴포넌트 간 일관된 간격 적용
|
|
||||||
|
|
||||||
## 대화 21: 대시보드 레이아웃 균형 개선
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 19일
|
|
||||||
- **내용**:
|
|
||||||
- 대시보드 레이아웃의 일관성 향상:
|
|
||||||
- '시스템 활동'과 '최근 활동' 위젯 간의 간격을 표준화 (24px로 통일)
|
|
||||||
- 최근 활동 섹션의 불필요한 중첩 레이아웃 제거 및 단순화
|
|
||||||
- 위젯 간 상하 간격 일관성 확보로 시각적 조화 개선
|
|
||||||
- UI 요소 정리 및 최적화
|
|
||||||
- SizedBox 고정 높이 제한 제거로 컨텐츠에 따른 자연스러운 크기 조정
|
|
||||||
- 중복 컨테이너 래핑 최소화로 레이아웃 성능 향상
|
|
||||||
|
|
||||||
## 대화 22: 대시보드 UI 간격 표준화
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 19일
|
|
||||||
- **내용**:
|
|
||||||
- 대시보드 화면의 모든 위젯 간 상하 간격 표준화:
|
|
||||||
- 주요 섹션 간 간격: 24px로 통일 (통계 그리드, 시스템 활동, 최근 활동)
|
|
||||||
- 섹션 내부 요소 간 간격: 16px로 통일
|
|
||||||
- 활동 차트 내부, 활동 레전드, 최근 활동 리스트 등의 간격 조정
|
|
||||||
- 컴포넌트 레이아웃 개선:
|
|
||||||
- 시스템 활동 카드에 하단 여백 추가
|
|
||||||
- 최근 활동 섹션에 상하 여백 추가
|
|
||||||
- 마지막 아이템 이후 불필요한 구분선 제거
|
|
||||||
- 전체적인 시각적 일관성 향상
|
|
||||||
- 모든 간격을 8px 단위의 배수로 설정 (8, 16, 24)
|
|
||||||
- 컴포넌트 내부 구성요소 간 관계성 강화
|
|
||||||
|
|
||||||
## 대화 23: 대시보드 UI 간격 미세 조정
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 19일
|
|
||||||
- **내용**:
|
|
||||||
- 대시보드 화면의 동일한 위젯 간 간격 문제 해결:
|
|
||||||
- MetronicCard 컴포넌트에 내장된 하단 마진(bottom: 16) 제거
|
|
||||||
- 마진 속성을 컴포넌트 매개변수로 제공하여 외부에서 조정 가능하도록 개선
|
|
||||||
- 각 카드의 시각적 간격 정확히 통일 (24px)
|
|
||||||
- 컴포넌트 아키텍처 개선:
|
|
||||||
- 중첩 스타일 속성을 통일하여 일관된 시각적 경험 제공
|
|
||||||
- 레이아웃 컴포넌트의 유연성 향상
|
|
||||||
- 다양한 화면에서 재사용 가능한 컴포넌트 설계
|
|
||||||
|
|
||||||
## 대화 24: 일관된 인터페이스를 위한 사이드바 메뉴 통합
|
|
||||||
|
|
||||||
- **날짜**: 2025년 04월 20일
|
|
||||||
- **내용**:
|
|
||||||
- 모든 주요 화면에 사이드바 메뉴 적용:
|
|
||||||
- 장비 입고 관리 화면에 MainLayout 적용
|
|
||||||
- 장비 출고 관리 화면에 MainLayout 적용
|
|
||||||
- 회사 관리 화면에 MainLayout 적용
|
|
||||||
- 사용자 관리 화면에 MainLayout 적용
|
|
||||||
- 라이센스 관리 화면에 MainLayout 적용
|
|
||||||
- 레이아웃 구조 개선:
|
|
||||||
- 모든 화면에 일관된 MainLayout 컴포넌트 적용
|
|
||||||
- 페이지별 적절한 currentRoute 값 설정으로 사이드바 메뉴 활성화 상태 관리
|
|
||||||
- 기존 Scaffold와 AppBar를 MainLayout으로 대체
|
|
||||||
- 사용자 경험 향상:
|
|
||||||
- 모든 화면에서 일관된 내비게이션 경험 제공
|
|
||||||
- 새로고침 및 추가 기능 버튼 통일
|
|
||||||
- 플로팅 액션 버튼을 통한 추가 기능 접근성 개선
|
|
||||||
65
lib/core/config/environment.dart
Normal file
65
lib/core/config/environment.dart
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
|
/// 환경 설정 관리 클래스
|
||||||
|
class Environment {
|
||||||
|
static const String dev = 'development';
|
||||||
|
static const String prod = 'production';
|
||||||
|
|
||||||
|
static late String _environment;
|
||||||
|
|
||||||
|
/// 현재 환경
|
||||||
|
static String get current => _environment;
|
||||||
|
|
||||||
|
/// 개발 환경 여부
|
||||||
|
static bool get isDevelopment => _environment == dev;
|
||||||
|
|
||||||
|
/// 프로덕션 환경 여부
|
||||||
|
static bool get isProduction => _environment == prod;
|
||||||
|
|
||||||
|
/// API 베이스 URL
|
||||||
|
static String get apiBaseUrl {
|
||||||
|
return dotenv.env['API_BASE_URL'] ?? 'http://localhost:8080/api/v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// API 타임아웃 (밀리초)
|
||||||
|
static int get apiTimeout {
|
||||||
|
final timeoutStr = dotenv.env['API_TIMEOUT'] ?? '30000';
|
||||||
|
return int.tryParse(timeoutStr) ?? 30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 로깅 활성화 여부
|
||||||
|
static bool get enableLogging {
|
||||||
|
final loggingStr = dotenv.env['ENABLE_LOGGING'] ?? 'false';
|
||||||
|
return loggingStr.toLowerCase() == 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 환경 초기화
|
||||||
|
static Future<void> initialize([String? environment]) async {
|
||||||
|
_environment = environment ??
|
||||||
|
const String.fromEnvironment('ENVIRONMENT', defaultValue: dev);
|
||||||
|
|
||||||
|
final envFile = _getEnvFile();
|
||||||
|
await dotenv.load(fileName: envFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 환경별 파일 경로 반환
|
||||||
|
static String _getEnvFile() {
|
||||||
|
switch (_environment) {
|
||||||
|
case prod:
|
||||||
|
return '.env.production';
|
||||||
|
case dev:
|
||||||
|
default:
|
||||||
|
return '.env.development';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 환경 변수 가져오기
|
||||||
|
static String? get(String key) {
|
||||||
|
return dotenv.env[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 환경 변수 존재 여부 확인
|
||||||
|
static bool has(String key) {
|
||||||
|
return dotenv.env.containsKey(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
lib/core/constants/api_endpoints.dart
Normal file
77
lib/core/constants/api_endpoints.dart
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/// API 엔드포인트 상수 정의
|
||||||
|
class ApiEndpoints {
|
||||||
|
// 인증
|
||||||
|
static const String login = '/auth/login';
|
||||||
|
static const String logout = '/auth/logout';
|
||||||
|
static const String refresh = '/auth/refresh';
|
||||||
|
static const String me = '/me';
|
||||||
|
|
||||||
|
// 장비 관리
|
||||||
|
static const String equipment = '/equipment';
|
||||||
|
static const String equipmentSearch = '/equipment/search';
|
||||||
|
static const String equipmentIn = '/equipment/in';
|
||||||
|
static const String equipmentOut = '/equipment/out';
|
||||||
|
static const String equipmentBatchOut = '/equipment/batch-out';
|
||||||
|
static const String equipmentManufacturers = '/equipment/manufacturers';
|
||||||
|
static const String equipmentNames = '/equipment/names';
|
||||||
|
static const String equipmentHistory = '/equipment/history';
|
||||||
|
static const String equipmentRentals = '/equipment/rentals';
|
||||||
|
static const String equipmentRepairs = '/equipment/repairs';
|
||||||
|
static const String equipmentDisposals = '/equipment/disposals';
|
||||||
|
|
||||||
|
// 회사 관리
|
||||||
|
static const String companies = '/companies';
|
||||||
|
static const String companiesSearch = '/companies/search';
|
||||||
|
static const String companiesNames = '/companies/names';
|
||||||
|
static const String companiesCheckDuplicate = '/companies/check-duplicate';
|
||||||
|
static const String companiesWithBranches = '/companies/with-branches';
|
||||||
|
static const String companiesBranches = '/companies/{id}/branches';
|
||||||
|
|
||||||
|
// 사용자 관리
|
||||||
|
static const String users = '/users';
|
||||||
|
static const String usersSearch = '/users/search';
|
||||||
|
static const String usersChangePassword = '/users/{id}/change-password';
|
||||||
|
static const String usersStatus = '/users/{id}/status';
|
||||||
|
|
||||||
|
// 라이선스 관리
|
||||||
|
static const String licenses = '/licenses';
|
||||||
|
static const String licensesExpiring = '/licenses/expiring';
|
||||||
|
static const String licensesAssign = '/licenses/{id}/assign';
|
||||||
|
static const String licensesUnassign = '/licenses/{id}/unassign';
|
||||||
|
|
||||||
|
// 창고 위치 관리
|
||||||
|
static const String warehouseLocations = '/warehouse-locations';
|
||||||
|
static const String warehouseLocationsSearch = '/warehouse-locations/search';
|
||||||
|
static const String warehouseEquipment = '/warehouse-locations/{id}/equipment';
|
||||||
|
static const String warehouseCapacity = '/warehouse-locations/{id}/capacity';
|
||||||
|
|
||||||
|
// 파일 관리
|
||||||
|
static const String filesUpload = '/files/upload';
|
||||||
|
static const String filesDownload = '/files/{id}';
|
||||||
|
|
||||||
|
// 보고서
|
||||||
|
static const String reports = '/reports';
|
||||||
|
static const String reportsPdf = '/reports/{type}/pdf';
|
||||||
|
static const String reportsExcel = '/reports/{type}/excel';
|
||||||
|
|
||||||
|
// 대시보드 및 통계
|
||||||
|
static const String overviewStats = '/overview/stats';
|
||||||
|
static const String overviewRecentActivities = '/overview/recent-activities';
|
||||||
|
static const String overviewEquipmentStatus = '/overview/equipment-status';
|
||||||
|
static const String overviewLicenseExpiry = '/overview/license-expiry';
|
||||||
|
|
||||||
|
// 대량 처리
|
||||||
|
static const String bulkUpload = '/bulk/upload';
|
||||||
|
static const String bulkUpdate = '/bulk/update';
|
||||||
|
|
||||||
|
// 감사 로그
|
||||||
|
static const String auditLogs = '/audit-logs';
|
||||||
|
|
||||||
|
// 백업
|
||||||
|
static const String backupCreate = '/backup/create';
|
||||||
|
static const String backupRestore = '/backup/restore';
|
||||||
|
|
||||||
|
// 검색 및 조회
|
||||||
|
static const String lookups = '/lookups';
|
||||||
|
static const String categories = '/lookups/categories';
|
||||||
|
}
|
||||||
75
lib/core/constants/app_constants.dart
Normal file
75
lib/core/constants/app_constants.dart
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/// 앱 전역 상수 정의
|
||||||
|
class AppConstants {
|
||||||
|
// API 관련
|
||||||
|
static const int defaultPageSize = 20;
|
||||||
|
static const int maxPageSize = 100;
|
||||||
|
static const Duration cacheTimeout = Duration(minutes: 5);
|
||||||
|
|
||||||
|
// 토큰 키
|
||||||
|
static const String accessTokenKey = 'access_token';
|
||||||
|
static const String refreshTokenKey = 'refresh_token';
|
||||||
|
static const String tokenTypeKey = 'token_type';
|
||||||
|
static const String expiresInKey = 'expires_in';
|
||||||
|
|
||||||
|
// 사용자 권한 매핑
|
||||||
|
static const Map<String, String> flutterToBackendRole = {
|
||||||
|
'S': 'admin', // Super user
|
||||||
|
'M': 'manager', // Manager
|
||||||
|
'U': 'staff', // User
|
||||||
|
'V': 'viewer', // Viewer
|
||||||
|
};
|
||||||
|
|
||||||
|
static const Map<String, String> backendToFlutterRole = {
|
||||||
|
'admin': 'S',
|
||||||
|
'manager': 'M',
|
||||||
|
'staff': 'U',
|
||||||
|
'viewer': 'V',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 장비 상태
|
||||||
|
static const Map<String, String> equipmentStatus = {
|
||||||
|
'available': '사용가능',
|
||||||
|
'in_use': '사용중',
|
||||||
|
'maintenance': '유지보수',
|
||||||
|
'disposed': '폐기',
|
||||||
|
'rented': '대여중',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 정렬 옵션
|
||||||
|
static const Map<String, String> sortOptions = {
|
||||||
|
'created_at': '생성일',
|
||||||
|
'updated_at': '수정일',
|
||||||
|
'name': '이름',
|
||||||
|
'status': '상태',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜 형식
|
||||||
|
static const String dateFormat = 'yyyy-MM-dd';
|
||||||
|
static const String dateTimeFormat = 'yyyy-MM-dd HH:mm:ss';
|
||||||
|
|
||||||
|
// 파일 업로드
|
||||||
|
static const int maxFileSize = 10 * 1024 * 1024; // 10MB
|
||||||
|
static const List<String> allowedFileExtensions = [
|
||||||
|
'jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 에러 메시지
|
||||||
|
static const String networkError = '네트워크 연결을 확인해주세요.';
|
||||||
|
static const String timeoutError = '요청 시간이 초과되었습니다.';
|
||||||
|
static const String unauthorizedError = '인증이 필요합니다.';
|
||||||
|
static const String serverError = '서버 오류가 발생했습니다.';
|
||||||
|
static const String unknownError = '알 수 없는 오류가 발생했습니다.';
|
||||||
|
|
||||||
|
// 정규식 패턴
|
||||||
|
static final RegExp emailRegex = RegExp(
|
||||||
|
r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+',
|
||||||
|
);
|
||||||
|
|
||||||
|
static final RegExp phoneRegex = RegExp(
|
||||||
|
r'^01[0-9]{1}-?[0-9]{4}-?[0-9]{4}$',
|
||||||
|
);
|
||||||
|
|
||||||
|
static final RegExp businessNumberRegex = RegExp(
|
||||||
|
r'^[0-9]{3}-?[0-9]{2}-?[0-9]{5}$',
|
||||||
|
);
|
||||||
|
}
|
||||||
117
lib/core/errors/exceptions.dart
Normal file
117
lib/core/errors/exceptions.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/// 커스텀 예외 클래스들 정의
|
||||||
|
|
||||||
|
/// 서버 예외
|
||||||
|
class ServerException implements Exception {
|
||||||
|
final String message;
|
||||||
|
final int? statusCode;
|
||||||
|
final Map<String, dynamic>? errors;
|
||||||
|
|
||||||
|
ServerException({
|
||||||
|
required this.message,
|
||||||
|
this.statusCode,
|
||||||
|
this.errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ServerException: $message (code: $statusCode)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 캐시 예외
|
||||||
|
class CacheException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
CacheException({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'CacheException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 네트워크 예외
|
||||||
|
class NetworkException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
NetworkException({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NetworkException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 인증 예외
|
||||||
|
class UnauthorizedException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
UnauthorizedException({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'UnauthorizedException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 유효성 검사 예외
|
||||||
|
class ValidationException implements Exception {
|
||||||
|
final String message;
|
||||||
|
final Map<String, List<String>>? fieldErrors;
|
||||||
|
|
||||||
|
ValidationException({
|
||||||
|
required this.message,
|
||||||
|
this.fieldErrors,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ValidationException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 권한 부족 예외
|
||||||
|
class ForbiddenException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
ForbiddenException({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ForbiddenException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 리소스 찾을 수 없음 예외
|
||||||
|
class NotFoundException implements Exception {
|
||||||
|
final String message;
|
||||||
|
final String? resourceType;
|
||||||
|
final String? resourceId;
|
||||||
|
|
||||||
|
NotFoundException({
|
||||||
|
required this.message,
|
||||||
|
this.resourceType,
|
||||||
|
this.resourceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotFoundException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 중복 리소스 예외
|
||||||
|
class DuplicateException implements Exception {
|
||||||
|
final String message;
|
||||||
|
final String? field;
|
||||||
|
final String? value;
|
||||||
|
|
||||||
|
DuplicateException({
|
||||||
|
required this.message,
|
||||||
|
this.field,
|
||||||
|
this.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'DuplicateException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 비즈니스 로직 예외
|
||||||
|
class BusinessException implements Exception {
|
||||||
|
final String message;
|
||||||
|
final String? code;
|
||||||
|
|
||||||
|
BusinessException({
|
||||||
|
required this.message,
|
||||||
|
this.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'BusinessException: $message (code: $code)';
|
||||||
|
}
|
||||||
117
lib/core/errors/failures.dart
Normal file
117
lib/core/errors/failures.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
|
/// 실패 처리를 위한 추상 클래스
|
||||||
|
abstract class Failure {
|
||||||
|
final String message;
|
||||||
|
final String? code;
|
||||||
|
|
||||||
|
const Failure({
|
||||||
|
required this.message,
|
||||||
|
this.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is Failure &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
message == other.message &&
|
||||||
|
code == other.code;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => message.hashCode ^ code.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 서버 실패
|
||||||
|
class ServerFailure extends Failure {
|
||||||
|
final int? statusCode;
|
||||||
|
final Map<String, dynamic>? errors;
|
||||||
|
|
||||||
|
const ServerFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
this.statusCode,
|
||||||
|
this.errors,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 캐시 실패
|
||||||
|
class CacheFailure extends Failure {
|
||||||
|
const CacheFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 네트워크 실패
|
||||||
|
class NetworkFailure extends Failure {
|
||||||
|
const NetworkFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 인증 실패
|
||||||
|
class AuthenticationFailure extends Failure {
|
||||||
|
const AuthenticationFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 권한 실패
|
||||||
|
class AuthorizationFailure extends Failure {
|
||||||
|
const AuthorizationFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 유효성 검사 실패
|
||||||
|
class ValidationFailure extends Failure {
|
||||||
|
final Map<String, List<String>>? fieldErrors;
|
||||||
|
|
||||||
|
const ValidationFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
this.fieldErrors,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 리소스 찾을 수 없음 실패
|
||||||
|
class NotFoundFailure extends Failure {
|
||||||
|
final String? resourceType;
|
||||||
|
final String? resourceId;
|
||||||
|
|
||||||
|
const NotFoundFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
this.resourceType,
|
||||||
|
this.resourceId,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 중복 리소스 실패
|
||||||
|
class DuplicateFailure extends Failure {
|
||||||
|
final String? field;
|
||||||
|
final String? value;
|
||||||
|
|
||||||
|
const DuplicateFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
this.field,
|
||||||
|
this.value,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 비즈니스 로직 실패
|
||||||
|
class BusinessFailure extends Failure {
|
||||||
|
const BusinessFailure({
|
||||||
|
required String message,
|
||||||
|
String? code,
|
||||||
|
}) : super(message: message, code: code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 타입 정의
|
||||||
|
typedef FutureEither<T> = Future<Either<Failure, T>>;
|
||||||
|
typedef FutureVoid = FutureEither<void>;
|
||||||
146
lib/core/utils/formatters.dart
Normal file
146
lib/core/utils/formatters.dart
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import '../constants/app_constants.dart';
|
||||||
|
|
||||||
|
/// 데이터 포맷터 유틸리티 클래스
|
||||||
|
class Formatters {
|
||||||
|
/// 날짜 포맷
|
||||||
|
static String formatDate(DateTime date) {
|
||||||
|
return DateFormat(AppConstants.dateFormat).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 날짜+시간 포맷
|
||||||
|
static String formatDateTime(DateTime dateTime) {
|
||||||
|
return DateFormat(AppConstants.dateTimeFormat).format(dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 상대적 시간 포맷 (예: 3분 전, 2시간 전)
|
||||||
|
static String formatRelativeTime(DateTime dateTime) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final difference = now.difference(dateTime);
|
||||||
|
|
||||||
|
if (difference.inDays > 365) {
|
||||||
|
return '${(difference.inDays / 365).floor()}년 전';
|
||||||
|
} else if (difference.inDays > 30) {
|
||||||
|
return '${(difference.inDays / 30).floor()}개월 전';
|
||||||
|
} else if (difference.inDays > 0) {
|
||||||
|
return '${difference.inDays}일 전';
|
||||||
|
} else if (difference.inHours > 0) {
|
||||||
|
return '${difference.inHours}시간 전';
|
||||||
|
} else if (difference.inMinutes > 0) {
|
||||||
|
return '${difference.inMinutes}분 전';
|
||||||
|
} else {
|
||||||
|
return '방금 전';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 숫자 포맷 (천 단위 구분)
|
||||||
|
static String formatNumber(int number) {
|
||||||
|
return NumberFormat('#,###').format(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 통화 포맷
|
||||||
|
static String formatCurrency(int amount) {
|
||||||
|
return NumberFormat.currency(
|
||||||
|
locale: 'ko_KR',
|
||||||
|
symbol: '₩',
|
||||||
|
decimalDigits: 0,
|
||||||
|
).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전화번호 포맷
|
||||||
|
static String formatPhone(String phone) {
|
||||||
|
final cleaned = phone.replaceAll(RegExp(r'[^0-9]'), '');
|
||||||
|
|
||||||
|
if (cleaned.length == 11) {
|
||||||
|
return '${cleaned.substring(0, 3)}-${cleaned.substring(3, 7)}-${cleaned.substring(7)}';
|
||||||
|
} else if (cleaned.length == 10) {
|
||||||
|
if (cleaned.startsWith('02')) {
|
||||||
|
return '${cleaned.substring(0, 2)}-${cleaned.substring(2, 6)}-${cleaned.substring(6)}';
|
||||||
|
} else {
|
||||||
|
return '${cleaned.substring(0, 3)}-${cleaned.substring(3, 6)}-${cleaned.substring(6)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사업자번호 포맷
|
||||||
|
static String formatBusinessNumber(String businessNumber) {
|
||||||
|
final cleaned = businessNumber.replaceAll(RegExp(r'[^0-9]'), '');
|
||||||
|
|
||||||
|
if (cleaned.length == 10) {
|
||||||
|
return '${cleaned.substring(0, 3)}-${cleaned.substring(3, 5)}-${cleaned.substring(5)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return businessNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 파일 크기 포맷
|
||||||
|
static String formatFileSize(int bytes) {
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return '$bytes B';
|
||||||
|
} else if (bytes < 1024 * 1024) {
|
||||||
|
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||||
|
} else if (bytes < 1024 * 1024 * 1024) {
|
||||||
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||||
|
} else {
|
||||||
|
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 백분율 포맷
|
||||||
|
static String formatPercentage(double value, {int decimals = 0}) {
|
||||||
|
return '${(value * 100).toStringAsFixed(decimals)}%';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 기간 포맷 (일수)
|
||||||
|
static String formatDuration(int days) {
|
||||||
|
if (days >= 365) {
|
||||||
|
final years = days ~/ 365;
|
||||||
|
final remainingDays = days % 365;
|
||||||
|
if (remainingDays > 0) {
|
||||||
|
return '$years년 $remainingDays일';
|
||||||
|
}
|
||||||
|
return '$years년';
|
||||||
|
} else if (days >= 30) {
|
||||||
|
final months = days ~/ 30;
|
||||||
|
final remainingDays = days % 30;
|
||||||
|
if (remainingDays > 0) {
|
||||||
|
return '$months개월 $remainingDays일';
|
||||||
|
}
|
||||||
|
return '$months개월';
|
||||||
|
} else {
|
||||||
|
return '$days일';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 상태 텍스트 변환
|
||||||
|
static String formatStatus(String status) {
|
||||||
|
return AppConstants.equipmentStatus[status] ?? status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 역할 텍스트 변환
|
||||||
|
static String formatRole(String role) {
|
||||||
|
switch (role) {
|
||||||
|
case 'S':
|
||||||
|
case 'admin':
|
||||||
|
return '관리자';
|
||||||
|
case 'M':
|
||||||
|
case 'manager':
|
||||||
|
return '매니저';
|
||||||
|
case 'U':
|
||||||
|
case 'staff':
|
||||||
|
return '직원';
|
||||||
|
case 'V':
|
||||||
|
case 'viewer':
|
||||||
|
return '열람자';
|
||||||
|
default:
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// null 값 처리
|
||||||
|
static String formatNullable(String? value, {String defaultValue = '-'}) {
|
||||||
|
return value?.isEmpty ?? true ? defaultValue : value!;
|
||||||
|
}
|
||||||
|
}
|
||||||
154
lib/core/utils/validators.dart
Normal file
154
lib/core/utils/validators.dart
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import '../constants/app_constants.dart';
|
||||||
|
|
||||||
|
/// 유효성 검사 유틸리티 클래스
|
||||||
|
class Validators {
|
||||||
|
/// 이메일 유효성 검사
|
||||||
|
static String? validateEmail(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '이메일을 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AppConstants.emailRegex.hasMatch(value)) {
|
||||||
|
return '올바른 이메일 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 비밀번호 유효성 검사
|
||||||
|
static String? validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '비밀번호를 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length < 8) {
|
||||||
|
return '비밀번호는 8자 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.contains(RegExp(r'[0-9]'))) {
|
||||||
|
return '비밀번호는 숫자를 포함해야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.contains(RegExp(r'[a-zA-Z]'))) {
|
||||||
|
return '비밀번호는 영문자를 포함해야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필수 입력 검사
|
||||||
|
static String? validateRequired(String? value, String fieldName) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return '$fieldName을(를) 입력해주세요.';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전화번호 유효성 검사
|
||||||
|
static String? validatePhone(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '전화번호를 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
final cleanedValue = value.replaceAll('-', '');
|
||||||
|
if (!AppConstants.phoneRegex.hasMatch(cleanedValue)) {
|
||||||
|
return '올바른 전화번호 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사업자번호 유효성 검사
|
||||||
|
static String? validateBusinessNumber(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '사업자번호를 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
final cleanedValue = value.replaceAll('-', '');
|
||||||
|
if (!AppConstants.businessNumberRegex.hasMatch(cleanedValue)) {
|
||||||
|
return '올바른 사업자번호 형식이 아닙니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 숫자 유효성 검사
|
||||||
|
static String? validateNumber(String? value, {
|
||||||
|
required String fieldName,
|
||||||
|
int? min,
|
||||||
|
int? max,
|
||||||
|
}) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '$fieldName을(를) 입력해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
final number = int.tryParse(value);
|
||||||
|
if (number == null) {
|
||||||
|
return '숫자만 입력 가능합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min != null && number < min) {
|
||||||
|
return '$fieldName은(는) $min 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max != null && number > max) {
|
||||||
|
return '$fieldName은(는) $max 이하여야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 날짜 범위 유효성 검사
|
||||||
|
static String? validateDateRange(
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? endDate,
|
||||||
|
) {
|
||||||
|
if (startDate == null || endDate == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate.isAfter(endDate)) {
|
||||||
|
return '시작일은 종료일보다 이전이어야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 파일 확장자 유효성 검사
|
||||||
|
static String? validateFileExtension(String fileName) {
|
||||||
|
final extension = fileName.split('.').last.toLowerCase();
|
||||||
|
|
||||||
|
if (!AppConstants.allowedFileExtensions.contains(extension)) {
|
||||||
|
return '허용되지 않는 파일 형식입니다. (${AppConstants.allowedFileExtensions.join(', ')})';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 파일 크기 유효성 검사
|
||||||
|
static String? validateFileSize(int sizeInBytes) {
|
||||||
|
if (sizeInBytes > AppConstants.maxFileSize) {
|
||||||
|
final maxSizeMB = AppConstants.maxFileSize / (1024 * 1024);
|
||||||
|
return '파일 크기는 ${maxSizeMB}MB를 초과할 수 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 시리얼 번호 유효성 검사
|
||||||
|
static String? validateSerialNumber(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return null; // 시리얼 번호는 선택사항
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length < 5) {
|
||||||
|
return '시리얼 번호는 5자 이상이어야 합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RegExp(r'^[A-Za-z0-9-]+$').hasMatch(value)) {
|
||||||
|
return '시리얼 번호는 영문, 숫자, 하이픈(-)만 사용 가능합니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
212
lib/data/datasources/remote/api_client.dart
Normal file
212
lib/data/datasources/remote/api_client.dart
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../../../core/config/environment.dart';
|
||||||
|
import 'interceptors/auth_interceptor.dart';
|
||||||
|
import 'interceptors/error_interceptor.dart';
|
||||||
|
import 'interceptors/logging_interceptor.dart';
|
||||||
|
|
||||||
|
/// API 클라이언트 클래스
|
||||||
|
class ApiClient {
|
||||||
|
late final Dio _dio;
|
||||||
|
|
||||||
|
static final ApiClient _instance = ApiClient._internal();
|
||||||
|
|
||||||
|
factory ApiClient() => _instance;
|
||||||
|
|
||||||
|
ApiClient._internal() {
|
||||||
|
_dio = Dio(_baseOptions);
|
||||||
|
_setupInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dio 인스턴스 getter
|
||||||
|
Dio get dio => _dio;
|
||||||
|
|
||||||
|
/// 기본 옵션 설정
|
||||||
|
BaseOptions get _baseOptions => BaseOptions(
|
||||||
|
baseUrl: Environment.apiBaseUrl,
|
||||||
|
connectTimeout: Duration(milliseconds: Environment.apiTimeout),
|
||||||
|
receiveTimeout: Duration(milliseconds: Environment.apiTimeout),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
validateStatus: (status) {
|
||||||
|
return status != null && status < 500;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 인터셉터 설정
|
||||||
|
void _setupInterceptors() {
|
||||||
|
_dio.interceptors.clear();
|
||||||
|
|
||||||
|
// 인증 인터셉터
|
||||||
|
_dio.interceptors.add(AuthInterceptor());
|
||||||
|
|
||||||
|
// 에러 처리 인터셉터
|
||||||
|
_dio.interceptors.add(ErrorInterceptor());
|
||||||
|
|
||||||
|
// 로깅 인터셉터 (개발 환경에서만)
|
||||||
|
if (Environment.enableLogging && kDebugMode) {
|
||||||
|
_dio.interceptors.add(LoggingInterceptor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 토큰 업데이트
|
||||||
|
void updateAuthToken(String token) {
|
||||||
|
_dio.options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 토큰 제거
|
||||||
|
void removeAuthToken() {
|
||||||
|
_dio.options.headers.remove('Authorization');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET 요청
|
||||||
|
Future<Response<T>> get<T>(
|
||||||
|
String path, {
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
ProgressCallback? onReceiveProgress,
|
||||||
|
}) {
|
||||||
|
return _dio.get<T>(
|
||||||
|
path,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
onReceiveProgress: onReceiveProgress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST 요청
|
||||||
|
Future<Response<T>> post<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
ProgressCallback? onSendProgress,
|
||||||
|
ProgressCallback? onReceiveProgress,
|
||||||
|
}) {
|
||||||
|
return _dio.post<T>(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
|
onReceiveProgress: onReceiveProgress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PUT 요청
|
||||||
|
Future<Response<T>> put<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
ProgressCallback? onSendProgress,
|
||||||
|
ProgressCallback? onReceiveProgress,
|
||||||
|
}) {
|
||||||
|
return _dio.put<T>(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
|
onReceiveProgress: onReceiveProgress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PATCH 요청
|
||||||
|
Future<Response<T>> patch<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
ProgressCallback? onSendProgress,
|
||||||
|
ProgressCallback? onReceiveProgress,
|
||||||
|
}) {
|
||||||
|
return _dio.patch<T>(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
|
onReceiveProgress: onReceiveProgress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DELETE 요청
|
||||||
|
Future<Response<T>> delete<T>(
|
||||||
|
String path, {
|
||||||
|
dynamic data,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
Options? options,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) {
|
||||||
|
return _dio.delete<T>(
|
||||||
|
path,
|
||||||
|
data: data,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
options: options,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 파일 업로드
|
||||||
|
Future<Response<T>> uploadFile<T>(
|
||||||
|
String path, {
|
||||||
|
required String filePath,
|
||||||
|
required String fileFieldName,
|
||||||
|
Map<String, dynamic>? additionalData,
|
||||||
|
ProgressCallback? onSendProgress,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
}) async {
|
||||||
|
final fileName = filePath.split('/').last;
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
fileFieldName: await MultipartFile.fromFile(
|
||||||
|
filePath,
|
||||||
|
filename: fileName,
|
||||||
|
),
|
||||||
|
...?additionalData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return _dio.post<T>(
|
||||||
|
path,
|
||||||
|
data: formData,
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onSendProgress: onSendProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 파일 다운로드
|
||||||
|
Future<Response> downloadFile(
|
||||||
|
String path, {
|
||||||
|
required String savePath,
|
||||||
|
ProgressCallback? onReceiveProgress,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
Map<String, dynamic>? queryParameters,
|
||||||
|
}) {
|
||||||
|
return _dio.download(
|
||||||
|
path,
|
||||||
|
savePath,
|
||||||
|
queryParameters: queryParameters,
|
||||||
|
onReceiveProgress: onReceiveProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.bytes,
|
||||||
|
followRedirects: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
lib/data/datasources/remote/interceptors/auth_interceptor.dart
Normal file
122
lib/data/datasources/remote/interceptors/auth_interceptor.dart
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import '../../../../core/constants/app_constants.dart';
|
||||||
|
import '../../../../core/constants/api_endpoints.dart';
|
||||||
|
|
||||||
|
/// 인증 인터셉터
|
||||||
|
class AuthInterceptor extends Interceptor {
|
||||||
|
final _storage = const FlutterSecureStorage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequest(
|
||||||
|
RequestOptions options,
|
||||||
|
RequestInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
// 로그인, 토큰 갱신 요청은 토큰 없이 진행
|
||||||
|
if (_isAuthEndpoint(options.path)) {
|
||||||
|
handler.next(options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 저장된 액세스 토큰 가져오기
|
||||||
|
final accessToken = await _storage.read(key: AppConstants.accessTokenKey);
|
||||||
|
|
||||||
|
if (accessToken != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $accessToken';
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(
|
||||||
|
DioException err,
|
||||||
|
ErrorInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
// 401 Unauthorized 에러 처리
|
||||||
|
if (err.response?.statusCode == 401) {
|
||||||
|
// 토큰 갱신 시도
|
||||||
|
final refreshSuccess = await _refreshToken();
|
||||||
|
|
||||||
|
if (refreshSuccess) {
|
||||||
|
// 새로운 토큰으로 원래 요청 재시도
|
||||||
|
try {
|
||||||
|
final newAccessToken = await _storage.read(key: AppConstants.accessTokenKey);
|
||||||
|
|
||||||
|
if (newAccessToken != null) {
|
||||||
|
err.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';
|
||||||
|
|
||||||
|
final response = await Dio().fetch(err.requestOptions);
|
||||||
|
handler.resolve(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 재시도 실패
|
||||||
|
handler.next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 토큰 갱신 실패 시 로그인 화면으로 이동
|
||||||
|
await _clearTokens();
|
||||||
|
// TODO: Navigate to login screen
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 토큰 갱신
|
||||||
|
Future<bool> _refreshToken() async {
|
||||||
|
try {
|
||||||
|
final refreshToken = await _storage.read(key: AppConstants.refreshTokenKey);
|
||||||
|
|
||||||
|
if (refreshToken == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dio = Dio();
|
||||||
|
final response = await dio.post(
|
||||||
|
'${dio.options.baseUrl}${ApiEndpoints.refresh}',
|
||||||
|
data: {
|
||||||
|
'refresh_token': refreshToken,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && response.data != null) {
|
||||||
|
final data = response.data;
|
||||||
|
|
||||||
|
// 새로운 토큰 저장
|
||||||
|
await _storage.write(
|
||||||
|
key: AppConstants.accessTokenKey,
|
||||||
|
value: data['access_token'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data['refresh_token'] != null) {
|
||||||
|
await _storage.write(
|
||||||
|
key: AppConstants.refreshTokenKey,
|
||||||
|
value: data['refresh_token'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 토큰 삭제
|
||||||
|
Future<void> _clearTokens() async {
|
||||||
|
await _storage.delete(key: AppConstants.accessTokenKey);
|
||||||
|
await _storage.delete(key: AppConstants.refreshTokenKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 인증 관련 엔드포인트 확인
|
||||||
|
bool _isAuthEndpoint(String path) {
|
||||||
|
return path.contains(ApiEndpoints.login) ||
|
||||||
|
path.contains(ApiEndpoints.refresh) ||
|
||||||
|
path.contains(ApiEndpoints.logout);
|
||||||
|
}
|
||||||
|
}
|
||||||
253
lib/data/datasources/remote/interceptors/error_interceptor.dart
Normal file
253
lib/data/datasources/remote/interceptors/error_interceptor.dart
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../../../core/errors/exceptions.dart';
|
||||||
|
import '../../../../core/constants/app_constants.dart';
|
||||||
|
|
||||||
|
/// 에러 처리 인터셉터
|
||||||
|
class ErrorInterceptor extends Interceptor {
|
||||||
|
@override
|
||||||
|
void onError(
|
||||||
|
DioException err,
|
||||||
|
ErrorInterceptorHandler handler,
|
||||||
|
) {
|
||||||
|
// 에러 타입에 따른 처리
|
||||||
|
switch (err.type) {
|
||||||
|
case DioExceptionType.connectionTimeout:
|
||||||
|
case DioExceptionType.sendTimeout:
|
||||||
|
case DioExceptionType.receiveTimeout:
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: NetworkException(
|
||||||
|
message: AppConstants.timeoutError,
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DioExceptionType.connectionError:
|
||||||
|
if (err.error is SocketException) {
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: NetworkException(
|
||||||
|
message: AppConstants.networkError,
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
handler.reject(err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DioExceptionType.badResponse:
|
||||||
|
_handleBadResponse(err, handler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DioExceptionType.cancel:
|
||||||
|
handler.reject(err);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DioExceptionType.badCertificate:
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: NetworkException(
|
||||||
|
message: '보안 인증서 오류가 발생했습니다.',
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DioExceptionType.unknown:
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: ServerException(
|
||||||
|
message: AppConstants.unknownError,
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 잘못된 응답 처리
|
||||||
|
void _handleBadResponse(
|
||||||
|
DioException err,
|
||||||
|
ErrorInterceptorHandler handler,
|
||||||
|
) {
|
||||||
|
final statusCode = err.response?.statusCode;
|
||||||
|
final data = err.response?.data;
|
||||||
|
|
||||||
|
String message = AppConstants.serverError;
|
||||||
|
Map<String, dynamic>? errors;
|
||||||
|
|
||||||
|
// API 응답에서 에러 메시지 추출
|
||||||
|
if (data != null) {
|
||||||
|
if (data is Map) {
|
||||||
|
message = data['message'] ?? data['error'] ?? message;
|
||||||
|
errors = data['errors'] as Map<String, dynamic>?;
|
||||||
|
} else if (data is String) {
|
||||||
|
message = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태 코드별 예외 처리
|
||||||
|
switch (statusCode) {
|
||||||
|
case 400:
|
||||||
|
// Bad Request - 유효성 검사 실패
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: ValidationException(
|
||||||
|
message: message,
|
||||||
|
fieldErrors: _parseFieldErrors(errors),
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 401:
|
||||||
|
// Unauthorized - 인증 실패
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: UnauthorizedException(
|
||||||
|
message: AppConstants.unauthorizedError,
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 403:
|
||||||
|
// Forbidden - 권한 부족
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: ForbiddenException(
|
||||||
|
message: '해당 작업을 수행할 권한이 없습니다.',
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 404:
|
||||||
|
// Not Found - 리소스 없음
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: NotFoundException(
|
||||||
|
message: message,
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 409:
|
||||||
|
// Conflict - 중복 리소스
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: DuplicateException(
|
||||||
|
message: message,
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 422:
|
||||||
|
// Unprocessable Entity - 비즈니스 로직 오류
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: BusinessException(
|
||||||
|
message: message,
|
||||||
|
code: data?['code'],
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 429:
|
||||||
|
// Too Many Requests
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: NetworkException(
|
||||||
|
message: '너무 많은 요청을 보냈습니다. 잠시 후 다시 시도해주세요.',
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 500:
|
||||||
|
case 502:
|
||||||
|
case 503:
|
||||||
|
case 504:
|
||||||
|
// Server Error
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: ServerException(
|
||||||
|
message: AppConstants.serverError,
|
||||||
|
statusCode: statusCode,
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
handler.reject(
|
||||||
|
DioException(
|
||||||
|
requestOptions: err.requestOptions,
|
||||||
|
error: ServerException(
|
||||||
|
message: message,
|
||||||
|
statusCode: statusCode,
|
||||||
|
errors: errors,
|
||||||
|
),
|
||||||
|
type: err.type,
|
||||||
|
response: err.response,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 필드 에러 파싱
|
||||||
|
Map<String, List<String>>? _parseFieldErrors(Map<String, dynamic>? errors) {
|
||||||
|
if (errors == null) return null;
|
||||||
|
|
||||||
|
final fieldErrors = <String, List<String>>{};
|
||||||
|
|
||||||
|
errors.forEach((key, value) {
|
||||||
|
if (value is List) {
|
||||||
|
fieldErrors[key] = value.map((e) => e.toString()).toList();
|
||||||
|
} else if (value is String) {
|
||||||
|
fieldErrors[key] = [value];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fieldErrors.isNotEmpty ? fieldErrors : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// 로깅 인터셉터
|
||||||
|
class LoggingInterceptor extends Interceptor {
|
||||||
|
@override
|
||||||
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
|
debugPrint('╔════════════════════════════════════════════════════════════');
|
||||||
|
debugPrint('║ REQUEST');
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ ${options.method} ${options.uri}');
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ Headers:');
|
||||||
|
options.headers.forEach((key, value) {
|
||||||
|
// 민감한 정보 마스킹
|
||||||
|
if (key.toLowerCase() == 'authorization') {
|
||||||
|
debugPrint('║ $key: ${_maskToken(value.toString())}');
|
||||||
|
} else {
|
||||||
|
debugPrint('║ $key: $value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.queryParameters.isNotEmpty) {
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ Query Parameters:');
|
||||||
|
options.queryParameters.forEach((key, value) {
|
||||||
|
debugPrint('║ $key: $value');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.data != null) {
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ Request Body:');
|
||||||
|
try {
|
||||||
|
final formattedData = _formatJson(options.data);
|
||||||
|
formattedData.split('\n').forEach((line) {
|
||||||
|
debugPrint('║ $line');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('║ ${options.data}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('╚════════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||||
|
final requestTime = response.requestOptions.extra['requestTime'] as DateTime?;
|
||||||
|
final responseTime = DateTime.now();
|
||||||
|
final duration = requestTime != null
|
||||||
|
? responseTime.difference(requestTime).inMilliseconds
|
||||||
|
: null;
|
||||||
|
|
||||||
|
debugPrint('╔════════════════════════════════════════════════════════════');
|
||||||
|
debugPrint('║ RESPONSE');
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ ${response.requestOptions.method} ${response.requestOptions.uri}');
|
||||||
|
debugPrint('║ Status: ${response.statusCode} ${response.statusMessage}');
|
||||||
|
if (duration != null) {
|
||||||
|
debugPrint('║ Duration: ${duration}ms');
|
||||||
|
}
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ Headers:');
|
||||||
|
response.headers.forEach((key, values) {
|
||||||
|
debugPrint('║ $key: ${values.join(', ')}');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data != null) {
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ Response Body:');
|
||||||
|
try {
|
||||||
|
final formattedData = _formatJson(response.data);
|
||||||
|
final lines = formattedData.split('\n');
|
||||||
|
// 너무 긴 응답은 일부만 출력
|
||||||
|
if (lines.length > 50) {
|
||||||
|
lines.take(25).forEach((line) {
|
||||||
|
debugPrint('║ $line');
|
||||||
|
});
|
||||||
|
debugPrint('║ ... (${lines.length - 50} lines omitted) ...');
|
||||||
|
lines.skip(lines.length - 25).forEach((line) {
|
||||||
|
debugPrint('║ $line');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
lines.forEach((line) {
|
||||||
|
debugPrint('║ $line');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('║ ${response.data}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('╚════════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
handler.next(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
|
debugPrint('╔════════════════════════════════════════════════════════════');
|
||||||
|
debugPrint('║ ERROR');
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ ${err.requestOptions.method} ${err.requestOptions.uri}');
|
||||||
|
debugPrint('║ Error Type: ${err.type}');
|
||||||
|
debugPrint('║ Error Message: ${err.message}');
|
||||||
|
|
||||||
|
if (err.response != null) {
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ Status: ${err.response!.statusCode} ${err.response!.statusMessage}');
|
||||||
|
|
||||||
|
if (err.response!.data != null) {
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ Error Response:');
|
||||||
|
try {
|
||||||
|
final formattedData = _formatJson(err.response!.data);
|
||||||
|
formattedData.split('\n').forEach((line) {
|
||||||
|
debugPrint('║ $line');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('║ ${err.response!.data}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.error != null) {
|
||||||
|
debugPrint('╟────────────────────────────────────────────────────────────');
|
||||||
|
debugPrint('║ Original Error: ${err.error}');
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('╚════════════════════════════════════════════════════════════');
|
||||||
|
|
||||||
|
handler.next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON 포맷팅
|
||||||
|
String _formatJson(dynamic data) {
|
||||||
|
try {
|
||||||
|
if (data is String) {
|
||||||
|
final parsed = json.decode(data);
|
||||||
|
return const JsonEncoder.withIndent(' ').convert(parsed);
|
||||||
|
} else {
|
||||||
|
return const JsonEncoder.withIndent(' ').convert(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return data.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 토큰 마스킹
|
||||||
|
String _maskToken(String token) {
|
||||||
|
if (token.length <= 20) {
|
||||||
|
return '***MASKED***';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bearer 프리픽스 처리
|
||||||
|
if (token.startsWith('Bearer ')) {
|
||||||
|
final actualToken = token.substring(7);
|
||||||
|
if (actualToken.length > 20) {
|
||||||
|
return 'Bearer ${actualToken.substring(0, 10)}...${actualToken.substring(actualToken.length - 10)}';
|
||||||
|
}
|
||||||
|
return 'Bearer ***MASKED***';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${token.substring(0, 10)}...${token.substring(token.length - 10)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 요청 시간 측정을 위한 확장
|
||||||
|
extension RequestOptionsExtension on RequestOptions {
|
||||||
|
void setRequestTime() {
|
||||||
|
extra['requestTime'] = DateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
65
lib/di/injection_container.dart
Normal file
65
lib/di/injection_container.dart
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import '../core/config/environment.dart';
|
||||||
|
import '../data/datasources/remote/api_client.dart';
|
||||||
|
|
||||||
|
/// GetIt 인스턴스
|
||||||
|
final getIt = GetIt.instance;
|
||||||
|
|
||||||
|
/// 의존성 주입 설정
|
||||||
|
Future<void> setupDependencies() async {
|
||||||
|
// 환경 초기화
|
||||||
|
await Environment.initialize();
|
||||||
|
|
||||||
|
// 외부 라이브러리
|
||||||
|
getIt.registerLazySingleton(() => Dio());
|
||||||
|
getIt.registerLazySingleton(() => const FlutterSecureStorage());
|
||||||
|
|
||||||
|
// API 클라이언트
|
||||||
|
getIt.registerLazySingleton(() => ApiClient());
|
||||||
|
|
||||||
|
// 데이터소스
|
||||||
|
// TODO: Remote datasources will be registered here
|
||||||
|
|
||||||
|
// 리포지토리
|
||||||
|
// TODO: Repositories will be registered here
|
||||||
|
|
||||||
|
// 유스케이스
|
||||||
|
// TODO: Use cases will be registered here
|
||||||
|
|
||||||
|
// 컨트롤러/프로바이더
|
||||||
|
// TODO: Controllers will be registered here
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 의존성 리셋 (테스트용)
|
||||||
|
Future<void> resetDependencies() async {
|
||||||
|
await getIt.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 특정 타입의 의존성 가져오기
|
||||||
|
T inject<T extends Object>() => getIt.get<T>();
|
||||||
|
|
||||||
|
/// 특정 타입의 의존성이 등록되어 있는지 확인
|
||||||
|
bool isRegistered<T extends Object>() => getIt.isRegistered<T>();
|
||||||
|
|
||||||
|
/// 의존성 등록 헬퍼 함수들
|
||||||
|
extension GetItHelpers on GetIt {
|
||||||
|
/// 싱글톤 등록 헬퍼
|
||||||
|
void registerSingletonIfNotRegistered<T extends Object>(
|
||||||
|
T Function() factory,
|
||||||
|
) {
|
||||||
|
if (!isRegistered<T>()) {
|
||||||
|
registerLazySingleton<T>(factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 팩토리 등록 헬퍼
|
||||||
|
void registerFactoryIfNotRegistered<T extends Object>(
|
||||||
|
T Function() factory,
|
||||||
|
) {
|
||||||
|
if (!isRegistered<T>()) {
|
||||||
|
registerFactory<T>(factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,8 +11,15 @@ import 'package:superport/screens/warehouse_location/warehouse_location_form.dar
|
|||||||
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/login/login_screen.dart';
|
import 'package:superport/screens/login/login_screen.dart';
|
||||||
|
import 'package:superport/di/injection_container.dart' as di;
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
|
// Flutter 바인딩 초기화
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// 의존성 주입 설정
|
||||||
|
await di.setupDependencies();
|
||||||
|
|
||||||
// MockDataService는 싱글톤으로 자동 초기화됨
|
// MockDataService는 싱글톤으로 자동 초기화됨
|
||||||
runApp(const SuperportApp());
|
runApp(const SuperportApp());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ class ShadcnTheme {
|
|||||||
titleTextStyle: headingH4,
|
titleTextStyle: headingH4,
|
||||||
iconTheme: const IconThemeData(color: foreground),
|
iconTheme: const IconThemeData(color: foreground),
|
||||||
),
|
),
|
||||||
cardTheme: CardTheme(
|
cardTheme: CardThemeData(
|
||||||
color: card,
|
color: card,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class AppThemeTailwind {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// 카드 테마
|
// 카드 테마
|
||||||
cardTheme: CardTheme(
|
cardTheme: CardThemeData(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) printing_registrar =
|
g_autoptr(FlPluginRegistrar) printing_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
||||||
printing_plugin_register_with_registrar(printing_registrar);
|
printing_plugin_register_with_registrar(printing_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
flutter_secure_storage_linux
|
||||||
printing
|
printing
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import flutter_secure_storage_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import printing
|
import printing
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
513
pubspec.lock
513
pubspec.lock
@@ -1,6 +1,27 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "76.0.0"
|
||||||
|
_macros:
|
||||||
|
dependency: transitive
|
||||||
|
description: dart
|
||||||
|
source: sdk
|
||||||
|
version: "0.3.3"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.11.0"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -21,10 +42,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.0"
|
version: "2.13.0"
|
||||||
barcode:
|
barcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -49,6 +70,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
build:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build
|
||||||
|
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
build_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_config
|
||||||
|
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
build_daemon:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_daemon
|
||||||
|
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.4"
|
||||||
|
build_resolvers:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_resolvers
|
||||||
|
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
build_runner:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: build_runner
|
||||||
|
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
build_runner_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_runner_core
|
||||||
|
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.2"
|
||||||
|
built_collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: built_collection
|
||||||
|
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.1"
|
||||||
|
built_value:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: built_value
|
||||||
|
sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.11.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -57,6 +142,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
checked_yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: checked_yaml
|
||||||
|
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -65,6 +158,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_builder:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_builder
|
||||||
|
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.10.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -73,6 +174,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -81,14 +190,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.6"
|
||||||
|
dart_style:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_style
|
||||||
|
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.8"
|
||||||
|
dartz:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: dartz
|
||||||
|
sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.1"
|
||||||
|
dio:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: dio
|
||||||
|
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.8.0+1"
|
||||||
|
dio_web_adapter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio_web_adapter
|
||||||
|
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -97,11 +238,35 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_dotenv:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_dotenv
|
||||||
|
sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.2.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -115,6 +280,54 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_secure_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage
|
||||||
|
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.2.4"
|
||||||
|
flutter_secure_storage_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_linux
|
||||||
|
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.3"
|
||||||
|
flutter_secure_storage_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_macos
|
||||||
|
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
flutter_secure_storage_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_platform_interface
|
||||||
|
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
flutter_secure_storage_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_web
|
||||||
|
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
flutter_secure_storage_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_windows
|
||||||
|
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -133,6 +346,30 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
frontend_server_client:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: frontend_server_client
|
||||||
|
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
get_it:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: get_it
|
||||||
|
sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.7.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
google_fonts:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -141,6 +378,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.1"
|
version: "6.2.1"
|
||||||
|
graphs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: graphs
|
||||||
|
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -149,6 +394,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
http_multi_server:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_multi_server
|
||||||
|
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -165,22 +418,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.4"
|
version: "4.5.4"
|
||||||
intl:
|
injectable:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: injectable
|
||||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
sha256: "5e1556ea1d374fe44cbe846414d9bab346285d3d8a1da5877c01ad0774006068"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.0"
|
version: "2.5.0"
|
||||||
|
injectable_generator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: injectable_generator
|
||||||
|
sha256: af403d76c7b18b4217335e0075e950cd0579fd7f8d7bd47ee7c85ada31680ba1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.2"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.20.2"
|
||||||
|
io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: io
|
||||||
|
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.7"
|
||||||
|
json_annotation:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.9.0"
|
||||||
|
json_serializable:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: json_serializable
|
||||||
|
sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.9.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.8"
|
version: "10.0.9"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -205,6 +506,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.1.1"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
macros:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: macros
|
||||||
|
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3-main.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -229,6 +546,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -237,6 +562,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -341,6 +674,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
posix:
|
posix:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -357,6 +698,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.14.2"
|
version: "5.14.2"
|
||||||
|
protobuf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protobuf
|
||||||
|
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -365,6 +714,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5"
|
version: "6.1.5"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
pubspec_parse:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pubspec_parse
|
||||||
|
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
qr:
|
qr:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -373,11 +738,67 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
recase:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: recase
|
||||||
|
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
|
retrofit:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: retrofit
|
||||||
|
sha256: "84d70114a5b6bae5f4c1302335f9cb610ebeb1b02023d5e7e87697aaff52926a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.0"
|
||||||
|
retrofit_generator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: retrofit_generator
|
||||||
|
sha256: "8dfc406cdfa171f33cbd21bf5bd8b6763548cc217de19cdeaa07a76727fac4ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.2.1"
|
||||||
|
shelf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.2"
|
||||||
|
shelf_web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_web_socket
|
||||||
|
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
source_gen:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_gen
|
||||||
|
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
|
source_helper:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_helper
|
||||||
|
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.5"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -402,6 +823,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -426,6 +855,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.4"
|
||||||
|
timing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timing
|
||||||
|
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
|
tuple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: tuple
|
||||||
|
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -470,10 +915,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.1"
|
version: "15.0.0"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
wave:
|
wave:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -490,6 +943,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket
|
||||||
|
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.14.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -506,6 +983,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.0"
|
version: "6.5.0"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.2 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.27.0"
|
||||||
|
|||||||
29
pubspec.yaml
29
pubspec.yaml
@@ -17,11 +17,40 @@ dependencies:
|
|||||||
wave: ^0.2.2
|
wave: ^0.2.2
|
||||||
flutter_svg: ^2.0.10
|
flutter_svg: ^2.0.10
|
||||||
google_fonts: ^6.1.0
|
google_fonts: ^6.1.0
|
||||||
|
|
||||||
|
# 네트워크
|
||||||
|
dio: ^5.4.0
|
||||||
|
retrofit: ^4.1.0
|
||||||
|
|
||||||
|
# 보안 저장소
|
||||||
|
flutter_secure_storage: ^9.0.0
|
||||||
|
|
||||||
|
# 의존성 주입
|
||||||
|
get_it: ^7.6.7
|
||||||
|
injectable: ^2.3.2
|
||||||
|
|
||||||
|
# JSON 처리
|
||||||
|
json_annotation: ^4.8.1
|
||||||
|
|
||||||
|
# 환경 설정
|
||||||
|
flutter_dotenv: ^5.1.0
|
||||||
|
|
||||||
|
# 에러 처리
|
||||||
|
dartz: ^0.10.1
|
||||||
|
|
||||||
|
# 국제화 및 포맷팅
|
||||||
|
intl: ^0.20.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
|
|
||||||
|
# 코드 생성
|
||||||
|
retrofit_generator: ^8.0.6
|
||||||
|
build_runner: ^2.4.8
|
||||||
|
json_serializable: ^6.7.1
|
||||||
|
injectable_generator: ^2.4.1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
PrintingPluginRegisterWithRegistrar(
|
PrintingPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
flutter_secure_storage_windows
|
||||||
printing
|
printing
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user