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/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') 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 })) }) ) 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; }>; }