From e2254cc9608374d3f8d7fcdcd1c0e924419804f7 Mon Sep 17 00:00:00 2001 From: "jeonghyo.k" Date: Mon, 30 Mar 2026 14:46:37 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(tiles):=20VWorld=20=EC=9C=84=EC=84=B1?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EB=B0=B1=EC=97=94=EB=93=9C=20=ED=94=84?= =?UTF-8?q?=EB=A1=9D=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VITE_VWORLD_API_KEY를 프론트엔드에서 직접 사용하던 방식에서 백엔드 프록시(/api/tiles/vworld)를 통해 API 키를 서버에서 관리하도록 변경. CORS 우회 + API 키 보호 효과. --- .claude/settings.json | 5 +- .claude/workflow-version.json | 4 +- backend/src/routes/tiles.ts | 49 +++++++++++++++++++ backend/src/server.ts | 5 +- .../src/common/components/map/MapView.tsx | 3 +- 5 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 backend/src/routes/tiles.ts diff --git a/.claude/settings.json b/.claude/settings.json index 43453f7..868df2d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -83,6 +83,5 @@ ] } ] - }, - "allow": [] -} \ No newline at end of file + } +} diff --git a/.claude/workflow-version.json b/.claude/workflow-version.json index 003eaf0..7af3ce8 100644 --- a/.claude/workflow-version.json +++ b/.claude/workflow-version.json @@ -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 -} \ No newline at end of file +} diff --git a/backend/src/routes/tiles.ts b/backend/src/routes/tiles.ts new file mode 100644 index 0000000..3b46048 --- /dev/null +++ b/backend/src/routes/tiles.ts @@ -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; diff --git a/backend/src/server.ts b/backend/src/server.ts index cf7e6a1..a577c9d 100755 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -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) => { diff --git a/frontend/src/common/components/map/MapView.tsx b/frontend/src/common/components/map/MapView.tsx index a287784..bda2fb7 100755 --- a/frontend/src/common/components/map/MapView.tsx +++ b/frontend/src/common/components/map/MapView.tsx @@ -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: '© 국토지리정보원 VWorld', }, From f3cfc869211bfde179b4e3d8d2a7bcb59a196955 Mon Sep 17 00:00:00 2001 From: "jeonghyo.k" Date: Mon, 30 Mar 2026 14:49:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 82cc3bc..65e427f 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,9 @@ ## [Unreleased] +### 추가 +- 지도: VWorld 위성타일 백엔드 프록시 추가 — API 키를 서버에서 관리하고 CORS 우회 + ## [2026-03-27.3] ### 추가 From d10b27db874ab679f8ed0c750d8e646edf55b1ec Mon Sep 17 00:00:00 2001 From: "jeonghyo.k" Date: Mon, 30 Mar 2026 15:10:06 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=A0=95=EB=A6=AC=20(2026-03-30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 65e427f..2a62d7a 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,22 +4,11 @@ ## [Unreleased] +## [2026-03-30] + ### 추가 - 지도: VWorld 위성타일 백엔드 프록시 추가 — API 키를 서버에서 관리하고 CORS 우회 -## [2026-03-27.3] - -### 추가 -- 역추적: 리플레이에 Python 역방향 시뮬레이션 파티클 표시 (보라색 ScatterplotLayer) -- 역추적: 전체 파티클 이동 경로 외각 폴리곤(컨벡스 헐) 표시 -- 역추적: 리플레이 바 — 재생 완료 후 재시작 기능 (↺ 아이콘) -- 역추적: 리플레이 바 — 드래그 시크 기능 추가 - -## [2026-03-27.2] - -### 수정 -- 역추적: 선박 항적 API URL을 프로덕션 URL로 변경 및 엔드포인트 경로 추가 (/api/v2/tracks/area-search) - ## [2026-03-27] ### 추가 @@ -28,6 +17,13 @@ - 역추적: 상위 5척 선박 경로 및 충돌 이벤트 리플레이 데이터 생성 - 역추적: 리플레이 바에 실제 분석 시간 범위 동적 표시 - DB: AIS_TRACK 테이블 추가 (선박 항적 이력, GIS 공간 인덱스) +- 역추적: 리플레이에 Python 역방향 시뮬레이션 파티클 표시 (보라색 ScatterplotLayer) +- 역추적: 전체 파티클 이동 경로 외각 폴리곤(컨벡스 헐) 표시 +- 역추적: 리플레이 바 — 재생 완료 후 재시작 기능 (↺ 아이콘) +- 역추적: 리플레이 바 — 드래그 시크 기능 추가 + +### 수정 +- 역추적: 선박 항적 API URL을 프로덕션 URL로 변경 및 엔드포인트 경로 추가 (/api/v2/tracks/area-search) ### 변경 - 역추적: 생성 API 응답을 BacktrackResult로 통합 (재조회 불필요) @@ -41,26 +37,20 @@ - 보고서: HWPX 이미지 내보내기 구조를 HWPX 스펙(hc:img + manifest 방식)으로 수정 - 확산예측: 분석 목록 정렬 기준 변경 (RUN_DTM DESC 우선) -## [2026-03-25.2] - -### 추가 -- 사고: 분석 패널 실데이터 연동 (확산예측·민감자원 API 연동, 카테고리 색상·이모지 매핑) -- 자산: 인근 기관 조회 API 추가 (/assets/orgs/nearby, PostGIS ST_DWithin) -- DB: PRED_EXEC 테이블 EXEC_USER_ID 컬럼 추가 (029 마이그레이션) - -### 변경 -- 사고: 지도에서 사고 선택 시 FlyTo 애니메이션 적용 -- 사고: 선택된 항목 재클릭 시 선택 해제 지원 - ## [2026-03-25] ### 추가 - 예측: 실행 이력 선택 기능 (predRunSn 기반 특정 예측 결과 조회) - DB: PRED_RUN_SN 마이그레이션 추가 (028_pred_run_sn) - 관리자: 수치예측자료 연계 모니터링 패널 추가 (HYCOM·GFS·WW3·KOAST POS_WIND/HYDR/WAVE) +- 사고: 분석 패널 실데이터 연동 (확산예측·민감자원 API 연동, 카테고리 색상·이모지 매핑) +- 자산: 인근 기관 조회 API 추가 (/assets/orgs/nearby, PostGIS ST_DWithin) +- DB: PRED_EXEC 테이블 EXEC_USER_ID 컬럼 추가 (029 마이그레이션) ### 변경 - 보고서: 기능 개선 (TemplateEditPage, ReportGenerator, hwpxExport 등) +- 사고: 지도에서 사고 선택 시 FlyTo 애니메이션 적용 +- 사고: 선택된 항목 재클릭 시 선택 해제 지원 ## [2026-03-24]