import { useState, useMemo } from 'react' import type { PredictionDetail, SimulationSummary, CenterPoint } from '../services/predictionApi' import type { DisplayControls } from './OilSpillView' import { haversineDistance, computeBearing } from '@common/utils/geo' interface AnalysisResult { area: number particleCount: number particlePercent: number sensitiveCount: number } interface RightPanelProps { onOpenBacktrack?: () => void onOpenRecalc?: () => void onOpenReport?: () => void detail?: PredictionDetail | null summary?: SimulationSummary | null displayControls?: DisplayControls onDisplayControlsChange?: (controls: DisplayControls) => void windHydrModel?: string windHydrModelOptions?: string[] onWindHydrModelChange?: (model: string) => void analysisTab?: 'polygon' | 'circle' onSwitchAnalysisTab?: (tab: 'polygon' | 'circle') => void drawAnalysisMode?: 'polygon' | null analysisPolygonPoints?: Array<{ lat: number; lon: number }> circleRadiusNm?: number onCircleRadiusChange?: (nm: number) => void analysisResult?: AnalysisResult | null incidentCoord?: { lat: number; lon: number } | null centerPoints?: CenterPoint[] predictionTime?: number boomBlockedVolume?: number onStartPolygonDraw?: () => void onRunPolygonAnalysis?: () => void onRunCircleAnalysis?: () => void onCancelAnalysis?: () => void onClearAnalysis?: () => void } export function RightPanel({ onOpenBacktrack, onOpenRecalc, onOpenReport, detail, summary, displayControls, onDisplayControlsChange, windHydrModel, windHydrModelOptions = [], onWindHydrModelChange, analysisTab = 'polygon', onSwitchAnalysisTab, drawAnalysisMode, analysisPolygonPoints = [], circleRadiusNm = 5, onCircleRadiusChange, analysisResult, incidentCoord, centerPoints, predictionTime, boomBlockedVolume = 0, onStartPolygonDraw, onRunPolygonAnalysis, onRunCircleAnalysis, onCancelAnalysis, onClearAnalysis, }: RightPanelProps) { const vessel = detail?.vessels?.[0] const vessel2 = detail?.vessels?.[1] const spill = detail?.spill const insurance = vessel?.insuranceData as Array<{ type: string; insurer: string; value: string; currency: string }> | null const [shipExpanded, setShipExpanded] = useState(false) const [insuranceExpanded, setInsuranceExpanded] = useState(false) const weatheringStatus = useMemo(() => { if (!summary) return null; const total = summary.remainingVolume + summary.evaporationVolume + summary.dispersionVolume + summary.beachedVolume + boomBlockedVolume; if (total <= 0) return null; const pct = (v: number) => Math.round((v / total) * 100); return { surface: pct(summary.remainingVolume), evaporation: pct(summary.evaporationVolume), dispersion: pct(summary.dispersionVolume), boom: pct(boomBlockedVolume), beached: pct(summary.beachedVolume), }; }, [summary, boomBlockedVolume]) const spreadSummary = useMemo(() => { if (!incidentCoord || !centerPoints || centerPoints.length === 0) return null const finalPoint = [...centerPoints].sort((a, b) => b.time - a.time)[0] const distM = haversineDistance(incidentCoord, { lat: finalPoint.lat, lon: finalPoint.lon }) const distKm = distM / 1000 const bearing = computeBearing(incidentCoord, { lat: finalPoint.lat, lon: finalPoint.lon }) const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'] const dirLabel = directions[Math.round(bearing / 45) % 8] const speedMs = predictionTime && predictionTime > 0 ? distM / (predictionTime * 3600) : null return { area: summary?.pollutionArea ?? null, distance: distKm, directionLabel: `${dirLabel} ${Math.round(bearing)}°`, speed: speedMs, } }, [incidentCoord, centerPoints, summary, predictionTime]) return (
{/* Tab Header */}
{/* Scrollable Content */}
{/* 표시 정보 제어 */}
onDisplayControlsChange?.({ ...displayControls!, showCurrent: v })} >유향/유속 onDisplayControlsChange?.({ ...displayControls!, showWind: v })} >풍향/풍속 onDisplayControlsChange?.({ ...displayControls!, showBeached: v })} >해안부착 onDisplayControlsChange?.({ ...displayControls!, showSensitiveResources: v })} > 민감자원 onDisplayControlsChange?.({ ...displayControls!, showTimeLabel: v })} >시간 표시
{windHydrModelOptions.length > 1 && (
데이터 모델
)}
{/* 오염분석 */}
{/* 탭 전환 */}
{(['polygon', 'circle'] as const).map((tab) => ( ))}
{/* 다각형 패널 */} {analysisTab === 'polygon' && (

지도에서 다각형 영역을 지정하여 해당 범위 내 오염도를 분석합니다.

{!drawAnalysisMode && !analysisResult && ( )} {drawAnalysisMode === 'polygon' && (
지도를 클릭하여 꼭짓점을 추가하세요
현재 {analysisPolygonPoints.length}개 선택됨
)} {analysisResult && !drawAnalysisMode && ( )}
)} {/* 원 분석 패널 */} {analysisTab === 'circle' && (

반경(NM)을 지정하면 사고지점 기준 원형 영역의 오염도를 분석합니다.

반경 선택 (NM)
{[1, 3, 5, 10, 15, 20, 30, 50].map((nm) => ( ))}
직접 입력 onCircleRadiusChange?.(parseFloat(e.target.value) || 0.1)} className="w-14 text-center py-1 px-1 bg-bg-0 border border-border rounded text-[11px] font-mono text-text-1 outline-none focus:border-primary-cyan" style={{ colorScheme: 'dark' }} /> NM
{analysisResult && ( )}
)}
{/* 오염 종합 상황 */}
{/* 확산 예측 요약 */}
{/* 유출유 풍화 상태 */}
{weatheringStatus ? ( <> ) : (

시뮬레이션 실행 후 표시됩니다

)}
{/* 사고 선박 제원 */} setShipExpanded(!shipExpanded)} >
{/* 선박 카드 */}
🚢
{vessel?.vesselNm || '—'}
IMO {vessel?.imoNo || '—'} · {vessel?.vesselTp || '—'}
사고
{/* 제원 */}
{/* 충돌 상대 */} {vessel2 && (
⚠ 충돌 상대: {vessel2.vesselNm}
{vessel2.flagCd} {vessel2.vesselTp} {vessel2.gt ? `${vessel2.gt.toLocaleString()}GT` : ''}
)}
{/* 선주 / 보험 */} setInsuranceExpanded(!insuranceExpanded)} >
{insurance && insurance.length > 0 ? ( <> {insurance.filter(ins => ins.type === 'P&I').map((ins, i) => ( ))} {insurance.filter(ins => ins.type === 'H&M').map((ins, i) => ( ))} {insurance.filter(ins => ins.type === 'CLC').map((ins, i) => ( ))} ) : (
보험 정보가 없습니다.
)}
{/* Bottom Action Buttons */}
) } // Helper Components function Section({ title, badge, badgeColor, children }: { title: string badge?: string badgeColor?: 'red' | 'green' children: React.ReactNode }) { return (

{title}

{badge && ( {badge} )}
{children}
) } function ControlledCheckbox({ checked, onChange, children, disabled = false, }: { checked: boolean; onChange: (v: boolean) => void; children: string; disabled?: boolean; }) { return ( ); } function StatBox({ label, value, unit, color }: { label: string value: string unit: string color: string }) { return (
{label} {value} {unit}
) } function PredictionCard({ value, label, color }: { value: string; label: string; color: string }) { return (
{label} {value}
) } function ProgressBar({ label, value, color }: { label: string; value: number; color: string }) { return (
{label}
{value}%
) } function CollapsibleSection({ title, expanded, onToggle, children }: { title: string expanded: boolean onToggle: () => void children: React.ReactNode }) { return (

{title}

{expanded ? '▾' : '▸'}
{expanded && children}
) } function SpecCard({ value, label, color }: { value: string; label: string; color: string }) { return (
{value}
{label}
) } function InfoRow({ label, value, mono, valueColor }: { label: string value: string mono?: boolean valueColor?: string }) { return (
{label} {value}
) } function InsuranceCard({ title, color, items }: { title: string color: 'cyan' | 'purple' | 'red' items: Array<{ label: string; value: string; mono?: boolean; valueColor?: string }> }) { const colorMap = { cyan: { border: 'rgba(6,182,212,0.15)', bg: 'rgba(6,182,212,0.02)', text: 'var(--cyan)' }, purple: { border: 'rgba(168,85,247,0.15)', bg: 'rgba(168,85,247,0.02)', text: 'var(--purple)' }, red: { border: 'rgba(239,68,68,0.15)', bg: 'rgba(239,68,68,0.02)', text: 'var(--red)' } } const colors = colorMap[color] return (
{title}
{items.map((item, i) => (
{item.label} {item.value}
))}
) } function PollResult({ result, summary, onClear, onRerun, radiusNm, }: { result: AnalysisResult summary?: SimulationSummary | null onClear?: () => void onRerun?: () => void radiusNm?: number }) { const pollutedArea = (result.area * result.particlePercent / 100).toFixed(2) return (
{radiusNm && (
분석 결과 반경 {radiusNm} NM
)}
{result.area.toFixed(2)}
분석면적(km²)
{result.particlePercent}%
오염비율
{pollutedArea}
오염면적(km²)
{summary && (
해상잔존량 {summary.remainingVolume.toFixed(2)} m³
)} {summary && (
연안부착량 {summary.beachedVolume.toFixed(2)} m³
)}
민감자원 포함 {result.sensitiveCount}개소
{onRerun && ( )}
) }