From bf16762dab50f12fd28dfebf18069a4c75430c93 Mon Sep 17 00:00:00 2001 From: htlee Date: Sun, 1 Mar 2026 01:30:10 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(prediction):=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20500=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ACDNT_WEATHER 테이블의 실제 컬럼명에 맞게 weather 쿼리 수정 (WEATHER_DTM→OBS_DTM, WIND_SPD→WIND 등 존재하지 않는 컬럼 참조 제거) Co-Authored-By: Claude Opus 4.6 --- backend/src/prediction/predictionService.ts | 50 +++++++++++-------- .../tabs/prediction/services/predictionApi.ts | 16 +++--- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/backend/src/prediction/predictionService.ts b/backend/src/prediction/predictionService.ts index b076730..e46afd3 100644 --- a/backend/src/prediction/predictionService.ts +++ b/backend/src/prediction/predictionService.ts @@ -54,13 +54,15 @@ interface PredictionDetail { insuranceData: unknown; }>; weather: Array<{ - weatherDtm: string; - windSpd: number | null; - windDir: string | null; - waveHgt: number | null; - currentSpd: number | null; - currentDir: string | null; - temp: number | null; + obsDtm: string; + locNm: string; + temp: string; + weatherDc: string; + wind: string; + wave: string; + humid: string; + vis: string; + sst: string; }>; } @@ -244,16 +246,18 @@ export async function getAnalysisDetail(acdntSn: number): Promise) => ({ - weatherDtm: String(w['weather_dtm'] ?? ''), - windSpd: w['wind_spd'] != null ? parseFloat(String(w['wind_spd'])) : null, - windDir: w['wind_dir'] != null ? String(w['wind_dir']) : null, - waveHgt: w['wave_hgt'] != null ? parseFloat(String(w['wave_hgt'])) : null, - currentSpd: w['current_spd'] != null ? parseFloat(String(w['current_spd'])) : null, - currentDir: w['current_dir'] != null ? String(w['current_dir']) : null, - temp: w['temp'] != null ? parseFloat(String(w['temp'])) : null, + obsDtm: w['obs_dtm'] ? String(w['obs_dtm']) : '', + locNm: String(w['loc_nm'] ?? ''), + temp: String(w['temp'] ?? ''), + weatherDc: String(w['weather_dc'] ?? ''), + wind: String(w['wind'] ?? ''), + wave: String(w['wave'] ?? ''), + humid: String(w['humid'] ?? ''), + vis: String(w['vis'] ?? ''), + sst: String(w['sst'] ?? ''), })); return { diff --git a/frontend/src/tabs/prediction/services/predictionApi.ts b/frontend/src/tabs/prediction/services/predictionApi.ts index 53b4651..fcd0d65 100644 --- a/frontend/src/tabs/prediction/services/predictionApi.ts +++ b/frontend/src/tabs/prediction/services/predictionApi.ts @@ -54,13 +54,15 @@ export interface PredictionDetail { insuranceData: unknown; }>; weather: Array<{ - weatherDtm: string; - windSpd: number | null; - windDir: string | null; - waveHgt: number | null; - currentSpd: number | null; - currentDir: string | null; - temp: number | null; + obsDtm: string; + locNm: string; + temp: string; + weatherDc: string; + wind: string; + wave: string; + humid: string; + vis: string; + sst: string; }>; } -- 2.45.2 From 08b8f16001c054ecaa4b140dc2ca691f4cbd18f4 Mon Sep 17 00:00:00 2001 From: htlee Date: Sun, 1 Mar 2026 01:31:58 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(prediction):=20=EC=8B=9C=EB=AE=AC?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20API=EB=A5=BC=20localhost=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20api=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetch('http://localhost:3001/...') → api.post('/simulation/run', ...) 배포 환경에서 CORS loopback 차단 문제 해결 Co-Authored-By: Claude Opus 4.6 --- .../prediction/components/OilSpillView.tsx | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/frontend/src/tabs/prediction/components/OilSpillView.tsx b/frontend/src/tabs/prediction/components/OilSpillView.tsx index db35a4f..fd84ca3 100755 --- a/frontend/src/tabs/prediction/components/OilSpillView.tsx +++ b/frontend/src/tabs/prediction/components/OilSpillView.tsx @@ -14,6 +14,7 @@ import type { BacktrackPhase, BacktrackVessel, BacktrackConditions, ReplayShip, import { TOTAL_REPLAY_FRAMES } from '@common/types/backtrack' import { fetchBacktrackByAcdnt, createBacktrack, fetchPredictionDetail } from '../services/predictionApi' import type { PredictionDetail } from '../services/predictionApi' +import { api } from '@common/services/api' export type PredictionModel = 'KOSPS' | 'POSEIDON' | 'OpenDrift' // eslint-disable-next-line react-refresh/only-export-components @@ -259,27 +260,16 @@ export function OilSpillView() { const models = Array.from(selectedModels) const results = await Promise.all( models.map(async (model) => { - const response = await fetch('http://localhost:3001/api/simulation/run', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model, - lat: incidentCoord.lat, - lon: incidentCoord.lon, - duration_hours: predictionTime, - oil_type: oilType, - spill_amount: spillAmount, - spill_type: spillType - }) + const { data } = await api.post<{ trajectory: Array<{ lat: number; lon: number; time: number; particle?: number }> }>('/simulation/run', { + model, + lat: incidentCoord.lat, + lon: incidentCoord.lon, + duration_hours: predictionTime, + oil_type: oilType, + spill_amount: spillAmount, + spill_type: spillType, }) - - if (!response.ok) { - throw new Error(`API 오류 (${model}): ${response.status}`) - } - - const data = await response.json() - return (data.trajectory as Array<{ lat: number; lon: number; time: number; particle?: number }>) - .map(p => ({ ...p, model })) + return data.trajectory.map(p => ({ ...p, model })) }) ) -- 2.45.2 From 5e4044d46199696b9173349d5b24c3bbaf2856ed Mon Sep 17 00:00:00 2001 From: htlee Date: Sun, 1 Mar 2026 01:36:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9?= =?UTF-8?q?=20URL=20=EC=A0=9C=EA=B1=B0=20+=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx: 중복 API_BASE_URL 정의 → @common/services/api import - MapView.tsx: GeoServer localhost:8080 → VITE_GEOSERVER_URL 환경변수 - ShipInsurance.tsx: 해운조합 API URL → VITE_HAEWOON_API_URL 환경변수 - server.ts CORS: 운영 도메인 → FRONTEND_URL 환경변수 통합 - server.ts CSP: localhost 허용을 개발 환경(NODE_ENV≠production)에만 적용 Co-Authored-By: Claude Opus 4.6 --- backend/src/server.ts | 19 +++++++++++++------ frontend/src/App.tsx | 4 ++-- .../src/common/components/map/MapView.tsx | 4 +++- .../tabs/assets/components/ShipInsurance.tsx | 4 +++- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/backend/src/server.ts b/backend/src/server.ts index 25e2865..6efd807 100755 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -49,7 +49,13 @@ app.use(helmet({ scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "blob:"], - connectSrc: ["'self'", "http://localhost:*", "https://*.gc-si.dev", "https://*.data.go.kr", "https://*.khoa.go.kr"], + connectSrc: [ + "'self'", + ...(process.env.NODE_ENV !== 'production' ? ['http://localhost:*'] : []), + 'https://*.gc-si.dev', + 'https://*.data.go.kr', + 'https://*.khoa.go.kr', + ], fontSrc: ["'self'"], objectSrc: ["'none'"], frameSrc: ["'none'"], @@ -65,11 +71,12 @@ app.disable('x-powered-by') // 3. CORS: 허용된 출처만 접근 가능 const allowedOrigins = [ - 'http://localhost:5173', // Vite dev server - 'http://localhost:5174', - 'http://localhost:3000', - 'https://wing-demo.gc-si.dev', - process.env.FRONTEND_URL, // 운영 환경 프론트엔드 URL (추가 도메인) + process.env.FRONTEND_URL || 'https://wing-demo.gc-si.dev', + ...(process.env.NODE_ENV !== 'production' ? [ + 'http://localhost:5173', + 'http://localhost:5174', + 'http://localhost:3000', + ] : []), ].filter(Boolean) as string[] app.use(cors({ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7059d68..66dce36 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import { LoginPage } from '@common/components/auth/LoginPage' import { registerMainTabSwitcher } from '@common/hooks/useSubMenu' import { useAuthStore } from '@common/store/authStore' import { useMenuStore } from '@common/store/menuStore' +import { API_BASE_URL } from '@common/services/api' import { OilSpillView } from '@tabs/prediction' import { ReportsView } from '@tabs/reports' import { HNSView } from '@tabs/hns' @@ -46,8 +47,7 @@ function App() { [JSON.stringify({ action: 'TAB_VIEW', detail: activeMainTab })], { type: 'text/plain' } ) - const apiBase = import.meta.env.VITE_API_URL || 'http://localhost:3001/api' - navigator.sendBeacon(`${apiBase}/audit/log`, blob) + navigator.sendBeacon(`${API_BASE_URL}/audit/log`, blob) }, [activeMainTab, isAuthenticated]) // 세션 확인 중 스플래시 diff --git a/frontend/src/common/components/map/MapView.tsx b/frontend/src/common/components/map/MapView.tsx index fce41a6..50afc57 100755 --- a/frontend/src/common/components/map/MapView.tsx +++ b/frontend/src/common/components/map/MapView.tsx @@ -9,6 +9,8 @@ import type { BoomLine, BoomLineCoord } from '@common/types/boomLine' import type { ReplayShip, CollisionEvent } from '@common/types/backtrack' import { BacktrackReplayOverlay } from './BacktrackReplayOverlay' +const GEOSERVER_URL = import.meta.env.VITE_GEOSERVER_URL || 'http://localhost:8080' + // Fix Leaflet default icon issue import icon from 'leaflet/dist/images/marker-icon.png' import iconShadow from 'leaflet/dist/images/marker-shadow.png' @@ -190,7 +192,7 @@ export function MapView({ {wmsLayers.map(layer => ( diff --git a/frontend/src/tabs/assets/components/ShipInsurance.tsx b/frontend/src/tabs/assets/components/ShipInsurance.tsx index 47dbeb6..862aaee 100644 --- a/frontend/src/tabs/assets/components/ShipInsurance.tsx +++ b/frontend/src/tabs/assets/components/ShipInsurance.tsx @@ -1,6 +1,8 @@ import { useState } from 'react' import type { InsuranceRow } from './assetTypes' +const DEFAULT_HAEWOON_API = import.meta.env.VITE_HAEWOON_API_URL || 'https://api.haewoon.or.kr/v1/insurance' + // 샘플 데이터 (외부 한국해운조합 API 연동 전 데모용) const INSURANCE_DEMO_DATA: InsuranceRow[] = [ { shipName: '유조선 한라호', mmsi: '440123456', imo: '9876001', insType: 'P&I 보험', insurer: '한국P&I클럽', policyNo: 'PI-2025-1234', start: '2025-07-01', expiry: '2026-06-30', limit: '50억' }, @@ -13,7 +15,7 @@ const INSURANCE_DEMO_DATA: InsuranceRow[] = [ function ShipInsurance() { const [apiConnected, setApiConnected] = useState(false) const [showConfig, setShowConfig] = useState(false) - const [configEndpoint, setConfigEndpoint] = useState('https://api.haewoon.or.kr/v1/insurance') + const [configEndpoint, setConfigEndpoint] = useState(DEFAULT_HAEWOON_API) const [configApiKey, setConfigApiKey] = useState('') const [configKeyType, setConfigKeyType] = useState('mmsi') const [configRespType, setConfigRespType] = useState('json') -- 2.45.2