import { useState, useRef } from 'react' import { decimalToDMS } from '@common/utils/coordinates' import { ComboBox } from '@common/components/ui/ComboBox' import { ALL_MODELS } from './OilSpillView' import type { PredictionModel } from './OilSpillView' import { analyzeImage } from '../services/predictionApi' import type { ImageAnalyzeResult } from '../services/predictionApi' interface PredictionInputSectionProps { expanded: boolean onToggle: () => void accidentTime: string onAccidentTimeChange: (time: string) => void incidentCoord: { lon: number; lat: number } | null onCoordChange: (coord: { lon: number; lat: number }) => void isSelectingLocation: boolean onMapSelectClick: () => void onRunSimulation: () => void isRunningSimulation: boolean selectedModels: Set onModelsChange: (models: Set) => void predictionTime: number onPredictionTimeChange: (time: number) => void spillType: string onSpillTypeChange: (type: string) => void oilType: string onOilTypeChange: (type: string) => void spillAmount: number onSpillAmountChange: (amount: number) => void incidentName: string onIncidentNameChange: (name: string) => void spillUnit: string onSpillUnitChange: (unit: string) => void onImageAnalysisResult?: (result: ImageAnalyzeResult) => void } const PredictionInputSection = ({ expanded, onToggle, accidentTime, onAccidentTimeChange, incidentCoord, onCoordChange, isSelectingLocation, onMapSelectClick, onRunSimulation, isRunningSimulation, selectedModels, onModelsChange, predictionTime, onPredictionTimeChange, spillType, onSpillTypeChange, oilType, onOilTypeChange, spillAmount, onSpillAmountChange, incidentName, onIncidentNameChange, spillUnit, onSpillUnitChange, onImageAnalysisResult, }: PredictionInputSectionProps) => { const [inputMode, setInputMode] = useState<'direct' | 'upload'>('direct') const [uploadedFile, setUploadedFile] = useState(null) const [isAnalyzing, setIsAnalyzing] = useState(false) const [analyzeError, setAnalyzeError] = useState(null) const [analyzeResult, setAnalyzeResult] = useState(null) const fileInputRef = useRef(null) const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0] ?? null setUploadedFile(file) setAnalyzeError(null) setAnalyzeResult(null) } const handleRemoveFile = () => { setUploadedFile(null) setAnalyzeError(null) setAnalyzeResult(null) if (fileInputRef.current) fileInputRef.current.value = '' } const handleAnalyze = async () => { if (!uploadedFile) return setIsAnalyzing(true) setAnalyzeError(null) try { const result = await analyzeImage(uploadedFile) setAnalyzeResult(result) onImageAnalysisResult?.(result) } catch (err: unknown) { if (err && typeof err === 'object' && 'response' in err) { const res = (err as { response?: { data?: { error?: string } } }).response if (res?.data?.error === 'GPS_NOT_FOUND') { setAnalyzeError('GPS 정보가 없는 이미지입니다') return } if (res?.data?.error === 'TIMEOUT') { setAnalyzeError('분석 서버 응답 없음 (시간 초과)') return } } setAnalyzeError('이미지 분석 중 오류가 발생했습니다') } finally { setIsAnalyzing(false) } } return (

예측정보 입력

{expanded ? '▼' : '▶'}
{expanded && (
{/* Input Mode Selection */}
{/* Direct Input Mode */} {inputMode === 'direct' && ( <> onIncidentNameChange(e.target.value)} /> )} {/* Image Upload Mode */} {inputMode === 'upload' && ( <> {/* 파일 선택 영역 */} {!uploadedFile ? ( ) : (
📄 {uploadedFile.name}
)} {/* 분석 실행 버튼 */} {/* 에러 메시지 */} {analyzeError && (
⚠ {analyzeError}
)} {/* 분석 완료 메시지 */} {analyzeResult && (
✓ 분석 완료
위도 {analyzeResult.lat.toFixed(4)} / 경도 {analyzeResult.lon.toFixed(4)}
유종: {analyzeResult.oilType} / 면적: {analyzeResult.area.toFixed(1)} m²
)} )} {/* 사고 발생 시각 */}
onAccidentTimeChange(e.target.value)} style={{ colorScheme: 'dark' }} />
{/* Coordinates + Map Button */}
{ const value = e.target.value === '' ? 0 : parseFloat(e.target.value) onCoordChange({ lon: incidentCoord?.lon ?? 0, lat: isNaN(value) ? 0 : value }) }} placeholder="위도°" /> { const value = e.target.value === '' ? 0 : parseFloat(e.target.value) onCoordChange({ lat: incidentCoord?.lat ?? 0, lon: isNaN(value) ? 0 : value }) }} placeholder="경도°" />
{/* 도분초 표시 */} {incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && (
{decimalToDMS(incidentCoord.lat, true)} / {decimalToDMS(incidentCoord.lon, false)}
)}
{/* Oil Type + Oil Kind */}
{/* Volume + Unit + Duration */}
onSpillAmountChange(parseInt(e.target.value) || 0)} /> onPredictionTimeChange(parseInt(v))} options={[ { value: '6', label: '6시간' }, { value: '12', label: '12시간' }, { value: '24', label: '24시간' }, { value: '48', label: '48시간' }, { value: '72', label: '72시간' } ]} />
{/* Divider */}
{/* Model Selection (다중 선택) */}
{([ { id: 'KOSPS' as PredictionModel, color: 'var(--cyan)' }, { id: 'POSEIDON' as PredictionModel, color: 'var(--red)' }, { id: 'OpenDrift' as PredictionModel, color: 'var(--blue)' }, ] as const).map(m => (
{ const next = new Set(selectedModels) if (next.has(m.id)) { next.delete(m.id) } else { next.add(m.id) } onModelsChange(next) }} > {m.id}
))}
{ if (selectedModels.size === ALL_MODELS.length) { onModelsChange(new Set(['KOSPS'])) } else { onModelsChange(new Set(ALL_MODELS)) } }} > 앙상블
{/* Run Button */}
)}
) } export default PredictionInputSection