From 2b31d3af5f65f97c69cf16b0c53f5d82769e4e95 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Thu, 24 Jul 2025 14:54:28 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20API=20=ED=86=B5=ED=95=A9=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EA=B8=B0=EC=B4=88=20=EC=9D=B8=ED=94=84?= =?UTF-8?q?=EB=9D=BC=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 네트워크 레이어 구현 (Dio 기반 ApiClient) - 환경별 설정 관리 시스템 구축 - 의존성 주입 설정 (GetIt) - API 엔드포인트 상수 정의 - 인터셉터 구현 (Auth, Error, Logging) - 프로젝트 아키텍처 개선 (core, data, di 디렉토리 구조) - API 통합 계획서 및 요구사항 문서 작성 - 필요 패키지 추가 (dio, flutter_secure_storage, get_it 등) --- .env.example | 6 + .gitignore | 5 + CLAUDE.md | 174 ++++ ..._Additional_Implementation_Requirements.md | 335 +++++++ doc/API_Integration_Plan.md | 893 ++++++++++++++++++ doc/development_log.md | 327 ------- doc/refac.md | 0 lib/core/config/environment.dart | 65 ++ lib/core/constants/api_endpoints.dart | 77 ++ lib/core/constants/app_constants.dart | 75 ++ lib/core/errors/exceptions.dart | 117 +++ lib/core/errors/failures.dart | 117 +++ lib/core/utils/formatters.dart | 146 +++ lib/core/utils/validators.dart | 154 +++ lib/data/datasources/remote/api_client.dart | 212 +++++ .../remote/interceptors/auth_interceptor.dart | 122 +++ .../interceptors/error_interceptor.dart | 253 +++++ .../interceptors/logging_interceptor.dart | 177 ++++ lib/di/injection_container.dart | 65 ++ lib/main.dart | 9 +- lib/screens/common/theme_shadcn.dart | 2 +- lib/screens/common/theme_tailwind.dart | 2 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 513 +++++++++- pubspec.yaml | 29 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 29 files changed, 3542 insertions(+), 344 deletions(-) create mode 100644 .env.example create mode 100644 CLAUDE.md create mode 100644 doc/API_Additional_Implementation_Requirements.md create mode 100644 doc/API_Integration_Plan.md delete mode 100644 doc/development_log.md delete mode 100644 doc/refac.md create mode 100644 lib/core/config/environment.dart create mode 100644 lib/core/constants/api_endpoints.dart create mode 100644 lib/core/constants/app_constants.dart create mode 100644 lib/core/errors/exceptions.dart create mode 100644 lib/core/errors/failures.dart create mode 100644 lib/core/utils/formatters.dart create mode 100644 lib/core/utils/validators.dart create mode 100644 lib/data/datasources/remote/api_client.dart create mode 100644 lib/data/datasources/remote/interceptors/auth_interceptor.dart create mode 100644 lib/data/datasources/remote/interceptors/error_interceptor.dart create mode 100644 lib/data/datasources/remote/interceptors/logging_interceptor.dart create mode 100644 lib/di/injection_container.dart diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b4b0d11 --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 79c113f..8ed78d2 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,8 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Environment variables +.env +.env.* +!.env.example diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f877667 --- /dev/null +++ b/CLAUDE.md @@ -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 도구 속성 표시 금지 \ No newline at end of file diff --git a/doc/API_Additional_Implementation_Requirements.md b/doc/API_Additional_Implementation_Requirements.md new file mode 100644 index 0000000..2836bf1 --- /dev/null +++ b/doc/API_Additional_Implementation_Requirements.md @@ -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 타입 없음 + claims: web::ReqData, // ❌ TokenClaims 타입 없음 + // ... +) + +// 수정 필요 +pub async fn handle_equipment_in( + db: web::Data, // ✅ + claims: web::ReqData, // ✅ + // ... +) +``` + +### 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 호출로 전환할 수 있습니다. \ No newline at end of file diff --git a/doc/API_Integration_Plan.md b/doc/API_Integration_Plan.md new file mode 100644 index 0000000..46e3394 --- /dev/null +++ b/doc/API_Integration_Plan.md @@ -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 _equipments = []; + final List _companies = []; + + // 동기적 메서드 + List 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 get equipments => MockDataService().getEquipments(); +} + +// 개선 후 +class EquipmentController extends ChangeNotifier { + List _equipments = []; + bool _isLoading = false; + String? _error; + + Future 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 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? 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_ (네트워크 레이어 및 기본 인프라 구현 완료) \ No newline at end of file diff --git a/doc/development_log.md b/doc/development_log.md deleted file mode 100644 index 5b2f32b..0000000 --- a/doc/development_log.md +++ /dev/null @@ -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으로 대체 - - 사용자 경험 향상: - - 모든 화면에서 일관된 내비게이션 경험 제공 - - 새로고침 및 추가 기능 버튼 통일 - - 플로팅 액션 버튼을 통한 추가 기능 접근성 개선 \ No newline at end of file diff --git a/doc/refac.md b/doc/refac.md deleted file mode 100644 index e69de29..0000000 diff --git a/lib/core/config/environment.dart b/lib/core/config/environment.dart new file mode 100644 index 0000000..9d08ba4 --- /dev/null +++ b/lib/core/config/environment.dart @@ -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 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); + } +} \ No newline at end of file diff --git a/lib/core/constants/api_endpoints.dart b/lib/core/constants/api_endpoints.dart new file mode 100644 index 0000000..210a5fe --- /dev/null +++ b/lib/core/constants/api_endpoints.dart @@ -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'; +} \ No newline at end of file diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 0000000..bcef6db --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -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 flutterToBackendRole = { + 'S': 'admin', // Super user + 'M': 'manager', // Manager + 'U': 'staff', // User + 'V': 'viewer', // Viewer + }; + + static const Map backendToFlutterRole = { + 'admin': 'S', + 'manager': 'M', + 'staff': 'U', + 'viewer': 'V', + }; + + // 장비 상태 + static const Map equipmentStatus = { + 'available': '사용가능', + 'in_use': '사용중', + 'maintenance': '유지보수', + 'disposed': '폐기', + 'rented': '대여중', + }; + + // 정렬 옵션 + static const Map 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 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}$', + ); +} \ No newline at end of file diff --git a/lib/core/errors/exceptions.dart b/lib/core/errors/exceptions.dart new file mode 100644 index 0000000..a66ce21 --- /dev/null +++ b/lib/core/errors/exceptions.dart @@ -0,0 +1,117 @@ +/// 커스텀 예외 클래스들 정의 + +/// 서버 예외 +class ServerException implements Exception { + final String message; + final int? statusCode; + final Map? 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>? 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)'; +} \ No newline at end of file diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart new file mode 100644 index 0000000..83d98ab --- /dev/null +++ b/lib/core/errors/failures.dart @@ -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? 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>? 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 = Future>; +typedef FutureVoid = FutureEither; \ No newline at end of file diff --git a/lib/core/utils/formatters.dart b/lib/core/utils/formatters.dart new file mode 100644 index 0000000..2a02ab5 --- /dev/null +++ b/lib/core/utils/formatters.dart @@ -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!; + } +} \ No newline at end of file diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart new file mode 100644 index 0000000..88c458b --- /dev/null +++ b/lib/core/utils/validators.dart @@ -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; + } +} \ No newline at end of file diff --git a/lib/data/datasources/remote/api_client.dart b/lib/data/datasources/remote/api_client.dart new file mode 100644 index 0000000..ea69721 --- /dev/null +++ b/lib/data/datasources/remote/api_client.dart @@ -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> get( + String path, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + }) { + return _dio.get( + path, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ); + } + + /// POST 요청 + Future> post( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) { + return _dio.post( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + /// PUT 요청 + Future> put( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) { + return _dio.put( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + /// PATCH 요청 + Future> patch( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) { + return _dio.patch( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + /// DELETE 요청 + Future> delete( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) { + return _dio.delete( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } + + /// 파일 업로드 + Future> uploadFile( + String path, { + required String filePath, + required String fileFieldName, + Map? 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( + path, + data: formData, + options: Options( + headers: { + 'Content-Type': 'multipart/form-data', + }, + ), + onSendProgress: onSendProgress, + cancelToken: cancelToken, + ); + } + + /// 파일 다운로드 + Future downloadFile( + String path, { + required String savePath, + ProgressCallback? onReceiveProgress, + CancelToken? cancelToken, + Map? queryParameters, + }) { + return _dio.download( + path, + savePath, + queryParameters: queryParameters, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + options: Options( + responseType: ResponseType.bytes, + followRedirects: false, + ), + ); + } +} \ No newline at end of file diff --git a/lib/data/datasources/remote/interceptors/auth_interceptor.dart b/lib/data/datasources/remote/interceptors/auth_interceptor.dart new file mode 100644 index 0000000..24f7256 --- /dev/null +++ b/lib/data/datasources/remote/interceptors/auth_interceptor.dart @@ -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 _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 _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); + } +} \ No newline at end of file diff --git a/lib/data/datasources/remote/interceptors/error_interceptor.dart b/lib/data/datasources/remote/interceptors/error_interceptor.dart new file mode 100644 index 0000000..e75f05e --- /dev/null +++ b/lib/data/datasources/remote/interceptors/error_interceptor.dart @@ -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? errors; + + // API 응답에서 에러 메시지 추출 + if (data != null) { + if (data is Map) { + message = data['message'] ?? data['error'] ?? message; + errors = data['errors'] as Map?; + } 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>? _parseFieldErrors(Map? errors) { + if (errors == null) return null; + + final fieldErrors = >{}; + + 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; + } +} \ No newline at end of file diff --git a/lib/data/datasources/remote/interceptors/logging_interceptor.dart b/lib/data/datasources/remote/interceptors/logging_interceptor.dart new file mode 100644 index 0000000..9979fd2 --- /dev/null +++ b/lib/data/datasources/remote/interceptors/logging_interceptor.dart @@ -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(); + } +} \ No newline at end of file diff --git a/lib/di/injection_container.dart b/lib/di/injection_container.dart new file mode 100644 index 0000000..c571082 --- /dev/null +++ b/lib/di/injection_container.dart @@ -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 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 resetDependencies() async { + await getIt.reset(); +} + +/// 특정 타입의 의존성 가져오기 +T inject() => getIt.get(); + +/// 특정 타입의 의존성이 등록되어 있는지 확인 +bool isRegistered() => getIt.isRegistered(); + +/// 의존성 등록 헬퍼 함수들 +extension GetItHelpers on GetIt { + /// 싱글톤 등록 헬퍼 + void registerSingletonIfNotRegistered( + T Function() factory, + ) { + if (!isRegistered()) { + registerLazySingleton(factory); + } + } + + /// 팩토리 등록 헬퍼 + void registerFactoryIfNotRegistered( + T Function() factory, + ) { + if (!isRegistered()) { + registerFactory(factory); + } + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index a7d6963..7250d52 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,8 +11,15 @@ import 'package:superport/screens/warehouse_location/warehouse_location_form.dar import 'package:superport/utils/constants.dart'; import 'package:flutter_localizations/flutter_localizations.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는 싱글톤으로 자동 초기화됨 runApp(const SuperportApp()); } diff --git a/lib/screens/common/theme_shadcn.dart b/lib/screens/common/theme_shadcn.dart index 162f100..d86acfa 100644 --- a/lib/screens/common/theme_shadcn.dart +++ b/lib/screens/common/theme_shadcn.dart @@ -199,7 +199,7 @@ class ShadcnTheme { titleTextStyle: headingH4, iconTheme: const IconThemeData(color: foreground), ), - cardTheme: CardTheme( + cardTheme: CardThemeData( color: card, elevation: 0, shape: RoundedRectangleBorder( diff --git a/lib/screens/common/theme_tailwind.dart b/lib/screens/common/theme_tailwind.dart index 6beef1c..ee98ac0 100644 --- a/lib/screens/common/theme_tailwind.dart +++ b/lib/screens/common/theme_tailwind.dart @@ -56,7 +56,7 @@ class AppThemeTailwind { ), // 카드 테마 - cardTheme: CardTheme( + cardTheme: CardThemeData( color: Colors.white, elevation: 1, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index ce0e550..47a9c3d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include 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 = fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); printing_plugin_register_with_registrar(printing_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 0c2c3c3..3518f0c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux printing ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 313be45..865a9a7 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_macos import path_provider_foundation import printing func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 1b8eeb0..20caff3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,27 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile 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: dependency: transitive description: @@ -21,10 +42,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" barcode: dependency: transitive description: @@ -49,6 +70,70 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -57,6 +142,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -65,6 +158,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -73,6 +174,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" crypto: dependency: transitive description: @@ -81,14 +190,46 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -97,11 +238,35 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: flutter source: sdk 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: dependency: "direct dev" description: @@ -115,6 +280,54 @@ packages: description: flutter source: sdk 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: dependency: "direct main" description: @@ -133,6 +346,30 @@ packages: description: flutter source: sdk 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: dependency: "direct main" description: @@ -141,6 +378,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" http: dependency: transitive description: @@ -149,6 +394,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -165,22 +418,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" - intl: - dependency: transitive + injectable: + dependency: "direct main" description: - name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + name: injectable + sha256: "5e1556ea1d374fe44cbe846414d9bab346285d3d8a1da5877c01ad0774006068" url: "https://pub.dev" 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: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -205,6 +506,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -229,6 +546,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: @@ -237,6 +562,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -341,6 +674,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" posix: dependency: transitive description: @@ -357,6 +698,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.14.2" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" provider: dependency: "direct main" description: @@ -365,6 +714,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -373,11 +738,67 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: flutter source: sdk 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: dependency: transitive description: @@ -402,6 +823,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -426,6 +855,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -470,10 +915,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" 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: dependency: "direct main" description: @@ -490,6 +943,30 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -506,6 +983,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: - dart: ">=3.7.2 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index cc1e5c5..9080a73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,11 +17,40 @@ dependencies: wave: ^0.2.2 flutter_svg: ^2.0.10 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: flutter_test: sdk: flutter 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: uses-material-design: true diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3dea03b..286f7bc 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); PrintingPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PrintingPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e685eaf..8b889b9 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows printing )