제주 드론 도로영상 지원: DJI 로그 어댑터·분할영상 연속재생·카메라 자동감지·오버레이 개선
- 제주 DJI 시간기반 비행로그(CSV) 파싱 어댑터: 세그먼트 창 정렬, 가상 fps 환산 - 분할 영상(이름 오름차순) 연속재생 + 좌상단 영상목록 콤보(선택 재생), 세그먼트별 드론 로그 재정렬 - KML 다중 파일 병합 파싱: STA 체이니지 측점(71개) + 지장물 신포맷(타입/이름/텍스트박스_색상) - 시설물 라벨: 지정색 배경 박스 + 흰 글자, 팝업 중앙 정렬, 호버 구간 깜빡임 수정 - 카메라 자동감지(djmd: ZenmuseP1, focal 29.9mm) + <영상명>.camera.json PC 저장/폴더 자동 적용 - Yaw 자동추정(GPS 진행방위 기반) + 라벨 드래그 Yaw 역산 모드 - 스테이션바: GPS 이동거리 진행도(호버 시 정지), 노선밖 거리 표기, 원거리 구조물 마크 제외 - 서버: /api/camera 라우트, ecosystem 포트 54000·제주 데이터 경로 - docs/history: 작업 이력 21건 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import annotationsRouter from './routes/annotations';
|
||||
import geoRouter from './routes/geo';
|
||||
import elevationRouter from './routes/elevation';
|
||||
import tileRouter from './routes/tile';
|
||||
import cameraRouter from './routes/camera';
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -37,6 +38,7 @@ app.use('/api/annotations/:videoId', annotationsRouter);
|
||||
app.use('/api/geo', geoRouter);
|
||||
app.use('/api/elevation', elevationRouter);
|
||||
app.use('/api/tile', tileRouter);
|
||||
app.use('/api/camera', cameraRouter);
|
||||
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 카메라 파라미터 파일 저장/조회
|
||||
*
|
||||
* 클라이언트(카메라 파라미터 패널)의 '서버에 저장' 버튼이 현재 파라미터를
|
||||
* 영상과 같은 폴더(VIDEOS_DIR)에 `<영상 base>.camera.json` 으로 저장한다.
|
||||
* 이후 사용자가 폴더를 다시 선택하면(영상과 함께 이 json 이 딸려 오면)
|
||||
* 클라이언트 로더가 이 파일의 카메라 값을 최우선으로 적용한다.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { config } from '../config';
|
||||
|
||||
const router = Router();
|
||||
|
||||
function cameraFilePath(videoId: string): string | null {
|
||||
const base = videoId.replace(/\.[^.]+$/, '');
|
||||
if (!base) return null;
|
||||
const filePath = path.resolve(config.videosDir, `${base}.camera.json`);
|
||||
// Path traversal 방어 — VIDEOS_DIR 내부만 허용
|
||||
if (!filePath.startsWith(path.resolve(config.videosDir))) return null;
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// PUT /api/camera/:videoId — 카메라 파라미터 저장 (body = { camera: {...}, model?, savedAt? })
|
||||
router.put('/:videoId', (req: Request, res: Response) => {
|
||||
const filePath = cameraFilePath(req.params.videoId);
|
||||
if (!filePath) {
|
||||
res.status(400).json({ error: 'Invalid video ID' });
|
||||
return;
|
||||
}
|
||||
const body = req.body;
|
||||
if (!body || typeof body !== 'object' || typeof body.camera !== 'object') {
|
||||
res.status(400).json({ error: 'body.camera 객체가 필요합니다' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(body, null, 2), 'utf-8');
|
||||
res.json({ ok: true, file: path.basename(filePath) });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/camera/:videoId — 저장된 카메라 파라미터 조회 (없으면 404)
|
||||
router.get('/:videoId', (req: Request, res: Response) => {
|
||||
const filePath = cameraFilePath(req.params.videoId);
|
||||
if (!filePath) {
|
||||
res.status(400).json({ error: 'Invalid video ID' });
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
res.status(404).json({ error: 'camera.json 없음' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
res.json(JSON.parse(fs.readFileSync(filePath, 'utf-8')));
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user