docs: v1.0.0 버저닝 릴리즈 노트 작성 #65
@ -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<PredictionDeta
|
||||
|
||||
const weatherSql = `
|
||||
SELECT
|
||||
WEATHER_DTM,
|
||||
WIND_SPD,
|
||||
WIND_DIR,
|
||||
WAVE_HGT,
|
||||
CURRENT_SPD,
|
||||
CURRENT_DIR,
|
||||
TEMP
|
||||
OBS_DTM,
|
||||
LOC_NM,
|
||||
TEMP,
|
||||
WEATHER_DC,
|
||||
WIND,
|
||||
WAVE,
|
||||
HUMID,
|
||||
VIS,
|
||||
SST
|
||||
FROM ACDNT_WEATHER
|
||||
WHERE ACDNT_SN = $1
|
||||
ORDER BY WEATHER_DTM ASC
|
||||
ORDER BY OBS_DTM ASC
|
||||
`;
|
||||
const { rows: weatherRows } = await wingPool.query(weatherSql, [acdntSn]);
|
||||
|
||||
@ -288,13 +292,15 @@ export async function getAnalysisDetail(acdntSn: number): Promise<PredictionDeta
|
||||
}));
|
||||
|
||||
const weather = weatherRows.map((w: Record<string, unknown>) => ({
|
||||
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 {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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])
|
||||
|
||||
// 세션 확인 중 스플래시
|
||||
|
||||
@ -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 => (
|
||||
<TileLayer
|
||||
key={layer.id}
|
||||
url={`http://localhost:8080/geoserver/gwc/service/wms?service=WMS&version=1.1.0&request=GetMap&layers=${layer.wmsLayer}&styles=&bbox={bbox}&width=256&height=256&srs=EPSG:3857&format=image/png&transparent=true`}
|
||||
url={`${GEOSERVER_URL}/geoserver/gwc/service/wms?service=WMS&version=1.1.0&request=GetMap&layers=${layer.wmsLayer}&styles=&bbox={bbox}&width=256&height=256&srs=EPSG:3857&format=image/png&transparent=true`}
|
||||
attribution='© MPC GeoServer'
|
||||
opacity={layerOpacity / 100}
|
||||
/>
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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 }))
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@ -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;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user