import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { Button } from '~/shared/components/button/Button'; import { Modal, ModalRegion } from '~/shared/components/modal'; import { Select, type SelectOption } from '~/shared/components/select/Select'; import { Tree, type TreeItemType, type TreeRef } from '~/shared/components/tree/Tree'; import { fetchFolderList, fetchRegions } from '../api/imagery'; import { useAerialChunkUpload, type UploadingFile } from '../hooks/useAerialChunkUpload'; type AerialRegisterModalProps = { isOpen: boolean; onClose: () => void; onUploadSuccess?: () => void; }; type FormValues = { type: string; region: string; folderPath: string; }; const TYPE_OPTIONS: SelectOption[] = [ { value: 'mapFrame', label: '도곽' }, ]; const formatFileSize = (bytes: number) => { if (bytes === 0) return '0B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`; }; const ProgressBar = ({ progress }: { progress: number }) => { return (
); }; const FileItem = ({ file, folderPath, onRemove, }: { file: UploadingFile; folderPath: string; onRemove: (id: string) => void; }) => { const fullPath = folderPath ? `${folderPath}/${file.fileName}` : file.fileName; return (
{fullPath}
); }; export const AerialRegisterModal = ({ isOpen, onClose, onUploadSuccess }: AerialRegisterModalProps) => { const treeRef = useRef(null); const fileInputRef = useRef(null); const { control, setValue, watch, reset: resetForm } = useForm({ defaultValues: { type: 'mapFrame', region: '', folderPath: '', }, }); const selectedRegion = watch('region'); const selectedFolder = watch('folderPath'); const [regions, setRegions] = useState([]); const [treeItems, setTreeItems] = useState([]); const [isLoadingFolders, setIsLoadingFolders] = useState(false); const { files, addFiles, startUpload, removeFile, reset: resetUpload, isUploading, isAllCompleted, hasFiles, totalSize, uploadedSize, overallProgress, } = useAerialChunkUpload(); const memorizedSelectedKeys = useMemo( () => new Set(selectedFolder ? [selectedFolder] : []), [selectedFolder] ); // 지역 목록 조회 useEffect(() => { const loadRegions = async () => { try { const data = await fetchRegions(); setRegions(data.map((r) => ({ value: r.code, label: r.name }))); } catch (error) { console.error('Failed to load regions:', error); } }; if (isOpen) { loadRegions(); } }, [isOpen]); // 지역 선택 시 폴더 목록 조회 useEffect(() => { if (!selectedRegion) { setTreeItems([]); return; } const loadFolders = async () => { setIsLoadingFolders(true); try { const data = await fetchFolderList(selectedRegion); const items: TreeItemType[] = data.folders.map((folder) => ({ id: folder.fullPath, title: folder.folderNm, type: 'directory', })); setTreeItems(items); } catch (error) { console.error('Failed to load folders:', error); } finally { setIsLoadingFolders(false); } }; loadFolders(); }, [selectedRegion]); const handleExpandFolder = async (key: string | number) => { const folderPath = String(key); try { const data = await fetchFolderList(folderPath); data.folders.forEach((folder) => { const newItem: TreeItemType = { id: folder.fullPath, title: folder.folderNm, type: 'directory', }; treeRef.current?.addItem(newItem, folderPath); }); } catch (error) { console.error('Failed to load subfolders:', error); } }; const handleCollapseFolder = (key: string | number) => { treeRef.current?.clearItemsByParentKey(key); }; const handleFileChange = (e: ChangeEvent) => { const selectedFiles = e.target.files; if (!selectedFiles || selectedFiles.length === 0) return; const fileArray = Array.from(selectedFiles); const addedFiles = addFiles(fileArray); // 파일 선택 즉시 업로드 시작 startUpload(addedFiles); // input 초기화 (같은 파일 재선택 가능하도록) if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleUpload = () => { if (!selectedFolder) { ModalRegion.alert({ content: '업로드할 폴더를 선택해주세요.' }); return; } if (!hasFiles) { ModalRegion.alert({ content: '업로드할 파일을 선택해주세요.' }); return; } if (isUploading) { ModalRegion.alert({ content: '파일 업로드가 진행 중입니다.' }); return; } if (isAllCompleted) { ModalRegion.alert({ content: '파일 업로드가 완료되었습니다.', onCancel: () => { onUploadSuccess?.(); handleClose(); }, }); } }; const handleClose = () => { resetUpload(); resetForm(); setTreeItems([]); onClose(); }; const handleCancel = () => { if (isUploading || hasFiles) { ModalRegion.confirm({ content: '업로드를 취소하시겠습니까?\n진행 중인 업로드가 중단됩니다.', confirmText: '예', cancelText: '아니오', onConfirm: (close) => { close(); handleClose(); }, }); } else { handleClose(); } }; const currentUploadingFile = files.find((f) => f.status === 'uploading'); return ( {() => (
{ e.preventDefault(); handleUpload(); }}> 업로드 폴더 선택
{/* 타입 선택 */}
( { onChange(v); setValue('folderPath', ''); }} placeholder="지역선택" className="w-40" /> )} />
{/* 폴더 선택 */}
{isLoadingFolders ? (
로딩 중...
) : treeItems.length > 0 ? ( ( { handleExpandFolder(key); onChange(String(key)); }} onCollapse={(key) => { handleCollapseFolder(key); }} onSelect={(key) => { if (key) onChange(String(key)); }} enableDragAndDrop={false} persistSelectionOnCollapse /> )} /> ) : selectedRegion ? (
폴더가 없습니다.
) : (
지역을 선택해주세요.
)}

※ 업로드 대상 폴더를 정확하게 지정하시기 바랍니다.

{/* 파일명 */}
{files.length > 0 ? ( files.map((file) => ( )) ) : ( 파일이 업로드 되면 자동 입력됩니다. )}
{/* 파일 선택 */}
{currentUploadingFile?.fileName || ''}
{/* 진행률 표시 */} {hasFiles && (
{overallProgress}%
)} {hasFiles && (
Loading {formatFileSize(uploadedSize)} / {formatFileSize(totalSize)}
)}
)}
); };