Merge pull request 'release: 2026-03-27.3 (5건 커밋)' (#138) from develop into main
All checks were successful
Build and Deploy Wing-Demo / build-and-deploy (push) Successful in 33s
All checks were successful
Build and Deploy Wing-Demo / build-and-deploy (push) Successful in 33s
This commit is contained in:
커밋
d71c43ae5a
@ -3,6 +3,7 @@ import { getBacktrack } from './predictionService.js';
|
||||
|
||||
const PYTHON_API_URL = process.env.PYTHON_API_URL ?? 'http://localhost:5003';
|
||||
const VESSEL_TRACK_API_URL = process.env.VESSEL_TRACK_API_URL ?? 'https://guide.gc-si.dev/signal-batch';
|
||||
const VESSEL_TRACK_COOKIE = process.env.VESSEL_TRACK_COOKIE ?? '';
|
||||
|
||||
// 유종 코드(DB) → OpenDrift 유종 코드 매핑
|
||||
const OIL_TYPE_MAP: Record<string, string> = {
|
||||
@ -13,8 +14,6 @@ const OIL_TYPE_MAP: Record<string, string> = {
|
||||
'KEROSENE': 'FUEL OIL NO.1 (KEROSENE)',
|
||||
'GASOLINE': 'GENERIC GASOLINE',
|
||||
};
|
||||
const POLL_INTERVAL_MS = 3000;
|
||||
const POLL_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
|
||||
// AIS 선박유형 코드 → 위험도 점수 매핑
|
||||
// AIS VESSEL_TP: 80-89=유조선류, 70-79=카고, 30-39=어선
|
||||
@ -133,25 +132,6 @@ function toTimeLabel(d: Date): string {
|
||||
return `${String(kst.getUTCHours()).padStart(2, '0')}:${String(kst.getUTCMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Python 결과 폴링 (DONE까지 대기)
|
||||
async function pollUntilDone(jobId: string): Promise<PythonTimeStep[]> {
|
||||
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise<void>(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||
try {
|
||||
const res = await fetch(`${PYTHON_API_URL}/status/${jobId}`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) continue;
|
||||
const data = await res.json() as { status: string; result?: PythonTimeStep[]; error?: string };
|
||||
if (data.status === 'DONE' && data.result) return data.result;
|
||||
if (data.status === 'ERROR') throw new Error(data.error ?? 'Python 분석 오류');
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message !== 'fetch failed') throw e;
|
||||
}
|
||||
}
|
||||
throw new Error('역추적 분석 시간 초과 (30분)');
|
||||
}
|
||||
|
||||
// 파티클 스텝별 탐색 영역 계산
|
||||
function computeParticleSteps(
|
||||
@ -286,16 +266,29 @@ async function fetchVesselTracks(
|
||||
polygons: [polygon],
|
||||
};
|
||||
|
||||
console.log(body);
|
||||
console.log('[backtrack] VESSEL_TRACK_API 호출', {
|
||||
url: `${VESSEL_TRACK_API_URL}/api/v2/tracks/area-search`,
|
||||
startTime: body.startTime,
|
||||
endTime: body.endTime,
|
||||
srchRadiusNm,
|
||||
polygons: body.polygons.length,
|
||||
hasCookie: !!VESSEL_TRACK_COOKIE,
|
||||
});
|
||||
|
||||
const res = await fetch(`${VESSEL_TRACK_API_URL}/api/v2/tracks/area-search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(VESSEL_TRACK_COOKIE ? { 'Cookie': VESSEL_TRACK_COOKIE } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
console.log('[backtrack] VESSEL_TRACK_API 응답', { status: res.status, ok: res.ok });
|
||||
if (!res.ok) throw new Error(`선박 항적 API 오류: ${res.status}`);
|
||||
return res.json() as Promise<VesselTrackApiResponse>;
|
||||
const data = await res.json() as VesselTrackApiResponse;
|
||||
console.log('[backtrack] VESSEL_TRACK_API 데이터', { totalVessels: data.summary?.totalVessels ?? 0, tracks: data.tracks?.length ?? 0 });
|
||||
return data;
|
||||
}
|
||||
|
||||
// 스코어링 + 순위 (선박 항적 API 응답 기반)
|
||||
@ -473,6 +466,7 @@ export async function runBacktrackAnalysis(backtrackSn: number): Promise<void> {
|
||||
}
|
||||
|
||||
// Python 역방향 시뮬레이션 실행 (파티클 시각화용)
|
||||
console.log('[backtrack] Python 역방향 시뮬레이션 요청', { lat: bt.lat, lon: bt.lon, anlysHours, matVol, matTy, spillTime: spillTime.toISOString() });
|
||||
let rawResult: PythonTimeStep[];
|
||||
try {
|
||||
const pythonRes = await fetch(`${PYTHON_API_URL}/run-model-backward`, {
|
||||
@ -490,8 +484,9 @@ export async function runBacktrackAnalysis(backtrackSn: number): Promise<void> {
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
if (!pythonRes.ok) throw new Error(`Python 서버 오류: ${pythonRes.status}`);
|
||||
const { job_id } = await pythonRes.json() as { job_id: string };
|
||||
rawResult = await pollUntilDone(job_id);
|
||||
const pythonData = await pythonRes.json() as { success: boolean; result: PythonTimeStep[] };
|
||||
rawResult = pythonData.result ?? [];
|
||||
console.log('[backtrack] Python 역방향 시뮬레이션 완료 — steps:', rawResult.length);
|
||||
} catch (pyErr) {
|
||||
// Python 미연동 시 폴백: 빈 파티클 스텝 생성
|
||||
console.warn('[backtrack] Python 미연동 — 폴백 모드 사용:', pyErr);
|
||||
@ -515,16 +510,19 @@ export async function runBacktrackAnalysis(backtrackSn: number): Promise<void> {
|
||||
|
||||
// 선박 항적 API 호출
|
||||
const srchRadius = bt.srchRadiusNm ?? 10;
|
||||
console.log('[backtrack] 선박 항적 API 호출 시작', { srchRadius, analysisStart: analysisStart.toISOString(), spillTime: spillTime.toISOString() });
|
||||
const apiResponse = await fetchVesselTracks(
|
||||
bt.lat, bt.lon, srchRadius, analysisStart, spillTime,
|
||||
);
|
||||
|
||||
const totalVessels = apiResponse.summary.totalVessels;
|
||||
console.log('[backtrack] 선박 점수 계산 시작 — totalVessels:', totalVessels);
|
||||
const ranked = scoreAndRankVesselsFromApi(
|
||||
apiResponse.tracks,
|
||||
apiResponse.hitDetails,
|
||||
bt.lat, bt.lon, srchRadius, anlysHours,
|
||||
);
|
||||
console.log('[backtrack] 선박 점수 계산 완료 — ranked:', ranked.length, '/', totalVessels, '| top:', ranked[0]?.name, ranked[0]?.probability);
|
||||
const replayShips = buildReplayShipsFromApi(ranked);
|
||||
const collisionEvent = findCollisionEventFromApi(ranked, analysisStart, spillTime);
|
||||
|
||||
@ -536,7 +534,15 @@ export async function runBacktrackAnalysis(backtrackSn: number): Promise<void> {
|
||||
// vessels에서 내부 필드 제거
|
||||
const vessels = ranked.map(({ _rawScore: _r, _track: _t, _minDistIdx: _m, ...v }) => v);
|
||||
|
||||
const rsltData = { vessels, replayShips, collisionEvent, timeRange };
|
||||
// rawResult 샘플링 (최대 24 스텝, 파티클은 [lon, lat] 형식으로 저장)
|
||||
const MAX_BACKWARD_STEPS = 24;
|
||||
const sampleRate = Math.max(1, Math.ceil(rawResult.length / MAX_BACKWARD_STEPS));
|
||||
const backwardParticles = rawResult
|
||||
.filter((_, i) => i % sampleRate === 0)
|
||||
.slice(0, MAX_BACKWARD_STEPS)
|
||||
.map(step => step.particles.map(p => [p.lon, p.lat] as [number, number]));
|
||||
|
||||
const rsltData = { vessels, replayShips, collisionEvent, timeRange, backwardParticles };
|
||||
|
||||
await wingPool.query(
|
||||
`UPDATE wing.BACKTRACK
|
||||
|
||||
@ -4,6 +4,14 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026-03-27.3]
|
||||
|
||||
### 추가
|
||||
- 역추적: 리플레이에 Python 역방향 시뮬레이션 파티클 표시 (보라색 ScatterplotLayer)
|
||||
- 역추적: 전체 파티클 이동 경로 외각 폴리곤(컨벡스 헐) 표시
|
||||
- 역추적: 리플레이 바 — 재생 완료 후 재시작 기능 (↺ 아이콘)
|
||||
- 역추적: 리플레이 바 — 드래그 시크 기능 추가
|
||||
|
||||
## [2026-03-27.2]
|
||||
|
||||
### 수정
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
import type { ReplayShip, CollisionEvent } from '@common/types/backtrack'
|
||||
|
||||
interface BacktrackReplayBarProps {
|
||||
@ -12,6 +13,7 @@ interface BacktrackReplayBarProps {
|
||||
replayShips: ReplayShip[]
|
||||
collisionEvent: CollisionEvent | null
|
||||
replayTimeRange?: { start: string; end: string }
|
||||
hasBackwardParticles?: boolean
|
||||
}
|
||||
|
||||
export function BacktrackReplayBar({
|
||||
@ -26,8 +28,51 @@ export function BacktrackReplayBar({
|
||||
replayShips,
|
||||
collisionEvent,
|
||||
replayTimeRange,
|
||||
hasBackwardParticles,
|
||||
}: BacktrackReplayBarProps) {
|
||||
const progress = (replayFrame / totalFrames) * 100
|
||||
const isFinished = replayFrame >= totalFrames
|
||||
|
||||
// 드래그 시크
|
||||
const barRef = useRef<HTMLDivElement>(null)
|
||||
const isDraggingRef = useRef(false)
|
||||
const onSeekRef = useRef(onSeek)
|
||||
const totalFramesRef = useRef(totalFrames)
|
||||
|
||||
useEffect(() => { onSeekRef.current = onSeek }, [onSeek])
|
||||
useEffect(() => { totalFramesRef.current = totalFrames }, [totalFrames])
|
||||
|
||||
useEffect(() => {
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!isDraggingRef.current || !barRef.current) return
|
||||
const rect = barRef.current.getBoundingClientRect()
|
||||
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
onSeekRef.current(Math.round(pct * totalFramesRef.current))
|
||||
}
|
||||
const onMouseUp = () => { isDraggingRef.current = false }
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleBarMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
isDraggingRef.current = true
|
||||
if (!barRef.current) return
|
||||
const rect = barRef.current.getBoundingClientRect()
|
||||
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
onSeek(Math.round(pct * totalFrames))
|
||||
}
|
||||
|
||||
// 재생 완료 후 재생 버튼 클릭 → 처음부터 재시작
|
||||
const handlePlayClick = () => {
|
||||
if (!isPlaying && isFinished) {
|
||||
onSeek(0)
|
||||
}
|
||||
onTogglePlay()
|
||||
}
|
||||
|
||||
// 타임 계산
|
||||
let startLabel: string
|
||||
@ -64,12 +109,6 @@ export function BacktrackReplayBar({
|
||||
currentTimeLabel = `${dayLabel} ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')} KST`
|
||||
}
|
||||
|
||||
const handleSeekClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
onSeek(Math.round(pct * totalFrames))
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute flex flex-col"
|
||||
@ -126,7 +165,7 @@ export function BacktrackReplayBar({
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Play/Pause */}
|
||||
<button
|
||||
onClick={onTogglePlay}
|
||||
onClick={handlePlayClick}
|
||||
className="shrink-0 w-9 h-9 rounded-full flex items-center justify-center text-sm cursor-pointer"
|
||||
style={{
|
||||
background: isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.15)',
|
||||
@ -134,15 +173,16 @@ export function BacktrackReplayBar({
|
||||
color: isPlaying ? '#fff' : 'var(--purple)',
|
||||
}}
|
||||
>
|
||||
{isPlaying ? '⏸' : '▶'}
|
||||
{isPlaying ? '⏸' : isFinished ? '↺' : '▶'}
|
||||
</button>
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
{/* Progress bar */}
|
||||
<div
|
||||
ref={barRef}
|
||||
className="relative h-5 flex items-center cursor-pointer"
|
||||
onClick={handleSeekClick}
|
||||
onMouseDown={handleBarMouseDown}
|
||||
>
|
||||
<div
|
||||
className="w-full h-1 bg-border relative overflow-visible"
|
||||
@ -197,7 +237,7 @@ export function BacktrackReplayBar({
|
||||
</div>
|
||||
|
||||
{/* Legend row */}
|
||||
<div className="flex items-center gap-[14px] pt-1 border-t border-border">
|
||||
<div className="flex items-center gap-[14px] pt-1 border-t border-border flex-wrap">
|
||||
{replayShips.map((ship) => (
|
||||
<div key={ship.vesselName} className="flex items-center gap-1.5">
|
||||
<div className="w-4 h-[3px]" style={{ background: ship.color, borderRadius: '1px' }} />
|
||||
@ -206,6 +246,12 @@ export function BacktrackReplayBar({
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{hasBackwardParticles && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full" style={{ background: '#a855f7', opacity: 0.8 }} />
|
||||
<span className="text-[9px] text-text-2 font-mono">역방향 예측</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,7 +1,29 @@
|
||||
import { ScatterplotLayer, PathLayer } from '@deck.gl/layers'
|
||||
import type { ReplayShip, CollisionEvent, ReplayPathPoint } from '@common/types/backtrack'
|
||||
import { ScatterplotLayer, PathLayer, PolygonLayer } from '@deck.gl/layers'
|
||||
import type { ReplayShip, CollisionEvent, ReplayPathPoint, BackwardParticleStep } from '@common/types/backtrack'
|
||||
import { hexToRgba } from './mapUtils'
|
||||
|
||||
// Andrew's monotone chain — 전체 파티클 경로의 외각 폴리곤 계산
|
||||
function convexHull(points: [number, number][]): [number, number][] {
|
||||
if (points.length < 3) return points
|
||||
const cross = (O: [number, number], A: [number, number], B: [number, number]) =>
|
||||
(A[0] - O[0]) * (B[1] - O[1]) - (A[1] - O[1]) * (B[0] - O[0])
|
||||
const sorted = [...points].sort((a, b) => a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1])
|
||||
const lower: [number, number][] = []
|
||||
for (const p of sorted) {
|
||||
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop()
|
||||
lower.push(p)
|
||||
}
|
||||
const upper: [number, number][] = []
|
||||
for (let i = sorted.length - 1; i >= 0; i--) {
|
||||
const p = sorted[i]
|
||||
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop()
|
||||
upper.push(p)
|
||||
}
|
||||
lower.pop()
|
||||
upper.pop()
|
||||
return [...lower, ...upper]
|
||||
}
|
||||
|
||||
function getInterpolatedPosition(
|
||||
path: ReplayPathPoint[],
|
||||
frame: number,
|
||||
@ -24,11 +46,14 @@ interface BacktrackReplayParams {
|
||||
replayFrame: number
|
||||
totalFrames: number
|
||||
incidentCoord: { lat: number; lon: number }
|
||||
backwardParticles?: BackwardParticleStep[]
|
||||
}
|
||||
|
||||
const BACKWARD_COLOR: [number, number, number, number] = [168, 85, 247, 160]
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function createBacktrackLayers(params: BacktrackReplayParams): any[] {
|
||||
const { replayShips, collisionEvent, replayFrame, totalFrames, incidentCoord } = params
|
||||
const { replayShips, collisionEvent, replayFrame, totalFrames, incidentCoord, backwardParticles } = params
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const layers: any[] = []
|
||||
const progress = replayFrame / totalFrames
|
||||
@ -145,5 +170,46 @@ export function createBacktrackLayers(params: BacktrackReplayParams): any[] {
|
||||
}
|
||||
}
|
||||
|
||||
// 역방향 예측 파티클 + 전체 경로 외각 폴리곤
|
||||
if (backwardParticles && backwardParticles.length > 0) {
|
||||
// 전체 스텝의 모든 파티클을 합쳐 외각 폴리곤 계산 (정적 — 항상 표시)
|
||||
const allPoints = backwardParticles.flat()
|
||||
if (allPoints.length >= 3) {
|
||||
const hull = convexHull(allPoints)
|
||||
if (hull.length >= 3) {
|
||||
layers.push(
|
||||
new PolygonLayer({
|
||||
id: 'bt-backward-hull',
|
||||
data: [{ polygon: hull }],
|
||||
getPolygon: (d: { polygon: [number, number][] }) => d.polygon,
|
||||
getFillColor: [168, 85, 247, 18],
|
||||
getLineColor: [168, 85, 247, 110],
|
||||
getLineWidth: 1,
|
||||
stroked: true,
|
||||
filled: true,
|
||||
lineWidthMinPixels: 1,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 프레임 파티클
|
||||
const stepIndex = Math.round((1 - progress) * (backwardParticles.length - 1))
|
||||
const particles = backwardParticles[stepIndex] ?? []
|
||||
if (particles.length > 0) {
|
||||
layers.push(
|
||||
new ScatterplotLayer({
|
||||
id: 'bt-backward-particles',
|
||||
data: particles,
|
||||
getPosition: (d: [number, number]) => d,
|
||||
getRadius: 3,
|
||||
getFillColor: BACKWARD_COLOR,
|
||||
radiusMinPixels: 2.5,
|
||||
radiusMaxPixels: 5,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return layers
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import type { PredictionModel, SensitiveResource } from '@tabs/prediction/compon
|
||||
import type { HydrDataStep, SensitiveResourceFeatureCollection } from '@tabs/prediction/services/predictionApi'
|
||||
import HydrParticleOverlay from './HydrParticleOverlay'
|
||||
import type { BoomLine, BoomLineCoord } from '@common/types/boomLine'
|
||||
import type { ReplayShip, CollisionEvent } from '@common/types/backtrack'
|
||||
import type { ReplayShip, CollisionEvent, BackwardParticleStep } from '@common/types/backtrack'
|
||||
import { createBacktrackLayers } from './BacktrackReplayOverlay'
|
||||
import { buildMeasureLayers } from './measureLayers'
|
||||
import { MeasureOverlay } from './MeasureOverlay'
|
||||
@ -358,6 +358,7 @@ interface MapViewProps {
|
||||
replayFrame: number
|
||||
totalFrames: number
|
||||
incidentCoord: { lat: number; lon: number }
|
||||
backwardParticles?: BackwardParticleStep[]
|
||||
}
|
||||
sensitiveResources?: SensitiveResource[]
|
||||
sensitiveResourceGeojson?: SensitiveResourceFeatureCollection | null
|
||||
@ -1100,6 +1101,7 @@ export function MapView({
|
||||
replayFrame: backtrackReplay.replayFrame,
|
||||
totalFrames: backtrackReplay.totalFrames,
|
||||
incidentCoord: backtrackReplay.incidentCoord,
|
||||
backwardParticles: backtrackReplay.backwardParticles,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -56,3 +56,6 @@ export interface BacktrackTimeRange {
|
||||
start: string // ISO 문자열
|
||||
end: string // ISO 문자열
|
||||
}
|
||||
|
||||
// 역방향 예측 파티클 스텝 — 각 스텝은 [lon, lat] 쌍의 배열
|
||||
export type BackwardParticleStep = [number, number][]
|
||||
|
||||
@ -12,7 +12,7 @@ import { useSubMenu, navigateToTab, setReportGenCategory, setOilReportPayload, t
|
||||
import { fetchWeatherSnapshotForCoord } from '@tabs/weather/services/weatherUtils'
|
||||
import { useWeatherSnapshotStore } from '@common/store/weatherSnapshotStore'
|
||||
import type { BoomLine, AlgorithmSettings, ContainmentResult, BoomLineCoord } from '@common/types/boomLine'
|
||||
import type { BacktrackPhase, BacktrackVessel, BacktrackConditions, BacktrackInputConditions, ReplayShip, CollisionEvent } from '@common/types/backtrack'
|
||||
import type { BacktrackPhase, BacktrackVessel, BacktrackConditions, BacktrackInputConditions, ReplayShip, CollisionEvent, BackwardParticleStep } from '@common/types/backtrack'
|
||||
import { TOTAL_REPLAY_FRAMES } from '@common/types/backtrack'
|
||||
import { fetchBacktrackByAcdnt, createBacktrack, fetchPredictionDetail, fetchAnalysisTrajectory, fetchSensitiveResources, fetchSensitiveResourcesGeojson } from '../services/predictionApi'
|
||||
import type { CenterPoint, HydrDataStep, ImageAnalyzeResult, OilParticle, PredictionDetail, RunModelSyncResponse, SimulationSummary, SensitiveResourceCategory, SensitiveResourceFeatureCollection, WindPoint } from '../services/predictionApi'
|
||||
@ -194,6 +194,7 @@ export function OilSpillView() {
|
||||
const [replayShips, setReplayShips] = useState<ReplayShip[]>([])
|
||||
const [collisionEvent, setCollisionEvent] = useState<CollisionEvent | null>(null)
|
||||
const [replayTimeRange, setReplayTimeRange] = useState<{ start: string; end: string } | null>(null)
|
||||
const [backwardParticles, setBackwardParticles] = useState<BackwardParticleStep[]>([])
|
||||
|
||||
// 재계산 상태
|
||||
const [recalcModalOpen, setRecalcModalOpen] = useState(false)
|
||||
@ -250,6 +251,7 @@ export function OilSpillView() {
|
||||
if (rslt['timeRange']) {
|
||||
setReplayTimeRange(rslt['timeRange'] as { start: string; end: string })
|
||||
}
|
||||
setBackwardParticles(Array.isArray(rslt['backwardParticles']) ? rslt['backwardParticles'] as BackwardParticleStep[] : [])
|
||||
setBacktrackConditions({
|
||||
estimatedSpillTime: bt.estSpilDtm ? new Date(bt.estSpilDtm).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '',
|
||||
analysisRange: bt.anlysRange || '±12시간',
|
||||
@ -308,6 +310,7 @@ export function OilSpillView() {
|
||||
if (Array.isArray(rslt['replayShips'])) setReplayShips(rslt['replayShips'] as ReplayShip[])
|
||||
if (rslt['collisionEvent']) setCollisionEvent(rslt['collisionEvent'] as CollisionEvent)
|
||||
if (rslt['timeRange']) setReplayTimeRange(rslt['timeRange'] as { start: string; end: string })
|
||||
setBackwardParticles(Array.isArray(rslt['backwardParticles']) ? rslt['backwardParticles'] as BackwardParticleStep[] : [])
|
||||
setBacktrackConditions(prev => ({ ...prev, totalVessels: bt.totalVessels || 0 }))
|
||||
setBacktrackPhase('results')
|
||||
} else {
|
||||
@ -1051,6 +1054,7 @@ export function OilSpillView() {
|
||||
replayFrame,
|
||||
totalFrames: TOTAL_REPLAY_FRAMES,
|
||||
incidentCoord,
|
||||
backwardParticles,
|
||||
} : undefined}
|
||||
showCurrent={displayControls.showCurrent}
|
||||
showWind={displayControls.showWind}
|
||||
@ -1241,6 +1245,7 @@ export function OilSpillView() {
|
||||
replayShips={replayShips}
|
||||
collisionEvent={collisionEvent}
|
||||
replayTimeRange={replayTimeRange ?? undefined}
|
||||
hasBackwardParticles={backwardParticles.length > 0}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user