release: 2026-03-30 (202건 커밋) #141

병합
jhkang develop 에서 main 로 5 commits 를 머지했습니다 2026-03-30 15:12:52 +09:00
6개의 변경된 파일72개의 추가작업 그리고 29개의 파일을 삭제

파일 보기

@ -83,6 +83,5 @@
] ]
} }
] ]
}, }
"allow": [] }
}

파일 보기

@ -1,7 +1,7 @@
{ {
"applied_global_version": "1.6.1", "applied_global_version": "1.6.1",
"applied_date": "2026-03-26", "applied_date": "2026-03-30",
"project_type": "react-ts", "project_type": "react-ts",
"gitea_url": "https://gitea.gc-si.dev", "gitea_url": "https://gitea.gc-si.dev",
"custom_pre_commit": true "custom_pre_commit": true
} }

파일 보기

@ -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 { testWingDbConnection } from './db/wingDb.js'
import layersRouter from './routes/layers.js' import layersRouter from './routes/layers.js'
import simulationRouter from './routes/simulation.js' import simulationRouter from './routes/simulation.js'
import tilesRouter from './routes/tiles.js'
import authRouter from './auth/authRouter.js' import authRouter from './auth/authRouter.js'
import userRouter from './users/userRouter.js' import userRouter from './users/userRouter.js'
import roleRouter from './roles/roleRouter.js' import roleRouter from './roles/roleRouter.js'
@ -105,7 +106,8 @@ const generalLimiter = rateLimit({
legacyHeaders: false, legacyHeaders: false,
skip: (req) => { skip: (req) => {
// HLS 스트리밍 프록시는 빈번한 세그먼트 요청이 발생하므로 제외 // 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: { message: {
error: '요청 횟수 초과', error: '요청 횟수 초과',
@ -172,6 +174,7 @@ app.use('/api/aerial', aerialRouter)
app.use('/api/rescue', rescueRouter) app.use('/api/rescue', rescueRouter)
app.use('/api/map-base', mapBaseRouter) app.use('/api/map-base', mapBaseRouter)
app.use('/api/monitor', monitorRouter) app.use('/api/monitor', monitorRouter)
app.use('/api/tiles', tilesRouter)
// 헬스 체크 // 헬스 체크
app.get('/health', (_req, res) => { app.get('/health', (_req, res) => {

파일 보기

@ -4,18 +4,10 @@
## [Unreleased] ## [Unreleased]
## [2026-03-27.3] ## [2026-03-30]
### 추가 ### 추가
- 역추적: 리플레이에 Python 역방향 시뮬레이션 파티클 표시 (보라색 ScatterplotLayer) - 지도: VWorld 위성타일 백엔드 프록시 추가 — API 키를 서버에서 관리하고 CORS 우회
- 역추적: 전체 파티클 이동 경로 외각 폴리곤(컨벡스 헐) 표시
- 역추적: 리플레이 바 — 재생 완료 후 재시작 기능 (↺ 아이콘)
- 역추적: 리플레이 바 — 드래그 시크 기능 추가
## [2026-03-27.2]
### 수정
- 역추적: 선박 항적 API URL을 프로덕션 URL로 변경 및 엔드포인트 경로 추가 (/api/v2/tracks/area-search)
## [2026-03-27] ## [2026-03-27]
@ -25,6 +17,13 @@
- 역추적: 상위 5척 선박 경로 및 충돌 이벤트 리플레이 데이터 생성 - 역추적: 상위 5척 선박 경로 및 충돌 이벤트 리플레이 데이터 생성
- 역추적: 리플레이 바에 실제 분석 시간 범위 동적 표시 - 역추적: 리플레이 바에 실제 분석 시간 범위 동적 표시
- DB: AIS_TRACK 테이블 추가 (선박 항적 이력, GIS 공간 인덱스) - DB: AIS_TRACK 테이블 추가 (선박 항적 이력, GIS 공간 인덱스)
- 역추적: 리플레이에 Python 역방향 시뮬레이션 파티클 표시 (보라색 ScatterplotLayer)
- 역추적: 전체 파티클 이동 경로 외각 폴리곤(컨벡스 헐) 표시
- 역추적: 리플레이 바 — 재생 완료 후 재시작 기능 (↺ 아이콘)
- 역추적: 리플레이 바 — 드래그 시크 기능 추가
### 수정
- 역추적: 선박 항적 API URL을 프로덕션 URL로 변경 및 엔드포인트 경로 추가 (/api/v2/tracks/area-search)
### 변경 ### 변경
- 역추적: 생성 API 응답을 BacktrackResult로 통합 (재조회 불필요) - 역추적: 생성 API 응답을 BacktrackResult로 통합 (재조회 불필요)
@ -38,26 +37,20 @@
- 보고서: HWPX 이미지 내보내기 구조를 HWPX 스펙(hc:img + manifest 방식)으로 수정 - 보고서: HWPX 이미지 내보내기 구조를 HWPX 스펙(hc:img + manifest 방식)으로 수정
- 확산예측: 분석 목록 정렬 기준 변경 (RUN_DTM DESC 우선) - 확산예측: 분석 목록 정렬 기준 변경 (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] ## [2026-03-25]
### 추가 ### 추가
- 예측: 실행 이력 선택 기능 (predRunSn 기반 특정 예측 결과 조회) - 예측: 실행 이력 선택 기능 (predRunSn 기반 특정 예측 결과 조회)
- DB: PRED_RUN_SN 마이그레이션 추가 (028_pred_run_sn) - DB: PRED_RUN_SN 마이그레이션 추가 (028_pred_run_sn)
- 관리자: 수치예측자료 연계 모니터링 패널 추가 (HYCOM·GFS·WW3·KOAST POS_WIND/HYDR/WAVE) - 관리자: 수치예측자료 연계 모니터링 패널 추가 (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 등) - 보고서: 기능 개선 (TemplateEditPage, ReportGenerator, hwpxExport 등)
- 사고: 지도에서 사고 선택 시 FlyTo 애니메이션 적용
- 사고: 선택된 항목 재클릭 시 선택 해제 지원
## [2026-03-24] ## [2026-03-24]

파일 보기

@ -20,7 +20,6 @@ import { hexToRgba } from './mapUtils'
import { useMapStore } from '@common/store/mapStore' import { useMapStore } from '@common/store/mapStore'
const GEOSERVER_URL = import.meta.env.VITE_GEOSERVER_URL || 'http://localhost:8080' 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] const DEFAULT_CENTER: [number, number] = [37.39, 126.64]
@ -220,7 +219,7 @@ const SATELLITE_3D_STYLE: StyleSpecification = {
sources: { sources: {
'vworld-satellite': { 'vworld-satellite': {
type: 'raster', 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, tileSize: 256,
attribution: '&copy; <a href="https://www.vworld.kr">국토지리정보원 VWorld</a>', attribution: '&copy; <a href="https://www.vworld.kr">국토지리정보원 VWorld</a>',
}, },