wing-ops/frontend/src/tabs/prediction/components/LeftPanel.tsx
jeonghyo.k 3946ff6a25 feat(prediction): 이미지 분석 서버 Docker 패키징 + DB 코드 제거
- prediction/image/ FastAPI 서버 Docker 환경 구성
  - Dockerfile: PyTorch 2.1 + CUDA 12.1 기반 GPU 이미지
  - docker-compose.yml: GPU 할당 + 데이터 볼륨 마운트
  - requirements.txt: 서버 의존성 목록
  - .env.example: 환경변수 템플릿
  - DOCKER_USAGE.md: 빌드/실행/API 사용법 문서
  - Dockerfile에 .dockerignore 제외 폴더 mkdir -p 추가
- .gitignore: prediction/image 결과물 및 모델 가중치(.pth) 제외 추가
- dbInsert_csv.py, dbInsert_shp.py 삭제 (미사용 DB 로직)
- api.py: dbInsert import 및 주석 처리된 DB 호출 코드 제거
- aerialRouter.ts: req.params 타입 오류 수정
2026-03-10 18:37:36 +09:00

211 lines
8.4 KiB
TypeScript
Executable File

import { useState } from 'react'
import type { LeftPanelProps, ExpandedSections } from './leftPanelTypes'
import PredictionInputSection from './PredictionInputSection'
import InfoLayerSection from './InfoLayerSection'
import OilBoomSection from './OilBoomSection'
export type { LeftPanelProps }
export function LeftPanel({
selectedAnalysis,
enabledLayers,
onToggleLayer,
accidentTime,
onAccidentTimeChange,
incidentCoord,
onCoordChange,
isSelectingLocation,
onMapSelectClick,
onRunSimulation,
isRunningSimulation,
selectedModels,
onModelsChange,
predictionTime,
onPredictionTimeChange,
spillType,
onSpillTypeChange,
oilType,
onOilTypeChange,
spillAmount,
onSpillAmountChange,
incidentName,
onIncidentNameChange,
spillUnit,
onSpillUnitChange,
boomLines,
onBoomLinesChange,
oilTrajectory,
algorithmSettings,
onAlgorithmSettingsChange,
isDrawingBoom,
onDrawingBoomChange,
drawingPoints,
onDrawingPointsChange,
containmentResult,
onContainmentResultChange,
layerOpacity,
onLayerOpacityChange,
layerBrightness,
onLayerBrightnessChange,
onImageAnalysisResult,
}: LeftPanelProps) {
const [expandedSections, setExpandedSections] = useState<ExpandedSections>({
predictionInput: true,
incident: false,
impactResources: false,
infoLayer: true,
oilBoom: false,
})
const toggleSection = (section: keyof ExpandedSections) => {
setExpandedSections(prev => ({
...prev,
[section]: !prev[section]
}))
}
return (
<div className="w-80 min-w-[320px] bg-bg-1 border-r border-border flex flex-col">
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent">
{/* Prediction Input Section */}
<PredictionInputSection
expanded={expandedSections.predictionInput}
onToggle={() => toggleSection('predictionInput')}
accidentTime={accidentTime}
onAccidentTimeChange={onAccidentTimeChange}
incidentCoord={incidentCoord}
onCoordChange={onCoordChange}
isSelectingLocation={isSelectingLocation}
onMapSelectClick={onMapSelectClick}
onRunSimulation={onRunSimulation}
isRunningSimulation={isRunningSimulation}
selectedModels={selectedModels}
onModelsChange={onModelsChange}
predictionTime={predictionTime}
onPredictionTimeChange={onPredictionTimeChange}
spillType={spillType}
onSpillTypeChange={onSpillTypeChange}
oilType={oilType}
onOilTypeChange={onOilTypeChange}
spillAmount={spillAmount}
onSpillAmountChange={onSpillAmountChange}
incidentName={incidentName}
onIncidentNameChange={onIncidentNameChange}
spillUnit={spillUnit}
onSpillUnitChange={onSpillUnitChange}
onImageAnalysisResult={onImageAnalysisResult}
/>
{/* Incident Section */}
<div className="border-b border-border">
<div
onClick={() => toggleSection('incident')}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-[rgba(255,255,255,0.02)]"
>
<h3 className="text-[13px] font-bold text-text-2 font-korean">
</h3>
<span className="text-[10px] text-text-3">
{expandedSections.incident ? '▼' : '▶'}
</span>
</div>
{expandedSections.incident && (
<div className="px-4 pb-4 space-y-3">
{/* Status Badge */}
<div className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[9px] font-semibold bg-[rgba(239,68,68,0.15)] text-status-red border border-[rgba(239,68,68,0.3)]">
<span className="w-1.5 h-1.5 rounded-full bg-status-red animate-pulse" />
</div>
{/* Info Grid */}
<div className="grid gap-1">
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-mono">{selectedAnalysis ? `INC-2025-${String(selectedAnalysis.id).padStart(4, '0')}` : 'INC-2025-0042'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-korean">{selectedAnalysis?.name || '씨프린스호'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-mono">{selectedAnalysis?.occurredAt || '2025-02-10 06:30'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-korean">{selectedAnalysis?.oilType || 'BUNKER_C'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-mono">{selectedAnalysis ? `${selectedAnalysis.volume.toFixed(2)} kl` : '350.00 kl'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-korean">{selectedAnalysis?.analyst || '남해청, 방재과'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-status-orange font-semibold font-korean">{selectedAnalysis?.location || '여수 돌산 남방 5NM'}</span>
</div>
</div>
</div>
)}
</div>
{/* Impact Resources Section */}
<div className="border-b border-border">
<div
onClick={() => toggleSection('impactResources')}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-[rgba(255,255,255,0.02)]"
>
<h3 className="text-[13px] font-bold text-text-2 font-korean">
</h3>
<span className="text-[10px] text-text-3">
{expandedSections.impactResources ? '▼' : '▶'}
</span>
</div>
{expandedSections.impactResources && (
<div className="px-4 pb-4">
<p className="text-[11px] text-text-3"> </p>
</div>
)}
</div>
{/* Info Layer Section */}
<InfoLayerSection
expanded={expandedSections.infoLayer}
onToggle={() => toggleSection('infoLayer')}
enabledLayers={enabledLayers}
onToggleLayer={onToggleLayer}
layerOpacity={layerOpacity}
onLayerOpacityChange={onLayerOpacityChange}
layerBrightness={layerBrightness}
onLayerBrightnessChange={onLayerBrightnessChange}
/>
{/* Oil Boom Placement Guide Section */}
<OilBoomSection
expanded={expandedSections.oilBoom}
onToggle={() => toggleSection('oilBoom')}
boomLines={boomLines}
onBoomLinesChange={onBoomLinesChange}
oilTrajectory={oilTrajectory}
incidentCoord={incidentCoord ?? { lat: 0, lon: 0 }}
algorithmSettings={algorithmSettings}
onAlgorithmSettingsChange={onAlgorithmSettingsChange}
isDrawingBoom={isDrawingBoom}
onDrawingBoomChange={onDrawingBoomChange}
drawingPoints={drawingPoints}
onDrawingPointsChange={onDrawingPointsChange}
containmentResult={containmentResult}
onContainmentResultChange={onContainmentResultChange}
/>
</div>
</div>
)
}