GhiVideo 소스 복제 — v4 작업 시작 기준
기존 GhiVideo 저장소 HEAD의 트래킹 소스 362개 파일을 복제. (node_modules·storage·빌드 산출물·대용량 미디어는 .gitignore 규칙대로 제외) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* 지도 타일 프록시 — 브라우저 CSP(img-src 'self') / COEP(require-corp) 때문에
|
||||
* 클라이언트가 외부 타일 서버를 직접 <img> 로 못 부른다. 서버가 중계해 같은 출처로 응답.
|
||||
*
|
||||
* GET /api/tile/:source/:z/:x/:y (y 는 숫자 또는 "숫자.png")
|
||||
* - source=osm : OpenStreetMap 일반(도로) 지도 https://tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
* - source=sat : Esri World Imagery 위성 .../tile/{z}/{y}/{x} (row=y, col=x 순서 주의)
|
||||
*
|
||||
* 주의: 각 타일 서버 사용 정책 준수(식별 User-Agent, 저부하). Esri World Imagery 는 출처표기 필요.
|
||||
*/
|
||||
router.get('/:source/:z/:x/:y', async (req, res) => {
|
||||
const source = String(req.params.source);
|
||||
const z = Number(req.params.z);
|
||||
const x = Number(req.params.x); // col (tx)
|
||||
const y = Number(String(req.params.y).replace(/\.png$/i, '')); // row (ty)
|
||||
if (![z, x, y].every(Number.isInteger) || z < 0 || z > 19) {
|
||||
res.status(400).json({ error: 'invalid tile coords' });
|
||||
return;
|
||||
}
|
||||
const n = 2 ** z;
|
||||
if (x < 0 || x >= n || y < 0 || y >= n) {
|
||||
res.status(400).json({ error: 'tile out of range' });
|
||||
return;
|
||||
}
|
||||
const upstreamUrl =
|
||||
source === 'sat'
|
||||
? `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${z}/${y}/${x}` // z/row/col
|
||||
: `https://tile.openstreetmap.org/${z}/${x}/${y}.png`; // z/col/row
|
||||
try {
|
||||
const upstream = await fetch(upstreamUrl, {
|
||||
headers: { 'User-Agent': 'GhiVideo/1.0 (drone route inspection player)' },
|
||||
});
|
||||
if (!upstream.ok) {
|
||||
res.status(502).json({ error: 'upstream ' + upstream.status });
|
||||
return;
|
||||
}
|
||||
const buf = Buffer.from(await upstream.arrayBuffer());
|
||||
res.setHeader('Content-Type', upstream.headers.get('content-type') ?? 'image/png');
|
||||
res.setHeader('Cache-Control', 'public, max-age=604800, immutable'); // 7일 캐시
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin'); // COEP 만족
|
||||
res.send(buf);
|
||||
} catch (e) {
|
||||
res.status(502).json({ error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user