diff --git a/frontend/src/tabs/prediction/components/OilBoomSection.tsx b/frontend/src/tabs/prediction/components/OilBoomSection.tsx index 39f72f8..c01dde6 100644 --- a/frontend/src/tabs/prediction/components/OilBoomSection.tsx +++ b/frontend/src/tabs/prediction/components/OilBoomSection.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import type { BoomLine, BoomLineCoord, AlgorithmSettings, ContainmentResult } from '@common/types/boomLine' -import { generateAIBoomLines, runContainmentAnalysis, computePolylineLength, computeBearing } from '@common/utils/geo' +import { generateAIBoomLines, runContainmentAnalysis } from '@common/utils/geo' interface OilBoomSectionProps { expanded: boolean @@ -19,6 +19,13 @@ interface OilBoomSectionProps { onContainmentResultChange: (result: ContainmentResult | null) => void } +const DEFAULT_SETTINGS: AlgorithmSettings = { + currentOrthogonalCorrection: 15, + safetyMarginMinutes: 60, + minContainmentEfficiency: 80, + waveHeightCorrectionFactor: 1.0, +} + const OilBoomSection = ({ expanded, onToggle, @@ -28,14 +35,40 @@ const OilBoomSection = ({ incidentCoord, algorithmSettings, onAlgorithmSettingsChange, - isDrawingBoom, onDrawingBoomChange, - drawingPoints, onDrawingPointsChange, containmentResult, onContainmentResultChange, }: OilBoomSectionProps) => { - const [boomPlacementTab, setBoomPlacementTab] = useState<'ai' | 'manual' | 'simulation'>('simulation') + 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 (
@@ -54,23 +87,28 @@ const OilBoomSection = ({ {expanded && (
- {/* Tab Buttons + Reset */} + {/* 탭 버튼 + 초기화 */}
{[ { id: 'ai' as const, label: 'AI 자동 추천' }, - { id: 'manual' as const, label: '수동 배치' }, - { id: 'simulation' as const, label: '시뮬레이션' } + { id: 'simulation' as const, label: '시뮬레이션' }, ].map(tab => ( ))}
- {/* Key Metrics (동적) */} + {/* 초기화 확인 팝업 */} + {showResetConfirm && ( +
+
+ ⚠ 오일펜스 배치 가이드를 초기화 합니다 +
+
+ 배치된 오일펜스 라인과 시뮬레이션 결과가 삭제됩니다. 확산 예측 결과는 유지됩니다. +
+
+ + +
+
+ )} + + {/* Key Metrics */}
{[ { value: String(boomLines.length), label: '배치 라인', color: 'var(--orange)' }, { value: boomLines.length > 0 ? `${(boomLines.reduce((s, l) => s + l.length, 0) / 1000).toFixed(1)}km` : '0km', label: '총 길이', color: 'var(--cyan)' }, - { value: boomLines.length > 0 ? `${Math.round(boomLines.reduce((s, l) => s + l.efficiency, 0) / boomLines.length)}%` : '—', label: '평균 효율', color: 'var(--orange)' } + { value: boomLines.length > 0 ? `${Math.round(boomLines.reduce((s, l) => s + l.efficiency, 0) / boomLines.length)}%` : '—', label: '평균 효율', color: 'var(--orange)' }, ].map((metric, idx) => (
{metric.value} @@ -129,61 +203,24 @@ const OilBoomSection = ({ ))}
- {/* ===== AI 자동 추천 탭 ===== */} - {boomPlacementTab === 'ai' && ( + {/* ===== 시뮬레이션 탭 ===== */} + {boomPlacementTab === 'simulation' && ( <> -
-
- 0 ? 'var(--green)' : 'var(--t3)' }} /> - 0 ? 'var(--green)' : 'var(--t3)' }} className="text-[10px] font-bold"> - {oilTrajectory.length > 0 ? '확산 데이터 준비 완료' : '확산 예측을 먼저 실행하세요'} + {/* 전제조건 체크 */} +
+
+ 0 ? 'var(--green)' : 'var(--red)' }} /> + 0 ? 'var(--green)' : 'var(--t3)' }}> + 확산 궤적 데이터 {oilTrajectory.length > 0 ? `(${oilTrajectory.length}개 입자)` : '없음'}
- -

- 확산 예측 기반 최적 배치안 -

- -

- {oilTrajectory.length > 0 - ? '확산 궤적을 분석하여 해류 직교 방향 1차 방어선, U형 포위 2차 방어선, 연안 보호 3차 방어선을 자동 생성합니다.' - : '상단에서 확산 예측을 실행한 뒤 AI 배치를 적용할 수 있습니다.' - } -

- -
{/* 알고리즘 설정 */}

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

{[ @@ -194,7 +231,7 @@ const OilBoomSection = ({ ].map((setting) => (
● {setting.label}
@@ -214,227 +251,50 @@ const OilBoomSection = ({ ))}
- - )} - {/* ===== 수동 배치 탭 ===== */} - {boomPlacementTab === 'manual' && ( - <> - {/* 드로잉 컨트롤 */} -
- {!isDrawingBoom ? ( - - ) : ( - <> - - - - )} -
- - {/* 드로잉 실시간 정보 */} - {isDrawingBoom && drawingPoints.length > 0 && ( -
- 포인트: {drawingPoints.length} - 길이: {computePolylineLength(drawingPoints).toFixed(0)}m - {drawingPoints.length >= 2 && ( - 방위각: {computeBearing(drawingPoints[0], drawingPoints[drawingPoints.length - 1]).toFixed(0)}° - )} -
- )} - - {/* 배치된 라인 목록 */} - {boomLines.length === 0 ? ( -

- 배치된 오일펜스 라인이 없습니다. -

- ) : ( - boomLines.map((line, idx) => ( -
-
- { - const updated = [...boomLines] - updated[idx] = { ...updated[idx], name: e.target.value } - onBoomLinesChange(updated) - }} - className="flex-1 text-[11px] font-bold bg-transparent border-none outline-none" - /> - -
-
-
- 길이 -
{line.length.toFixed(0)}m
-
-
- 각도 -
{line.angle.toFixed(0)}°
-
-
- 우선순위 - -
-
-
- )) - )} - - )} - - {/* ===== 시뮬레이션 탭 ===== */} - {boomPlacementTab === 'simulation' && ( - <> - {/* 전제조건 체크 */} -
-
- 0 ? 'var(--green)' : 'var(--red)' }} /> - 0 ? 'var(--green)' : 'var(--t3)' }}> - 확산 궤적 데이터 {oilTrajectory.length > 0 ? `(${oilTrajectory.length}개 입자)` : '없음'} - -
-
- 0 ? 'var(--green)' : 'var(--red)' }} /> - 0 ? 'var(--green)' : 'var(--t3)' }}> - 오일펜스 라인 {boomLines.length > 0 ? `(${boomLines.length}개 배치)` : '없음'} - -
-
- - {/* 실행 버튼 */} + {/* V자형 배치 + 시뮬레이션 실행 버튼 */} +

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

+ {/* 시뮬레이션 결과 */} {containmentResult && containmentResult.totalParticles > 0 && (
{/* 전체 효율 */}
{containmentResult.overallEfficiency}%
-
- 전체 차단 효율 -
+
전체 차단 효율
{/* 차단/통과 카운트 */}
-
- {containmentResult.blockedParticles} -
+
{containmentResult.blockedParticles}
차단 입자
-
- {containmentResult.passedParticles} -
+
{containmentResult.passedParticles}
통과 입자
@@ -443,20 +303,16 @@ const OilBoomSection = ({
= 80 ? 'var(--green)' : containmentResult.overallEfficiency >= 50 ? 'var(--orange)' : 'var(--red)' + background: containmentResult.overallEfficiency >= 80 ? 'var(--green)' : containmentResult.overallEfficiency >= 50 ? 'var(--orange)' : 'var(--red)', }} />
{/* 라인별 분석 */}
-

- 라인별 차단 분석 -

+

라인별 차단 분석

{containmentResult.perLineResults.map((r) => ( -
+
{r.boomLineName} = 50 ? 'var(--green)' : 'var(--orange)', marginLeft: '8px' }} className="font-bold font-mono"> {r.blocked}차단 / {r.efficiency}% @@ -464,60 +320,52 @@ const OilBoomSection = ({
))}
+ + {/* 배치된 방어선 카드 */} + {boomLines.map((line, idx) => { + const priorityColor = line.priority === 'CRITICAL' ? 'var(--red)' : line.priority === 'HIGH' ? 'var(--orange)' : 'var(--yellow)' + 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(--green)' : 'var(--orange)' }} /> + = 80 ? 'var(--green)' : 'var(--orange)' }} className="text-[9px] font-semibold"> + 차단 효율 {line.efficiency}% + +
+
+ ) + })}
)} )} - {/* 배치된 방어선 카드 (AI/수동 공통 표시) */} - {boomPlacementTab !== 'simulation' && boomLines.length > 0 && boomPlacementTab === 'ai' && ( - <> - {boomLines.map((line, idx) => { - const priorityColor = line.priority === 'CRITICAL' ? 'var(--red)' : line.priority === 'HIGH' ? 'var(--orange)' : 'var(--yellow)' - 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(--green)' : 'var(--orange)' }} /> - = 80 ? 'var(--green)' : 'var(--orange)' }} className="text-[9px] font-semibold"> - 차단 효율 {line.efficiency}% - -
-
- ) - })} - - )} -
)}