import { useState } from 'react' import type { BoomLine, BoomLineCoord, AlgorithmSettings, ContainmentResult } from '@common/types/boomLine' import { generateAIBoomLines, runContainmentAnalysis } from '@common/utils/geo' interface OilBoomSectionProps { expanded: boolean onToggle: () => void boomLines: BoomLine[] onBoomLinesChange: (lines: BoomLine[]) => void oilTrajectory: Array<{ lat: number; lon: number; time: number; particle?: number }> incidentCoord: { lon: number; lat: number } algorithmSettings: AlgorithmSettings onAlgorithmSettingsChange: (settings: AlgorithmSettings) => void isDrawingBoom: boolean onDrawingBoomChange: (drawing: boolean) => void drawingPoints: BoomLineCoord[] onDrawingPointsChange: (points: BoomLineCoord[]) => void containmentResult: ContainmentResult | null onContainmentResultChange: (result: ContainmentResult | null) => void } const DEFAULT_SETTINGS: AlgorithmSettings = { currentOrthogonalCorrection: 15, safetyMarginMinutes: 60, minContainmentEfficiency: 80, waveHeightCorrectionFactor: 1.0, } const OilBoomSection = ({ expanded, onToggle, boomLines, onBoomLinesChange, oilTrajectory, incidentCoord, algorithmSettings, onAlgorithmSettingsChange, onDrawingBoomChange, onDrawingPointsChange, containmentResult, onContainmentResultChange, }: OilBoomSectionProps) => { const [boomPlacementTab, setBoomPlacementTab] = useState<'ai' | 'simulation'>('simulation') const [showResetConfirm, setShowResetConfirm] = useState(false) const hasData = boomLines.length > 0 || containmentResult !== null /** V자형 오일붐 배치 + 차단 시뮬레이션 실행 */ const handleRunSimulation = () => { // 1단계: V자형 오일붐 자동 배치 const lines = generateAIBoomLines( oilTrajectory, { lat: incidentCoord.lat, lon: incidentCoord.lon }, algorithmSettings, ) onBoomLinesChange(lines) // 2단계: 차단 시뮬레이션 실행 const result = runContainmentAnalysis(oilTrajectory, lines) onContainmentResultChange(result) } /** 초기화 (오일펜스만, 확산예측 유지) */ const handleReset = () => { onBoomLinesChange([]) onDrawingBoomChange(false) onDrawingPointsChange([]) onContainmentResultChange(null) onAlgorithmSettingsChange({ ...DEFAULT_SETTINGS }) setShowResetConfirm(false) } return (

오일펜스 배치 가이드

{expanded ? '▼' : '▶'}
{expanded && (
{/* 탭 버튼 + 초기화 */}
{[ { id: 'ai' as const, label: 'AI 자동 추천' }, { id: 'simulation' as const, label: '시뮬레이션' }, ].map(tab => ( ))}
{/* 초기화 확인 팝업 */} {showResetConfirm && (
⚠ 오일펜스 배치 가이드를 초기화 합니다
배치된 오일펜스 라인과 시뮬레이션 결과가 삭제됩니다. 확산 예측 결과는 유지됩니다.
)} {/* Key Metrics */}
{[ { value: String(boomLines.length), label: '배치 라인', color: 'var(--color-accent)' }, { value: boomLines.length > 0 ? `${(boomLines.reduce((s, l) => s + l.length, 0) / 1000).toFixed(1)}km` : '0km', label: '총 길이', color: 'var(--color-accent)' }, { value: boomLines.length > 0 ? `${Math.round(boomLines.reduce((s, l) => s + l.efficiency, 0) / boomLines.length)}%` : '—', label: '평균 효율', color: 'var(--color-accent)' }, ].map((metric, idx) => (
{metric.value}
{metric.label}
))}
{/* ===== 시뮬레이션 탭 ===== */} {boomPlacementTab === 'simulation' && ( <> {/* 전제조건 체크 */}
0 ? 'var(--color-success)' : 'var(--color-danger)' }} /> 0 ? 'var(--color-success)' : 'var(--fg-disabled)' }}> 확산 궤적 데이터 {oilTrajectory.length > 0 ? `(${oilTrajectory.length}개 입자)` : '없음'}
{/* 알고리즘 설정 */}

📊 V자형 배치 알고리즘 설정

{[ { label: '해류 직교 보정', key: 'currentOrthogonalCorrection' as const, unit: '°', value: algorithmSettings.currentOrthogonalCorrection }, { label: '안전 마진 (도달시간)', key: 'safetyMarginMinutes' as const, unit: '분', value: algorithmSettings.safetyMarginMinutes }, { label: '최소 차단 효율', key: 'minContainmentEfficiency' as const, unit: '%', value: algorithmSettings.minContainmentEfficiency }, { label: '파고 보정 계수', key: 'waveHeightCorrectionFactor' as const, unit: 'x', value: algorithmSettings.waveHeightCorrectionFactor }, ].map((setting) => (
● {setting.label}
{ const val = parseFloat(e.target.value) || 0 onAlgorithmSettingsChange({ ...algorithmSettings, [setting.key]: val }) }} className="boom-setting-input" step={setting.key === 'waveHeightCorrectionFactor' ? 0.1 : 1} /> {setting.unit}
))}
{/* V자형 배치 + 시뮬레이션 실행 버튼 */}

확산 궤적을 분석하여 해류 직교 방향 1차 방어선(V형), U형 포위 2차 방어선, 연안 보호 3차 방어선을 자동 배치하고 차단 시뮬레이션을 실행합니다.

{/* 시뮬레이션 결과 */} {containmentResult && containmentResult.totalParticles > 0 && (
{/* 전체 효율 */}
{containmentResult.overallEfficiency}%
전체 차단 효율
{/* 차단/통과 카운트 */}
{containmentResult.blockedParticles}
차단 입자
{containmentResult.passedParticles}
통과 입자
{/* 효율 바 */}
= 80 ? 'var(--color-success)' : containmentResult.overallEfficiency >= 50 ? 'var(--color-warning)' : 'var(--color-danger)', }} />
{/* 라인별 분석 */}

라인별 차단 분석

{containmentResult.perLineResults.map((r) => (
{r.boomLineName} = 50 ? 'var(--color-success)' : 'var(--color-warning)', marginLeft: '8px' }} className="font-bold font-mono"> {r.blocked}차단 / {r.efficiency}%
))}
{/* 배치된 방어선 카드 */} {boomLines.map((line, idx) => { const priorityColor = line.priority === 'CRITICAL' ? 'var(--color-danger)' : line.priority === 'HIGH' ? 'var(--color-warning)' : 'var(--color-caution)' const priorityLabel = line.priority === 'CRITICAL' ? '긴급' : line.priority === 'HIGH' ? '중요' : '보통' return (
🛡 {idx + 1}차 방어선 ({line.type}) {priorityLabel}
길이
{line.length.toFixed(0)}m
각도
{line.angle.toFixed(0)}°
= 80 ? 'var(--color-success)' : 'var(--color-warning)' }} /> = 80 ? 'var(--color-success)' : 'var(--color-warning)' }} className="text-[9px] font-semibold"> 차단 효율 {line.efficiency}%
) })}
)} )}
)}
) } export default OilBoomSection