- 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 타입 오류 수정
211 lines
8.4 KiB
TypeScript
Executable File
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>
|
|
)
|
|
}
|