release: 2026-03-30 (202건 커밋) #141
@ -83,6 +83,5 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"allow": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"applied_global_version": "1.6.1",
|
||||
"applied_date": "2026-03-26",
|
||||
"applied_date": "2026-03-30",
|
||||
"project_type": "react-ts",
|
||||
"gitea_url": "https://gitea.gc-si.dev",
|
||||
"custom_pre_commit": true
|
||||
}
|
||||
}
|
||||
|
||||
49
backend/src/routes/tiles.ts
Normal file
49
backend/src/routes/tiles.ts
Normal file
@ -0,0 +1,49 @@
|
||||
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;
|
||||
@ -7,6 +7,7 @@ import cookieParser from 'cookie-parser'
|
||||
import { testWingDbConnection } from './db/wingDb.js'
|
||||
import layersRouter from './routes/layers.js'
|
||||
import simulationRouter from './routes/simulation.js'
|
||||
import tilesRouter from './routes/tiles.js'
|
||||
import authRouter from './auth/authRouter.js'
|
||||
import userRouter from './users/userRouter.js'
|
||||
import roleRouter from './roles/roleRouter.js'
|
||||
@ -105,7 +106,8 @@ const generalLimiter = rateLimit({
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
// HLS 스트리밍 프록시는 빈번한 세그먼트 요청이 발생하므로 제외
|
||||
return req.path.startsWith('/api/aerial/cctv/stream-proxy');
|
||||
return req.path.startsWith('/api/aerial/cctv/stream-proxy') ||
|
||||
req.path.startsWith('/api/tiles/');
|
||||
},
|
||||
message: {
|
||||
error: '요청 횟수 초과',
|
||||
@ -172,6 +174,7 @@ app.use('/api/aerial', aerialRouter)
|
||||
app.use('/api/rescue', rescueRouter)
|
||||
app.use('/api/map-base', mapBaseRouter)
|
||||
app.use('/api/monitor', monitorRouter)
|
||||
app.use('/api/tiles', tilesRouter)
|
||||
|
||||
// 헬스 체크
|
||||
app.get('/health', (_req, res) => {
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 추가
|
||||
- 지도: VWorld 위성타일 백엔드 프록시 추가 — API 키를 서버에서 관리하고 CORS 우회
|
||||
|
||||
## [2026-03-27.3]
|
||||
|
||||
### 추가
|
||||
|
||||
@ -20,7 +20,6 @@ import { hexToRgba } from './mapUtils'
|
||||
import { useMapStore } from '@common/store/mapStore'
|
||||
|
||||
const GEOSERVER_URL = import.meta.env.VITE_GEOSERVER_URL || 'http://localhost:8080'
|
||||
const VWORLD_API_KEY = import.meta.env.VITE_VWORLD_API_KEY || ''
|
||||
|
||||
// 인천 송도 국제도시
|
||||
const DEFAULT_CENTER: [number, number] = [37.39, 126.64]
|
||||
@ -220,7 +219,7 @@ const SATELLITE_3D_STYLE: StyleSpecification = {
|
||||
sources: {
|
||||
'vworld-satellite': {
|
||||
type: 'raster',
|
||||
tiles: [`https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Satellite/{z}/{y}/{x}.jpeg`],
|
||||
tiles: ['/api/tiles/vworld/{z}/{y}/{x}.jpeg'],
|
||||
tileSize: 256,
|
||||
attribution: '© <a href="https://www.vworld.kr">국토지리정보원 VWorld</a>',
|
||||
},
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user