/** * 포터블(로컬 단독 실행) 경량 서버 — Node 만으로 실행되는 단일 번들용 엔트리. * * 용도: 드론 도로영상 플레이어를 웹서버 설치 없이 아무 PC 에서 더블클릭으로 실행. * - 정적 클라이언트(client/) 서빙 + SPA 폴백 * - /api/elevation (DEM 프록시), /api/tile (지도 타일 프록시) — 클라이언트 필수 API * - 영상은 브라우저 폴더 선택(로컬 파일)로 재생하므로 업로드/HLS/DB(네이티브 모듈) 불필요 * * 빌드: scripts/build-portable.sh → dist-portable/GhiVideo-Portable/ * (esbuild 단일 번들 server.cjs + client/ + node.exe + GhiVideo.bat) */ import express from 'express'; import cors from 'cors'; import path from 'path'; import elevationRouter from './routes/elevation'; import tileRouter from './routes/tile'; const app = express(); app.use(cors({ origin: true })); app.use(express.json()); app.use('/api/elevation', elevationRouter); app.use('/api/tile', tileRouter); app.get('/api/health', (_req, res) => { res.json({ status: 'ok', mode: 'portable', timestamp: new Date().toISOString() }); }); // 정적 클라이언트 — 번들(server.cjs) 옆의 client/ 폴더. const clientDistPath = path.join(__dirname, 'client'); const cspHeader = "default-src 'self'; " + "script-src 'self'; " + "style-src 'self' 'unsafe-inline'; " + "font-src 'self' data:; " + "img-src 'self' blob: data:; " + "media-src 'self' blob:; " + "connect-src 'self'; " + "worker-src 'self' blob:;"; app.use((req, res, next) => { if (!req.path.startsWith('/api')) res.setHeader('Content-Security-Policy', cspHeader); next(); }); app.use(express.static(clientDistPath)); app.get('*', (req, res, next) => { if (req.path.startsWith('/api')) return next(); res.sendFile(path.join(clientDistPath, 'index.html')); }); const port = Number(process.env.PORT || 54000); app.listen(port, () => { console.log(`GhiVideo 포터블 서버 실행 중 — http://localhost:${port}`); console.log('이 창을 닫으면 서버가 종료됩니다.'); });