import { useState, useRef, useEffect, useCallback } from 'react'; import { stitchImages } from '../services/aerialApi'; import { analyzeImage } from '@tabs/prediction/services/predictionApi'; import { setPendingImageAnalysis } from '@common/utils/imageAnalysisSignal'; import { navigateToTab } from '@common/hooks/useSubMenu'; const MAX_IMAGES = 6; export function OilAreaAnalysis() { const [selectedFiles, setSelectedFiles] = useState([]); const [previewUrls, setPreviewUrls] = useState([]); const [stitchedBlob, setStitchedBlob] = useState(null); const [stitchedPreviewUrl, setStitchedPreviewUrl] = useState(null); const [isStitching, setIsStitching] = useState(false); const [isAnalyzing, setIsAnalyzing] = useState(false); const [error, setError] = useState(null); const fileInputRef = useRef(null); // Object URL 메모리 누수 방지 — 언마운트 시 전체 revoke useEffect(() => { return () => { previewUrls.forEach(url => URL.revokeObjectURL(url)); if (stitchedPreviewUrl) URL.revokeObjectURL(stitchedPreviewUrl); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleFileSelect = useCallback((e: React.ChangeEvent) => { setError(null); const incoming = Array.from(e.target.files ?? []); if (incoming.length === 0) return; setSelectedFiles(prev => { const merged = [...prev, ...incoming].slice(0, MAX_IMAGES); if (prev.length + incoming.length > MAX_IMAGES) { setError(`최대 ${MAX_IMAGES}장까지 선택할 수 있습니다.`); } return merged; }); // setSelectedFiles updater 밖에서 독립 호출 — updater 내부 side effect는 // React Strict Mode의 이중 호출로 인해 URL이 중복 생성되는 버그를 유발함 setPreviewUrls(prev => { const available = MAX_IMAGES - prev.length; const toAdd = incoming.slice(0, available); return [...prev, ...toAdd.map(f => URL.createObjectURL(f))]; }); // input 초기화 (동일 파일 재선택 허용) e.target.value = ''; }, []); const handleRemoveFile = useCallback((idx: number) => { setSelectedFiles(prev => prev.filter((_, i) => i !== idx)); setPreviewUrls(prev => { URL.revokeObjectURL(prev[idx]); return prev.filter((_, i) => i !== idx); }); // 합성 결과 초기화 (선택 파일이 바뀌었으므로) setStitchedBlob(null); if (stitchedPreviewUrl) { URL.revokeObjectURL(stitchedPreviewUrl); setStitchedPreviewUrl(null); } setError(null); }, [stitchedPreviewUrl]); const handleStitch = async () => { if (selectedFiles.length < 2) { setError('이미지를 2장 이상 선택해주세요.'); return; } setError(null); setIsStitching(true); try { const blob = await stitchImages(selectedFiles); if (stitchedPreviewUrl) URL.revokeObjectURL(stitchedPreviewUrl); setStitchedBlob(blob); setStitchedPreviewUrl(URL.createObjectURL(blob)); } catch (err) { const msg = err instanceof Error ? err.message : (err as { message?: string }).message ?? '이미지 합성에 실패했습니다.'; const status = err instanceof Error ? 0 : (err as { status?: number }).status ?? 0; setError(status === 504 ? '이미지 합성 서버 응답 시간이 초과되었습니다.' : msg); } finally { setIsStitching(false); } }; const handleAnalyze = async () => { if (!stitchedBlob) return; setError(null); setIsAnalyzing(true); try { const stitchedFile = new File([stitchedBlob], `stitch_${Date.now()}.jpg`, { type: 'image/jpeg' }); const result = await analyzeImage(stitchedFile); setPendingImageAnalysis({ ...result, autoRun: true }); navigateToTab('prediction', 'analysis'); } catch (err) { const msg = err instanceof Error ? err.message : '분석에 실패했습니다.'; setError(msg.includes('GPS') ? '이미지에 GPS 정보가 없습니다. GPS 정보가 포함된 이미지를 사용해주세요.' : msg); setIsAnalyzing(false); } }; const canStitch = selectedFiles.length >= 2 && !isStitching && !isAnalyzing; const canAnalyze = stitchedBlob !== null && !isStitching && !isAnalyzing; return (
{/* ── Left Panel ── */}
🧩 유출유면적분석
드론 사진을 합성하여 유출유 확산 면적과 기름 양을 산정합니다.
{/* 이미지 선택 버튼 */} {/* 선택된 이미지 목록 */} {selectedFiles.length > 0 && ( <>
선택된 이미지
{selectedFiles.map((file, i) => (
📷 {file.name}
))}
)} {/* 에러 메시지 */} {error && (
{error}
)} {/* 이미지 합성 버튼 */} {/* 분석 시작 버튼 */}
{/* ── Right Panel ── */}
{/* 3×2 이미지 그리드 */}
선택된 이미지 미리보기
{Array.from({ length: MAX_IMAGES }).map((_, i) => (
{previewUrls[i] ? ( {selectedFiles[i]?.name ) : (
{i + 1}
)}
))}
{/* 합성 결과 */}
합성 결과
{stitchedPreviewUrl ? ( 합성 결과 ) : (
{isStitching ? '⏳ 이미지를 합성하고 있습니다...' : '이미지를 선택하고 합성 버튼을 클릭하면\n합성 결과가 여기에 표시됩니다.'}
)}
); }