- prediction/image/ FastAPI 서버 Docker 환경 구성 - Dockerfile: PyTorch 2.1 + CUDA 12.1 기반 GPU 이미지 - docker-compose.yml: GPU 할당 + 데이터 볼륨 마운트 - requirements.txt: 서버 의존성 목록 - .env.example: 환경변수 템플릿 - DOCKER_USAGE.md: 빌드/실행/API 사용법 문서 - Dockerfile에 .dockerignore 제외 폴더 mkdir -p 추가 - .gitignore: prediction/image 결과물 및 모델 가중치(.pth) 제외 추가 - dbInsert_csv.py, dbInsert_shp.py 삭제 (미사용 DB 로직) - api.py: dbInsert import 및 주석 처리된 DB 호출 코드 제거 - aerialRouter.ts: req.params 타입 오류 수정
239 lines
7.8 KiB
Markdown
239 lines
7.8 KiB
Markdown
# 이미지 업로드 유류 분석 기능 구현 계획
|
|
|
|
## Context
|
|
|
|
드론/항공 촬영 이미지를 업로드하면 AI 세그멘테이션으로 유류 확산 정보(위치·유종·면적·부피)를 자동 추출하고, 결과를 DB에 저장한 뒤 예측정보 입력 폼에 자동 채워주는 기능이다.
|
|
이미지 분석 서버(`prediction/image/api.py`, FastAPI, 포트 5001)는 이미 구현되어 있으며, 프론트↔백엔드↔이미지 분석 서버 연동 및 결과 자동 채우기를 구현한다.
|
|
|
|
---
|
|
|
|
## 전체 흐름
|
|
|
|
```
|
|
[프론트] 이미지 선택 → 분석 요청 버튼
|
|
↓ POST /api/prediction/image-analyze (multipart: image)
|
|
[백엔드]
|
|
├─ fileId = UUID 생성
|
|
├─ camTy = "mx15hdi" (하드코딩, 추후 이미지 EXIF 카메라 정보로 자동 판별 예정)
|
|
├─ 이미지 분석 서버로 전달 POST http://IMAGE_API_URL/run-script/
|
|
├─ 응답 파싱: meta(위경도 DMS→십진수 변환), data[0].classId→유종
|
|
├─ ACDNT INSERT (lat/lon/임시사고명)
|
|
├─ SPIL_DATA INSERT (유종/면적/img_rslt_data JSONB)
|
|
└─ 응답: { acdntSn, lat, lon, oilType, area, volume }
|
|
↓
|
|
[프론트] 폼 자동 채우기 (좌표·유종·유출량)
|
|
→ 사용자가 나머지 입력 후 "확산예측 실행"
|
|
```
|
|
|
|
---
|
|
|
|
## 구현 단계
|
|
|
|
### Step 1 — DB 마이그레이션 (`database/migration/017_spil_img_rslt.sql`)
|
|
|
|
`SPIL_DATA` 테이블에 이미지 분석 결과 컬럼 추가.
|
|
|
|
```sql
|
|
ALTER TABLE wing.spil_data
|
|
ADD COLUMN IF NOT EXISTS img_rslt_data JSONB;
|
|
```
|
|
|
|
---
|
|
|
|
### Step 2 — 백엔드: 이미지 분석 엔드포인트
|
|
|
|
**파일**: `backend/src/prediction/predictionRouter.ts` (라우트 등록)
|
|
**신규 파일**: `backend/src/prediction/imageAnalyzeService.ts`
|
|
|
|
#### 엔드포인트
|
|
|
|
```
|
|
POST /api/prediction/image-analyze
|
|
Content-Type: multipart/form-data
|
|
Body: image (file)
|
|
```
|
|
|
|
#### `imageAnalyzeService.ts` 핵심 로직
|
|
|
|
```typescript
|
|
// 1. fileId 생성 (crypto.randomUUID)
|
|
|
|
// 2. 이미지 분석 서버 호출
|
|
// camTy는 현재 "mx15hdi"로 하드코딩한다.
|
|
// TODO: 추후 이미지 EXIF에서 카메라 모델명을 읽어 camTy를 자동 판별하는 로직을
|
|
// 이미지 분석 서버(api.py)에 추가할 예정이다. (check_camera_info 함수 활용)
|
|
// FormData: { camTy: 'mx15hdi', fileId, image }
|
|
// → POST ${IMAGE_API_URL}/run-script/
|
|
// 응답: { meta: string, data: OilPolygon[] }
|
|
|
|
// 3. meta 문자열 파싱 (mx15hdi CSV 컬럼 순서 사용)
|
|
// [Filename, Tlat_d, Tlat_m, Tlat_s, Tlon_d, Tlon_m, Tlon_s, ...]
|
|
// DMS → 십진수: d + m/60 + s/3600
|
|
|
|
// 4. 유종 매핑 (data[0].classId → UI 유종명)
|
|
// classId → oilType: { '검정': '벙커C유', '갈색': '벙커C유', '무지개': '경유', '은색': '등유' }
|
|
|
|
// 5. ACDNT INSERT (임시 사고명 = "이미지분석_YYYY-MM-DD HH:mm", lat, lon, occurredAt = 촬영시각)
|
|
// 6. SPIL_DATA INSERT (acdntSn, matTyCd, matVol=data[0].volume, imgRsltData=JSON.stringify(response))
|
|
|
|
// 7. 반환
|
|
interface ImageAnalyzeResult {
|
|
acdntSn: number;
|
|
lat: number;
|
|
lon: number;
|
|
oilType: string; // UI 유종명 (벙커C유 등)
|
|
area: number; // m²
|
|
volume: number; // m³
|
|
fileId: string;
|
|
}
|
|
```
|
|
|
|
#### 환경변수 추가 (`backend/.env`)
|
|
|
|
```
|
|
IMAGE_API_URL=http://localhost:5001
|
|
```
|
|
|
|
#### 에러 처리
|
|
|
|
| 조건 | 응답 |
|
|
|------|------|
|
|
| 이미지에 GPS EXIF 없음 | 422 `{ error: 'GPS_NOT_FOUND' }` |
|
|
| 이미지 서버 타임아웃(300s) | 504 |
|
|
|
|
---
|
|
|
|
### Step 3 — 프론트엔드: API 서비스
|
|
|
|
**파일**: `frontend/src/tabs/prediction/services/predictionApi.ts`
|
|
|
|
```typescript
|
|
interface ImageAnalyzeResult {
|
|
acdntSn: number;
|
|
lat: number;
|
|
lon: number;
|
|
oilType: string;
|
|
area: number;
|
|
volume: number;
|
|
fileId: string;
|
|
}
|
|
|
|
export const analyzeImage = async (
|
|
file: File
|
|
): Promise<ImageAnalyzeResult> => {
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
const { data } = await api.post<ImageAnalyzeResult>(
|
|
'/prediction/image-analyze',
|
|
formData,
|
|
{ headers: { 'Content-Type': 'multipart/form-data' }, timeout: 330000 }
|
|
);
|
|
return data;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### Step 4 — 프론트엔드: Props 타입 확장
|
|
|
|
**파일**: `frontend/src/tabs/prediction/components/leftPanelTypes.ts`
|
|
|
|
```typescript
|
|
// 기존 Props에 추가
|
|
onImageAnalysisResult?: (result: ImageAnalyzeResult) => void;
|
|
```
|
|
|
|
---
|
|
|
|
### Step 5 — 프론트엔드: PredictionInputSection 수정
|
|
|
|
**파일**: `frontend/src/tabs/prediction/components/PredictionInputSection.tsx`
|
|
|
|
#### 변경 사항
|
|
|
|
1. **"이미지 분석 실행" 버튼** (이미지 선택 후 활성화)
|
|
- 클릭 시 `analyzeImage(file)` 호출 (camTy는 백엔드에서 처리)
|
|
- 로딩 스피너 표시 (분석 소요시간 수십 초~수 분)
|
|
|
|
2. **분석 결과 표시** (성공 시)
|
|
- "분석 완료: 위도 XX.XXXX / 경도 XXX.XXXX / 유종: OO" 요약 메시지
|
|
|
|
3. **`onImageAnalysisResult` 콜백 호출**
|
|
- 분석 성공 시 부모로 결과 전달
|
|
|
|
4. **에러 처리**
|
|
- GPS_NOT_FOUND: "GPS 정보가 없는 이미지입니다" 메시지 표시
|
|
- 타임아웃: "분석 서버 응답 없음" 메시지 표시
|
|
|
|
5. **로컬 상태 교체**: `uploadedImage` (Base64 DataURL) 제거, `uploadedFile: File | null`로 교체
|
|
|
|
---
|
|
|
|
### Step 6 — 프론트엔드: OilSpillView 결과 처리
|
|
|
|
**파일**: `frontend/src/tabs/prediction/components/OilSpillView.tsx`
|
|
|
|
```typescript
|
|
const handleImageAnalysisResult = useCallback((result: ImageAnalyzeResult) => {
|
|
// 1. 사고 좌표 자동 채우기
|
|
setIncidentCoord({ lat: result.lat, lon: result.lon })
|
|
setFlyToCoord({ lat: result.lat, lon: result.lon })
|
|
|
|
// 2. 유종/유출량 자동 채우기
|
|
setOilType(result.oilType)
|
|
setSpillAmount(parseFloat(result.volume.toFixed(4)))
|
|
setSpillUnit('m³')
|
|
|
|
// 3. 분석 선택 상태 갱신 (acdntSn 연결 — 시뮬레이션 실행 시 기존 사고 사용)
|
|
setSelectedAnalysis({
|
|
acdntSn: result.acdntSn,
|
|
acdntNm: '',
|
|
// ... 나머지 기본값
|
|
})
|
|
}, [])
|
|
```
|
|
|
|
`LeftPanel`에 `onImageAnalysisResult={handleImageAnalysisResult}` 전달.
|
|
|
|
---
|
|
|
|
## 수정 대상 파일 요약
|
|
|
|
| 파일 | 변경 유형 |
|
|
|------|---------|
|
|
| `database/migration/017_spil_img_rslt.sql` | **신규** — SPIL_DATA 컬럼 추가 |
|
|
| `backend/src/prediction/imageAnalyzeService.ts` | **신규** — 이미지 분석 서비스 |
|
|
| `backend/src/prediction/predictionRouter.ts` | **수정** — 라우트 추가 |
|
|
| `backend/.env` | **수정** — IMAGE_API_URL 추가 |
|
|
| `frontend/src/tabs/prediction/services/predictionApi.ts` | **수정** — analyzeImage 함수 추가 |
|
|
| `frontend/src/tabs/prediction/components/leftPanelTypes.ts` | **수정** — Props 타입 추가 |
|
|
| `frontend/src/tabs/prediction/components/PredictionInputSection.tsx` | **수정** — 분석 실행 UI |
|
|
| `frontend/src/tabs/prediction/components/OilSpillView.tsx` | **수정** — 결과 처리 핸들러 |
|
|
|
|
---
|
|
|
|
## 검증 방법
|
|
|
|
1. **이미지 분석 서버 직접 테스트**
|
|
```bash
|
|
curl -X POST http://localhost:5001/run-script/ \
|
|
-F "camTy=mx15hdi" -F "fileId=test001" -F "image=@drone_image.jpg"
|
|
```
|
|
|
|
2. **백엔드 엔드포인트 테스트**
|
|
```bash
|
|
curl -X POST http://localhost:3001/api/prediction/image-analyze \
|
|
-F "image=@drone_image.jpg" \
|
|
-H "Cookie: <auth_cookie>"
|
|
```
|
|
- 응답: `{ acdntSn, lat, lon, oilType, area, volume, fileId }`
|
|
- DB 확인: ACDNT, SPIL_DATA 레코드 생성 여부
|
|
|
|
3. **프론트엔드 E2E 테스트**
|
|
- 이미지 업로드 모드 선택 → GPS EXIF 있는 이미지 업로드 → "이미지 분석 실행" 클릭
|
|
- 로딩 표시 → 완료 시: 지도 이동, 유종/좌표 폼 자동 채워짐 확인
|
|
- 나머지 필드(예상시각·유출시간 등) 직접 입력 후 "확산예측 실행" → 정상 시뮬레이션 확인
|
|
|
|
4. **에러 케이스 확인**
|
|
- GPS 없는 이미지 → "GPS 정보가 없는 이미지입니다" 메시지
|