wing-ops/backend/src/prediction/predictionRouter.ts
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

184 lines
6.8 KiB
TypeScript

import express from 'express';
import multer from 'multer';
import {
listAnalyses, getAnalysisDetail, getBacktrack, listBacktracksByAcdnt,
createBacktrack, saveBoomLine, listBoomLines, getAnalysisTrajectory,
} from './predictionService.js';
import { analyzeImageFile } from './imageAnalyzeService.js';
import { isValidNumber } from '../middleware/security.js';
import { requireAuth, requirePermission } from '../auth/authMiddleware.js';
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
const router = express.Router();
// GET /api/prediction/analyses — 분석 목록
router.get('/analyses', requireAuth, requirePermission('prediction', 'READ'), async (req, res) => {
try {
const { search } = req.query;
const items = await listAnalyses({ search: search as string | undefined });
res.json(items);
} catch (err) {
console.error('[prediction] 분석 목록 오류:', err);
res.status(500).json({ error: '분석 목록 조회 실패' });
}
});
// GET /api/prediction/analyses/:acdntSn — 분석 상세
router.get('/analyses/:acdntSn', requireAuth, requirePermission('prediction', 'READ'), async (req, res) => {
try {
const acdntSn = parseInt(req.params.acdntSn as string, 10);
if (!isValidNumber(acdntSn, 1, 999999)) {
res.status(400).json({ error: '유효하지 않은 사고 번호' });
return;
}
const detail = await getAnalysisDetail(acdntSn);
if (!detail) {
res.status(404).json({ error: '분석을 찾을 수 없습니다' });
return;
}
res.json(detail);
} catch (err) {
console.error('[prediction] 분석 상세 오류:', err);
res.status(500).json({ error: '분석 상세 조회 실패' });
}
});
// GET /api/prediction/analyses/:acdntSn/trajectory — 최신 OpenDrift 결과 조회
router.get('/analyses/:acdntSn/trajectory', requireAuth, requirePermission('prediction', 'READ'), async (req, res) => {
try {
const acdntSn = parseInt(req.params.acdntSn as string, 10);
if (!isValidNumber(acdntSn, 1, 999999)) {
res.status(400).json({ error: '유효하지 않은 사고 번호' });
return;
}
const result = await getAnalysisTrajectory(acdntSn);
if (!result) {
res.json({ trajectory: null, summary: null });
return;
}
res.json(result);
} catch (err) {
console.error('[prediction] trajectory 조회 오류:', err);
res.status(500).json({ error: 'trajectory 조회 실패' });
}
});
// GET /api/prediction/backtrack — 사고별 역추적 목록
router.get('/backtrack', requireAuth, requirePermission('prediction', 'READ'), async (req, res) => {
try {
const acdntSn = parseInt(req.query.acdntSn as string, 10);
if (!isValidNumber(acdntSn, 1, 999999)) {
res.status(400).json({ error: '유효하지 않은 사고 번호' });
return;
}
const items = await listBacktracksByAcdnt(acdntSn);
res.json(items);
} catch (err) {
console.error('[prediction] 역추적 목록 오류:', err);
res.status(500).json({ error: '역추적 목록 조회 실패' });
}
});
// GET /api/prediction/backtrack/:sn — 역추적 상세
router.get('/backtrack/:sn', requireAuth, requirePermission('prediction', 'READ'), async (req, res) => {
try {
const sn = parseInt(req.params.sn as string, 10);
if (!isValidNumber(sn, 1, 999999)) {
res.status(400).json({ error: '유효하지 않은 역추적 번호' });
return;
}
const item = await getBacktrack(sn);
if (!item) {
res.status(404).json({ error: '역추적 결과를 찾을 수 없습니다' });
return;
}
res.json(item);
} catch (err) {
console.error('[prediction] 역추적 상세 오류:', err);
res.status(500).json({ error: '역추적 조회 실패' });
}
});
// POST /api/prediction/backtrack — 역추적 생성
router.post('/backtrack', requireAuth, requirePermission('prediction', 'CREATE'), async (req, res) => {
try {
const { acdntSn, lat, lon, estSpilDtm, anlysRange, srchRadiusNm } = req.body;
if (!acdntSn || !lat || !lon) {
res.status(400).json({ error: '사고번호, 위도, 경도는 필수입니다' });
return;
}
const result = await createBacktrack({ acdntSn, lat, lon, estSpilDtm, anlysRange, srchRadiusNm });
res.status(201).json(result);
} catch (err) {
console.error('[prediction] 역추적 생성 오류:', err);
res.status(500).json({ error: '역추적 생성 실패' });
}
});
// GET /api/prediction/boom/:acdntSn — 오일펜스 목록
router.get('/boom/:acdntSn', requireAuth, requirePermission('prediction', 'READ'), async (req, res) => {
try {
const acdntSn = parseInt(req.params.acdntSn as string, 10);
if (!isValidNumber(acdntSn, 1, 999999)) {
res.status(400).json({ error: '유효하지 않은 사고 번호' });
return;
}
const items = await listBoomLines(acdntSn);
res.json(items);
} catch (err) {
console.error('[prediction] 오일펜스 목록 오류:', err);
res.status(500).json({ error: '오일펜스 목록 조회 실패' });
}
});
// POST /api/prediction/boom — 오일펜스 저장
router.post('/boom', requireAuth, requirePermission('prediction', 'CREATE'), async (req, res) => {
try {
const { acdntSn, boomNm, priorityOrd, geojson, lengthM, efficiencyPct } = req.body;
if (!acdntSn || !boomNm || !geojson) {
res.status(400).json({ error: '사고번호, 이름, GeoJSON은 필수입니다' });
return;
}
const result = await saveBoomLine({ acdntSn, boomNm, priorityOrd, geojson, lengthM, efficiencyPct });
res.status(201).json(result);
} catch (err) {
console.error('[prediction] 오일펜스 저장 오류:', err);
res.status(500).json({ error: '오일펜스 저장 실패' });
}
});
// POST /api/prediction/image-analyze — 이미지 업로드 분석
router.post(
'/image-analyze',
requireAuth,
requirePermission('prediction', 'CREATE'),
upload.single('image'),
async (req, res) => {
try {
if (!req.file) {
res.status(400).json({ error: '이미지 파일이 필요합니다' });
return;
}
const result = await analyzeImageFile(req.file.buffer, req.file.originalname);
res.json(result);
} catch (err: unknown) {
if (err instanceof Error) {
const code = (err as NodeJS.ErrnoException).code;
if (code === 'GPS_NOT_FOUND') {
res.status(422).json({ error: 'GPS_NOT_FOUND', message: 'GPS 정보가 없는 이미지입니다' });
return;
}
if (code === 'TIMEOUT') {
res.status(504).json({ error: 'TIMEOUT', message: '이미지 분석 서버 응답 시간 초과' });
return;
}
}
console.error('[prediction] 이미지 분석 오류:', err);
res.status(500).json({ error: '이미지 분석 실패' });
}
}
);
export default router;