wing-ops/prediction/image/image_plan.md
jeonghyo.k 3946ff6a25 feat(prediction): 이미지 분석 서버 Docker 패키징 + DB 코드 제거
- 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 타입 오류 수정
2026-03-10 18:37:36 +09:00

7.8 KiB

이미지 업로드 유류 분석 기능 구현 계획

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 테이블에 이미지 분석 결과 컬럼 추가.

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 핵심 로직

// 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

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

// 기존 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

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: '',
    // ... 나머지 기본값
  })
}, [])

LeftPanelonImageAnalysisResult={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. 이미지 분석 서버 직접 테스트

    curl -X POST http://localhost:5001/run-script/ \
      -F "camTy=mx15hdi" -F "fileId=test001" -F "image=@drone_image.jpg"
    
  2. 백엔드 엔드포인트 테스트

    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 정보가 없는 이미지입니다" 메시지