import { useState, useCallback, useRef, useEffect } from 'react'; import { fetchAerialMedia, downloadAerialMedia, getAerialMediaViewUrl, uploadAerialMedia, } from '../services/aerialApi'; import type { AerialMediaItem } from '../services/aerialApi'; import { navigateToTab } from '@common/hooks/useSubMenu'; // ── Helpers ── function formatDtm(dtm: string | null): string { if (!dtm) return '—'; const d = new Date(dtm); return d.toISOString().slice(0, 16).replace('T', ' '); } const equipIcon = (t: string) => (t === 'drone' ? '🛸' : t === 'plane' ? '✈' : '🛰'); const equipTagCls = () => 'text-fg'; const mediaTagCls = () => 'text-fg'; const FilterBtn = ({ label, active, onClick, }: { label: string; active: boolean; onClick: () => void; }) => ( ); // ── Component ── export function MediaManagement() { const [mediaItems, setMediaItems] = useState([]); const [loading, setLoading] = useState(true); const [selectedIds, setSelectedIds] = useState>(new Set()); const [equipFilter, setEquipFilter] = useState('all'); const [typeFilter, setTypeFilter] = useState>(new Set()); const [searchTerm, setSearchTerm] = useState(''); const [sortBy, setSortBy] = useState('latest'); const [showUpload, setShowUpload] = useState(false); const [downloadingId, setDownloadingId] = useState(null); const [bulkDownloading, setBulkDownloading] = useState(false); const [downloadResult, setDownloadResult] = useState<{ total: number; success: number } | null>( null, ); const [previewItem, setPreviewItem] = useState(null); const modalRef = useRef(null); const previewRef = useRef(null); const fileInputRef = useRef(null); const [uploadFile, setUploadFile] = useState(null); const [uploading, setUploading] = useState(false); const [dragOver, setDragOver] = useState(false); // const [uploadEquip, setUploadEquip] = useState('drone'); // const [uploadEquipNm, setUploadEquipNm] = useState('드론 (DJI M300 RTK)'); // const [uploadMemo, setUploadMemo] = useState(''); const loadData = useCallback(async () => { setLoading(true); try { const items = await fetchAerialMedia(); setMediaItems(items); } catch (err) { console.error('[aerial] 미디어 목록 조회 실패:', err); } finally { setLoading(false); } }, []); useEffect(() => { loadData(); }, [loadData]); useEffect(() => { const handler = (e: MouseEvent) => { if (modalRef.current && !modalRef.current.contains(e.target as Node)) { setShowUpload(false); } }; if (showUpload) document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [showUpload]); useEffect(() => { if (!previewItem) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setPreviewItem(null); }; document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [previewItem]); const filtered = mediaItems.filter((f) => { if (equipFilter !== 'all' && f.equipTpCd !== equipFilter) return false; if (typeFilter.size > 0) { const isPhoto = f.mediaTpCd !== '영상'; const isVideo = f.mediaTpCd === '영상'; if (typeFilter.has('photo') && !isPhoto) return false; if (typeFilter.has('video') && !isVideo) return false; } if (searchTerm && !f.fileNm.toLowerCase().includes(searchTerm.toLowerCase())) return false; return true; }); const sorted = [...filtered].sort((a, b) => { if (sortBy === 'name') return a.fileNm.localeCompare(b.fileNm); if (sortBy === 'size') return parseFloat(b.fileSz ?? '0') - parseFloat(a.fileSz ?? '0'); return (b.takngDtm ?? '').localeCompare(a.takngDtm ?? ''); }); const toggleId = (id: number) => { setSelectedIds((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const toggleAll = () => { if (selectedIds.size === sorted.length) { setSelectedIds(new Set()); } else { setSelectedIds(new Set(sorted.map((f) => f.aerialMediaSn))); } }; const toggleTypeFilter = (t: string) => { setTypeFilter((prev) => { const next = new Set(prev); if (next.has(t)) { next.delete(t); } else { next.add(t); } return next; }); }; const handleBulkDownload = async () => { if (bulkDownloading || selectedIds.size === 0) return; setBulkDownloading(true); let success = 0; const total = selectedIds.size; for (const sn of selectedIds) { const item = mediaItems.find((f) => f.aerialMediaSn === sn); if (!item) continue; try { await downloadAerialMedia(sn, item.orgnlNm ?? item.fileNm); success++; } catch { // 실패 건 스킵 } } setBulkDownloading(false); setDownloadResult({ total, success }); }; const handleDownload = async (e: React.MouseEvent, item: AerialMediaItem) => { e.stopPropagation(); if (downloadingId !== null) return; setDownloadingId(item.aerialMediaSn); try { await downloadAerialMedia(item.aerialMediaSn, item.orgnlNm ?? item.fileNm); } catch { alert('다운로드 실패: 이미지를 찾을 수 없습니다.'); } finally { setDownloadingId(null); } }; const handleUploadSubmit = async () => { if (!uploadFile || uploading) return; setUploading(true); try { await uploadAerialMedia(uploadFile, { // equipTpCd: uploadEquip, // equipNm: uploadEquipNm, // memo: uploadMemo, }); setShowUpload(false); setUploadFile(null); // setUploadMemo(''); await loadData(); } catch { alert('업로드 실패: 다시 시도해주세요.'); } finally { setUploading(false); } }; const handleFileDrop = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); const file = e.dataTransfer.files[0]; if (file) setUploadFile(file); }; const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) setUploadFile(file); }; const droneCount = mediaItems.filter((f) => f.equipTpCd === 'drone').length; const planeCount = mediaItems.filter((f) => f.equipTpCd === 'plane').length; const satCount = mediaItems.filter((f) => f.equipTpCd === 'satellite').length; return (
{/* Filters */}
촬영 장비: setEquipFilter('all')} /> setEquipFilter('drone')} /> setEquipFilter('plane')} /> setEquipFilter('satellite')} /> 유형: toggleTypeFilter('photo')} /> toggleTypeFilter('video')} />
setSearchTerm(e.target.value)} className="px-3 py-1.5 bg-bg-base border border-stroke rounded-sm text-fg font-korean text-label-2 outline-none w-40 focus:border-color-accent" />
{/* Summary Stats */}
{[ { icon: '📸', value: loading ? '…' : String(mediaItems.length), label: '총 파일', color: 'text-fg', }, { icon: '🛸', value: loading ? '…' : String(droneCount), label: '드론', color: 'text-fg', }, { icon: '✈', value: loading ? '…' : String(planeCount), label: '유인항공기', color: 'text-fg', }, { icon: '🛰', value: loading ? '…' : String(satCount), label: '위성', color: 'text-fg' }, { icon: '💾', value: '—', label: '총 용량', color: 'text-fg' }, ].map((s, i) => (
{s.icon}
{s.value}
{s.label}
))}
{/* File Table */}
{loading ? ( ) : ( sorted.map((f) => { const isPhoto = f.mediaTpCd !== '영상'; return ( ); }) )}
0} onChange={toggleAll} className="accent-primary-blue" /> 사고명 위치 파일명 장비 유형 촬영일시 용량 해상도 다운로드
불러오는 중...
toggleId(f.aerialMediaSn)} > toggleId(f.aerialMediaSn)} onClick={(e) => e.stopPropagation()} className="accent-primary-blue" /> {equipIcon(f.equipTpCd)} {f.acdntSn != null ? String(f.acdntSn) : '—'} {f.locDc ?? '—'} {f.fileNm} {f.equipNm} {isPhoto ? ( ) : ( 🎬 {f.mediaTpCd} )} {formatDtm(f.takngDtm)} {f.fileSz ?? '—'} {f.resolution ?? '—'}
{/* Bottom Actions */}
선택된 파일: {selectedIds.size}
{/* 선택 다운로드 결과 팝업 */} {downloadResult && (
📥
다운로드 완료
{downloadResult.total}건 선택
{downloadResult.success}건 다운로드 성공 {downloadResult.total - downloadResult.success > 0 && ( <> {' '} /{' '} {downloadResult.total - downloadResult.success} 건 실패 )}
)} {/* Upload Modal */} {showUpload && (
영상·사진 업로드
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleFileDrop} onClick={() => fileInputRef.current?.click()} className={`border-2 border-dashed rounded-md py-8 px-4 text-center mb-4 cursor-pointer transition-colors ${ dragOver ? 'border-color-accent bg-[rgba(6,182,212,0.06)]' : uploadFile ? 'border-[rgba(6,182,212,0.3)] bg-[rgba(6,182,212,0.04)]' : 'border-stroke-light hover:border-[rgba(6,182,212,0.4)]' }`} > {uploadFile ? ( <>
{uploadFile.name}
{(uploadFile.size / (1024 * 1024)).toFixed(2)} MB · 클릭하여 변경
) : ( <>
📁
파일을 드래그하거나 클릭하여 업로드
JPG, TIFF, GeoTIFF, MP4, MOV 지원 · 최대 2GB
)}
{/*