Merge pull request 'develop' (#46) from develop into main
All checks were successful
Build and Deploy Wing-Demo / build-and-deploy (push) Successful in 25s
All checks were successful
Build and Deploy Wing-Demo / build-and-deploy (push) Successful in 25s
Reviewed-on: #46
This commit is contained in:
커밋
0cd6986c3f
@ -54,13 +54,15 @@ interface PredictionDetail {
|
|||||||
insuranceData: unknown;
|
insuranceData: unknown;
|
||||||
}>;
|
}>;
|
||||||
weather: Array<{
|
weather: Array<{
|
||||||
weatherDtm: string;
|
obsDtm: string;
|
||||||
windSpd: number | null;
|
locNm: string;
|
||||||
windDir: string | null;
|
temp: string;
|
||||||
waveHgt: number | null;
|
weatherDc: string;
|
||||||
currentSpd: number | null;
|
wind: string;
|
||||||
currentDir: string | null;
|
wave: string;
|
||||||
temp: number | null;
|
humid: string;
|
||||||
|
vis: string;
|
||||||
|
sst: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,16 +246,18 @@ export async function getAnalysisDetail(acdntSn: number): Promise<PredictionDeta
|
|||||||
|
|
||||||
const weatherSql = `
|
const weatherSql = `
|
||||||
SELECT
|
SELECT
|
||||||
WEATHER_DTM,
|
OBS_DTM,
|
||||||
WIND_SPD,
|
LOC_NM,
|
||||||
WIND_DIR,
|
TEMP,
|
||||||
WAVE_HGT,
|
WEATHER_DC,
|
||||||
CURRENT_SPD,
|
WIND,
|
||||||
CURRENT_DIR,
|
WAVE,
|
||||||
TEMP
|
HUMID,
|
||||||
|
VIS,
|
||||||
|
SST
|
||||||
FROM ACDNT_WEATHER
|
FROM ACDNT_WEATHER
|
||||||
WHERE ACDNT_SN = $1
|
WHERE ACDNT_SN = $1
|
||||||
ORDER BY WEATHER_DTM ASC
|
ORDER BY OBS_DTM ASC
|
||||||
`;
|
`;
|
||||||
const { rows: weatherRows } = await wingPool.query(weatherSql, [acdntSn]);
|
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>) => ({
|
const weather = weatherRows.map((w: Record<string, unknown>) => ({
|
||||||
weatherDtm: String(w['weather_dtm'] ?? ''),
|
obsDtm: w['obs_dtm'] ? String(w['obs_dtm']) : '',
|
||||||
windSpd: w['wind_spd'] != null ? parseFloat(String(w['wind_spd'])) : null,
|
locNm: String(w['loc_nm'] ?? ''),
|
||||||
windDir: w['wind_dir'] != null ? String(w['wind_dir']) : null,
|
temp: String(w['temp'] ?? ''),
|
||||||
waveHgt: w['wave_hgt'] != null ? parseFloat(String(w['wave_hgt'])) : null,
|
weatherDc: String(w['weather_dc'] ?? ''),
|
||||||
currentSpd: w['current_spd'] != null ? parseFloat(String(w['current_spd'])) : null,
|
wind: String(w['wind'] ?? ''),
|
||||||
currentDir: w['current_dir'] != null ? String(w['current_dir']) : null,
|
wave: String(w['wave'] ?? ''),
|
||||||
temp: w['temp'] != null ? parseFloat(String(w['temp'])) : null,
|
humid: String(w['humid'] ?? ''),
|
||||||
|
vis: String(w['vis'] ?? ''),
|
||||||
|
sst: String(w['sst'] ?? ''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -49,7 +49,13 @@ app.use(helmet({
|
|||||||
scriptSrc: ["'self'"],
|
scriptSrc: ["'self'"],
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
imgSrc: ["'self'", "data:", "blob:"],
|
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'"],
|
fontSrc: ["'self'"],
|
||||||
objectSrc: ["'none'"],
|
objectSrc: ["'none'"],
|
||||||
frameSrc: ["'none'"],
|
frameSrc: ["'none'"],
|
||||||
@ -65,11 +71,12 @@ app.disable('x-powered-by')
|
|||||||
|
|
||||||
// 3. CORS: 허용된 출처만 접근 가능
|
// 3. CORS: 허용된 출처만 접근 가능
|
||||||
const allowedOrigins = [
|
const allowedOrigins = [
|
||||||
'http://localhost:5173', // Vite dev server
|
process.env.FRONTEND_URL || 'https://wing-demo.gc-si.dev',
|
||||||
'http://localhost:5174',
|
...(process.env.NODE_ENV !== 'production' ? [
|
||||||
'http://localhost:3000',
|
'http://localhost:5173',
|
||||||
'https://wing-demo.gc-si.dev',
|
'http://localhost:5174',
|
||||||
process.env.FRONTEND_URL, // 운영 환경 프론트엔드 URL (추가 도메인)
|
'http://localhost:3000',
|
||||||
|
] : []),
|
||||||
].filter(Boolean) as string[]
|
].filter(Boolean) as string[]
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { LoginPage } from '@common/components/auth/LoginPage'
|
|||||||
import { registerMainTabSwitcher } from '@common/hooks/useSubMenu'
|
import { registerMainTabSwitcher } from '@common/hooks/useSubMenu'
|
||||||
import { useAuthStore } from '@common/store/authStore'
|
import { useAuthStore } from '@common/store/authStore'
|
||||||
import { useMenuStore } from '@common/store/menuStore'
|
import { useMenuStore } from '@common/store/menuStore'
|
||||||
|
import { API_BASE_URL } from '@common/services/api'
|
||||||
import { OilSpillView } from '@tabs/prediction'
|
import { OilSpillView } from '@tabs/prediction'
|
||||||
import { ReportsView } from '@tabs/reports'
|
import { ReportsView } from '@tabs/reports'
|
||||||
import { HNSView } from '@tabs/hns'
|
import { HNSView } from '@tabs/hns'
|
||||||
@ -46,8 +47,7 @@ function App() {
|
|||||||
[JSON.stringify({ action: 'TAB_VIEW', detail: activeMainTab })],
|
[JSON.stringify({ action: 'TAB_VIEW', detail: activeMainTab })],
|
||||||
{ type: 'text/plain' }
|
{ type: 'text/plain' }
|
||||||
)
|
)
|
||||||
const apiBase = import.meta.env.VITE_API_URL || 'http://localhost:3001/api'
|
navigator.sendBeacon(`${API_BASE_URL}/audit/log`, blob)
|
||||||
navigator.sendBeacon(`${apiBase}/audit/log`, blob)
|
|
||||||
}, [activeMainTab, isAuthenticated])
|
}, [activeMainTab, isAuthenticated])
|
||||||
|
|
||||||
// 세션 확인 중 스플래시
|
// 세션 확인 중 스플래시
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import type { BoomLine, BoomLineCoord } from '@common/types/boomLine'
|
|||||||
import type { ReplayShip, CollisionEvent } from '@common/types/backtrack'
|
import type { ReplayShip, CollisionEvent } from '@common/types/backtrack'
|
||||||
import { BacktrackReplayOverlay } from './BacktrackReplayOverlay'
|
import { BacktrackReplayOverlay } from './BacktrackReplayOverlay'
|
||||||
|
|
||||||
|
const GEOSERVER_URL = import.meta.env.VITE_GEOSERVER_URL || 'http://localhost:8080'
|
||||||
|
|
||||||
// Fix Leaflet default icon issue
|
// Fix Leaflet default icon issue
|
||||||
import icon from 'leaflet/dist/images/marker-icon.png'
|
import icon from 'leaflet/dist/images/marker-icon.png'
|
||||||
import iconShadow from 'leaflet/dist/images/marker-shadow.png'
|
import iconShadow from 'leaflet/dist/images/marker-shadow.png'
|
||||||
@ -190,7 +192,7 @@ export function MapView({
|
|||||||
{wmsLayers.map(layer => (
|
{wmsLayers.map(layer => (
|
||||||
<TileLayer
|
<TileLayer
|
||||||
key={layer.id}
|
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'
|
attribution='© MPC GeoServer'
|
||||||
opacity={layerOpacity / 100}
|
opacity={layerOpacity / 100}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import type { InsuranceRow } from './assetTypes'
|
import type { InsuranceRow } from './assetTypes'
|
||||||
|
|
||||||
|
const DEFAULT_HAEWOON_API = import.meta.env.VITE_HAEWOON_API_URL || 'https://api.haewoon.or.kr/v1/insurance'
|
||||||
|
|
||||||
// 샘플 데이터 (외부 한국해운조합 API 연동 전 데모용)
|
// 샘플 데이터 (외부 한국해운조합 API 연동 전 데모용)
|
||||||
const INSURANCE_DEMO_DATA: InsuranceRow[] = [
|
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억' },
|
{ 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() {
|
function ShipInsurance() {
|
||||||
const [apiConnected, setApiConnected] = useState(false)
|
const [apiConnected, setApiConnected] = useState(false)
|
||||||
const [showConfig, setShowConfig] = 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 [configApiKey, setConfigApiKey] = useState('')
|
||||||
const [configKeyType, setConfigKeyType] = useState('mmsi')
|
const [configKeyType, setConfigKeyType] = useState('mmsi')
|
||||||
const [configRespType, setConfigRespType] = useState('json')
|
const [configRespType, setConfigRespType] = useState('json')
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import type { BacktrackPhase, BacktrackVessel, BacktrackConditions, ReplayShip,
|
|||||||
import { TOTAL_REPLAY_FRAMES } from '@common/types/backtrack'
|
import { TOTAL_REPLAY_FRAMES } from '@common/types/backtrack'
|
||||||
import { fetchBacktrackByAcdnt, createBacktrack, fetchPredictionDetail } from '../services/predictionApi'
|
import { fetchBacktrackByAcdnt, createBacktrack, fetchPredictionDetail } from '../services/predictionApi'
|
||||||
import type { PredictionDetail } from '../services/predictionApi'
|
import type { PredictionDetail } from '../services/predictionApi'
|
||||||
|
import { api } from '@common/services/api'
|
||||||
|
|
||||||
export type PredictionModel = 'KOSPS' | 'POSEIDON' | 'OpenDrift'
|
export type PredictionModel = 'KOSPS' | 'POSEIDON' | 'OpenDrift'
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
@ -259,27 +260,16 @@ export function OilSpillView() {
|
|||||||
const models = Array.from(selectedModels)
|
const models = Array.from(selectedModels)
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
models.map(async (model) => {
|
models.map(async (model) => {
|
||||||
const response = await fetch('http://localhost:3001/api/simulation/run', {
|
const { data } = await api.post<{ trajectory: Array<{ lat: number; lon: number; time: number; particle?: number }> }>('/simulation/run', {
|
||||||
method: 'POST',
|
model,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
lat: incidentCoord.lat,
|
||||||
body: JSON.stringify({
|
lon: incidentCoord.lon,
|
||||||
model,
|
duration_hours: predictionTime,
|
||||||
lat: incidentCoord.lat,
|
oil_type: oilType,
|
||||||
lon: incidentCoord.lon,
|
spill_amount: spillAmount,
|
||||||
duration_hours: predictionTime,
|
spill_type: spillType,
|
||||||
oil_type: oilType,
|
|
||||||
spill_amount: spillAmount,
|
|
||||||
spill_type: spillType
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
return data.trajectory.map(p => ({ ...p, model }))
|
||||||
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 }))
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -54,13 +54,15 @@ export interface PredictionDetail {
|
|||||||
insuranceData: unknown;
|
insuranceData: unknown;
|
||||||
}>;
|
}>;
|
||||||
weather: Array<{
|
weather: Array<{
|
||||||
weatherDtm: string;
|
obsDtm: string;
|
||||||
windSpd: number | null;
|
locNm: string;
|
||||||
windDir: string | null;
|
temp: string;
|
||||||
waveHgt: number | null;
|
weatherDc: string;
|
||||||
currentSpd: number | null;
|
wind: string;
|
||||||
currentDir: string | null;
|
wave: string;
|
||||||
temp: number | null;
|
humid: string;
|
||||||
|
vis: string;
|
||||||
|
sst: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user