wing-ops/backend/src/routes/tiles.ts
jeonghyo.k 6620f00ee1 feat(tiles): SR 민감자원 벡터타일 프록시 엔드포인트 추가
Martin 서버의 SR 벡터타일, TileJSON, 스타일 JSON을
백엔드 프록시를 통해 제공하는 라우트 추가.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 22:29:21 +09:00

129 lines
4.8 KiB
TypeScript

import { Router } from 'express';
const router = Router();
const VWORLD_API_KEY = process.env.VWORLD_API_KEY || '';
const ENC_UPSTREAM = 'https://tiles.gcnautical.com';
// ─── 공통 프록시 헬퍼 ───
async function proxyUpstream(upstreamUrl: string, res: import('express').Response, fallbackContentType = 'application/octet-stream') {
try {
const upstream = await fetch(upstreamUrl, {
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; WING-OPS/1.0)' },
});
if (!upstream.ok) {
res.status(upstream.status).end();
return;
}
const contentType = upstream.headers.get('content-type') || fallbackContentType;
const cacheControl = upstream.headers.get('cache-control') || 'public, max-age=86400';
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', cacheControl);
const buffer = await upstream.arrayBuffer();
res.end(Buffer.from(buffer));
} catch {
res.status(502).json({ error: '타일 서버 연결 실패' });
}
}
// ─── VWorld 위성타일 ───
// GET /api/tiles/vworld/:z/:y/:x — VWorld WMTS 위성타일 프록시 (CORS 우회)
// VWorld는 브라우저 직접 요청에 CORS 헤더를 반환하지 않으므로 서버에서 중계
router.get('/vworld/:z/:y/:x', async (req, res) => {
const { z, y } = req.params;
const x = req.params.x.replace(/\.jpeg$/i, '');
// z/y/x 정수 검증 (SSRF 방지)
if (!/^\d+$/.test(z) || !/^\d+$/.test(y) || !/^\d+$/.test(x)) {
res.status(400).json({ error: '잘못된 타일 좌표' });
return;
}
if (!VWORLD_API_KEY) {
res.status(503).json({ error: 'VWorld API 키가 설정되지 않았습니다.' });
return;
}
const tileUrl = `https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Satellite/${z}/${y}/${x}.jpeg`;
await proxyUpstream(tileUrl, res, 'image/jpeg');
});
// ─── SR 민감자원 벡터타일 ───
// GET /api/tiles/sr/tilejson — SR TileJSON 프록시 (source-layer 메타데이터)
router.get('/sr/tilejson', async (_req, res) => {
await proxyUpstream(`${ENC_UPSTREAM}/sr`, res, 'application/json');
});
// GET /api/tiles/sr/style — SR 스타일 JSON 프록시 (레이어별 type/paint/layout 정의)
router.get('/sr/style', async (_req, res) => {
await proxyUpstream(`${ENC_UPSTREAM}/style/sr`, res, 'application/json');
});
// GET /api/tiles/sr/:z/:x/:y — SR(민감자원) 벡터타일 프록시
router.get('/sr/:z/:x/:y', async (req, res) => {
const { z, x, y } = req.params;
if (!/^\d+$/.test(z) || !/^\d+$/.test(x) || !/^\d+$/.test(y)) {
res.status(400).json({ error: '잘못된 타일 좌표' });
return;
}
await proxyUpstream(`${ENC_UPSTREAM}/sr/${z}/${x}/${y}`, res, 'application/x-protobuf');
});
// ─── S-57 전자해도 (ENC) ───
// tiles.gcnautical.com CORS 제한 우회를 위한 프록시 엔드포인트 그룹
// GET /api/tiles/enc/style — 공식 style.json 프록시
router.get('/enc/style', async (_req, res) => {
await proxyUpstream(`${ENC_UPSTREAM}/style/nautical`, res, 'application/json');
});
// GET /api/tiles/enc/sprite/:file — sprite JSON/PNG 프록시 (sprite.json, sprite.png, sprite@2x.json, sprite@2x.png)
router.get('/enc/sprite/:file', async (req, res) => {
const { file } = req.params;
if (!/^sprite(@2x)?\.(json|png)$/.test(file)) {
res.status(400).json({ error: '잘못된 sprite 파일명' });
return;
}
const fallbackCt = file.endsWith('.png') ? 'image/png' : 'application/json';
await proxyUpstream(`${ENC_UPSTREAM}/sprite/${file}`, res, fallbackCt);
});
// GET /api/tiles/enc/font/:fontstack/:range — glyphs(PBF) 프록시
router.get('/enc/font/:fontstack/:range', async (req, res) => {
const { fontstack, range } = req.params;
if (!/^[\w\s%-]+$/.test(fontstack) || !/^\d+-\d+$/.test(range)) {
res.status(400).json({ error: '잘못된 폰트 요청' });
return;
}
await proxyUpstream(`${ENC_UPSTREAM}/font/${fontstack}/${range}`, res, 'application/x-protobuf');
});
// GET /api/tiles/enc/globe/:z/:x/:y — globe 벡터타일 프록시 (저줌 레벨용)
router.get('/enc/globe/:z/:x/:y', async (req, res) => {
const { z, x, y } = req.params;
if (!/^\d+$/.test(z) || !/^\d+$/.test(x) || !/^\d+$/.test(y)) {
res.status(400).json({ error: '잘못된 타일 좌표' });
return;
}
await proxyUpstream(`${ENC_UPSTREAM}/globe/${z}/${x}/${y}`, res, 'application/x-protobuf');
});
// GET /api/tiles/enc/:z/:x/:y — ENC 벡터타일 프록시 (표준 XYZ 순서)
router.get('/enc/:z/:x/:y', async (req, res) => {
const { z, x, y } = req.params;
if (!/^\d+$/.test(z) || !/^\d+$/.test(x) || !/^\d+$/.test(y)) {
res.status(400).json({ error: '잘못된 타일 좌표' });
return;
}
await proxyUpstream(`${ENC_UPSTREAM}/enc/${z}/${x}/${y}`, res, 'application/x-protobuf');
});
export default router;