VITE_VWORLD_API_KEY를 프론트엔드에서 직접 사용하던 방식에서 백엔드 프록시(/api/tiles/vworld)를 통해 API 키를 서버에서 관리하도록 변경. CORS 우회 + API 키 보호 효과.
50 lines
1.5 KiB
TypeScript
50 lines
1.5 KiB
TypeScript
import { Router } from 'express';
|
|
|
|
const router = Router();
|
|
|
|
const VWORLD_API_KEY = process.env.VWORLD_API_KEY || '';
|
|
|
|
// 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`;
|
|
|
|
try {
|
|
const upstream = await fetch(tileUrl, {
|
|
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') || 'image/jpeg';
|
|
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 타일 서버 연결 실패' });
|
|
}
|
|
});
|
|
|
|
export default router;
|