diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bfd6abc..8b2c620 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,14 +2,18 @@ import { RouterProvider } from 'react-router-dom'; import router from './router'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from './components/ui/toaster'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; const queryClient = new QueryClient(); function App() { return ( - - + + + + ); } diff --git a/frontend/src/components/ImagePreSignedForm/index.tsx b/frontend/src/components/ImagePreSignedForm/index.tsx index 9cd6ae9..1e4ed02 100644 --- a/frontend/src/components/ImagePreSignedForm/index.tsx +++ b/frontend/src/components/ImagePreSignedForm/index.tsx @@ -5,6 +5,7 @@ import useAuthStore from '@/stores/useAuthStore'; import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react'; import { FixedSizeList } from 'react-window'; import useUploadFiles from '@/hooks/useUploadFiles'; +import useUploadImagePresignedQuery from '@/queries/images/useUploadImagePresignedQuery'; import { unzipFilesWithPath, extractFilesRecursivelyWithPath } from '@/utils/fileUtils'; interface ImagePreSignedFormProps { @@ -34,9 +35,11 @@ export default function ImagePreSignedForm({ const [isUploaded, setIsUploaded] = useState(false); const [isFailed, setIsFailed] = useState(false); const [progress, setProgress] = useState(0); + const [uploadStatus, setUploadStatus] = useState<(boolean | null)[]>([]); + // Ensure to destructure the uploadFiles function properly from the hook const { uploadFiles } = useUploadFiles(); - + const uploadImageFile = useUploadImagePresignedQuery(); const handleClose = () => { onClose(); setFiles([]); @@ -45,6 +48,7 @@ export default function ImagePreSignedForm({ setIsUploaded(false); setIsFailed(false); setProgress(0); + setUploadStatus([]); }; const handleRefetch = () => { @@ -65,6 +69,7 @@ export default function ImagePreSignedForm({ } setFiles((prevFiles) => [...prevFiles, ...processedFiles]); + setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]); } event.target.value = ''; @@ -103,6 +108,7 @@ export default function ImagePreSignedForm({ } setFiles((prevFiles) => [...prevFiles, ...processedFiles]); + setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]); } else { const droppedFiles = event.dataTransfer.files; if (droppedFiles) { @@ -111,12 +117,14 @@ export default function ImagePreSignedForm({ processedFiles.push({ path: file.name, file }); } setFiles((prevFiles) => [...prevFiles, ...processedFiles]); + setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]); } } }; const handleRemoveFile = (index: number) => { setFiles(files.filter((_, i) => i !== index)); + setUploadStatus((prevStatus) => prevStatus.filter((_, i) => i !== index)); }; const handleUpload = async () => { @@ -138,19 +146,52 @@ export default function ImagePreSignedForm({ } } - try { - await uploadFiles({ - files: finalFiles, - projectId, - folderId, - memberId, - onProgress: (progressValue) => setProgress(progressValue), - }); - setIsUploaded(true); - handleRefetch(); - } catch (error) { - setIsFailed(true); - console.error('업로드 실패:', error); + if (uploadType === 'file') { + uploadImageFile.mutate( + { + memberId, + projectId, + folderId, + files: finalFiles.map(({ file }) => file), // Extract only the file + progressCallback: (index: number) => { + setUploadStatus((prevStatus) => { + const newStatus = [...prevStatus]; + newStatus[index] = true; // Mark as uploaded + return newStatus; + }); + }, + }, + { + onSuccess: () => { + handleRefetch(); + setIsUploaded(true); + }, + onError: () => { + setIsFailed(true); + setUploadStatus((prevStatus) => prevStatus.map((status) => (status === null ? false : status))); + }, + } + ); + } else { + try { + await uploadFiles({ + files: finalFiles, + projectId, + folderId, + memberId, + onProgress: (progressValue: number) => { + setProgress(progressValue); + }, + }); + + setUploadStatus(finalFiles.map(() => true)); + setIsUploaded(true); + handleRefetch(); + } catch (error) { + setIsFailed(true); + setUploadStatus(finalFiles.map(() => false)); + console.error('업로드 실패:', error); + } } } }; @@ -176,7 +217,7 @@ export default function ImagePreSignedForm({ type="file" webkitdirectory={uploadType === 'folder' ? '' : undefined} multiple={uploadType !== 'zip'} - accept={uploadType === 'zip' ? '.zip' : undefined} + accept={uploadType === 'zip' ? '.zip' : uploadType === 'file' ? '.jpg,.jpeg,.png' : undefined} className="absolute inset-0 h-full w-full cursor-pointer opacity-0" onChange={handleChange} /> @@ -212,13 +253,13 @@ export default function ImagePreSignedForm({ {files[index].path} {isUploading ? (
- {isUploaded ? ( + {uploadStatus[index] === true ? ( - ) : isFailed ? ( + ) : uploadStatus[index] === false ? ( void; }) { - const [isOpenUploadFile, setIsOpenUploadFile] = React.useState(false); + const [isOpenUpload, setIsOpenUpload] = React.useState(false); const [fileCount, setFileCount] = React.useState(0); - const [isOpenUploadPresigned, setIsOpenUploadPresigned] = React.useState(false); - const [presignedCount, setPresignedCount] = React.useState(0); - const [isOpenUploadFolderFile, setIsOpenUploadFolderFile] = React.useState(false); - const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState(false); - const [isOpenUploadZip, setIsOpenUploadZip] = React.useState(false); - const [isOpenTestUpload, setIsOpenTestUpload] = React.useState(false); + const [uploadType, setUploadType] = React.useState<'file' | 'folder' | 'zip'>('file'); - const uploadImageZipMutation = useUploadImageZipQuery(); - const uploadImageFolderFileMutation = useUploadImageFolderFileQuery(); - const uploadImageFileMutation = useUploadImageFileQuery(); - const uploadImageFolderMutation = useUploadImageFolderQuery(); - - const handleCloseUploadFile = () => setIsOpenUploadFile(false); - const handleCloseUploadFolderFile = () => setIsOpenUploadFolderFile(false); - const handleCloseUploadFolder = () => setIsOpenUploadFolder(false); - const handleCloseUploadZip = () => setIsOpenUploadZip(false); - const handleCloseTestUpload = () => setIsOpenTestUpload(false); + const handleCloseUpload = () => setIsOpenUpload(false); const handleFileCount = (fileCount: number) => { setFileCount(fileCount); @@ -56,148 +36,59 @@ export default function WorkspaceDropdownMenu({ - console.log('프로젝트 이름 수정')}>프로젝트 이름 수정 + { + setUploadType('file'); + setIsOpenUpload(true); + }} + > + 파일 업로드 + - setIsOpenUploadFile(true)}>파일 업로드 - setIsOpenUploadPresigned(true)}> - 파일 업로드 (PresignedUrl 이용) + { + setUploadType('folder'); + setIsOpenUpload(true); + }} + > + 폴더 업로드 - setIsOpenUploadFolderFile(true)}> - 폴더 업로드 (파일 업로드 API 이용) + + { + setUploadType('zip'); + setIsOpenUpload(true); + }} + > + 압축 파일 업로드 - setIsOpenUploadFolder(true)}> - 폴더 업로드 (백엔드 구현 필요) - - setIsOpenUploadZip(true)}>폴더 압축파일 업로드 - setIsOpenTestUpload(true)}> - 테스트 업로드 (PresignedUrl 이용) - {' '} - {/* 새로운 메뉴 항목 추가 */} - {/* 기존 Dialogs */} - - - 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} /> - setFileCount(fileCount)} - projectId={projectId} - folderId={folderId} - uploadImageZipMutation={uploadImageZipMutation} - uploadImageFolderFileMutation={uploadImageFolderFileMutation} - uploadImageFileMutation={uploadImageFileMutation} - uploadImageFolderMutation={uploadImageFolderMutation} - /> - - - - 0 ? `파일 업로드 PreSigned (${presignedCount})` : '파일 업로드 PreSigned'} + title={ + fileCount > 0 + ? `파일 업로드 (${fileCount})` + : uploadType === 'file' + ? '파일 업로드' + : uploadType === 'folder' + ? '폴더 업로드' + : '압축 파일 업로드' + } /> - setIsOpenUploadPresigned(false)} - onRefetch={onRefetch} - onFileCount={(fileCount: number) => setPresignedCount(fileCount)} - projectId={projectId} - folderId={folderId} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* 테스트 업로드 Dialog */} - - - - setFileCount(fileCount)} + onFileCount={handleFileCount} projectId={projectId} folderId={folderId} - uploadType="folder" // zip flie 가능 + uploadType={uploadType} /> diff --git a/frontend/src/components/WorkspaceSidebar/ProjectContextMenu.tsx b/frontend/src/components/WorkspaceSidebar/ProjectContextMenu.tsx index 6293c03..403f75f 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectContextMenu.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectContextMenu.tsx @@ -96,7 +96,6 @@ export default function ProjectContextMenu({ projectId, folderId, node, onRefetc } } }; - const handleDelete = () => { if (node?.type === 'folder') { deleteFolderMutation.mutate( @@ -104,7 +103,7 @@ export default function ProjectContextMenu({ projectId, folderId, node, onRefetc { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] }); - queryClient.invalidateQueries({ queryKey: ['project', projectId] }); + queryClient.invalidateQueries({ queryKey: ['folder', projectId, node.id] }); onRefetch(); }, } @@ -116,7 +115,6 @@ export default function ProjectContextMenu({ projectId, folderId, node, onRefetc onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] }); queryClient.invalidateQueries({ queryKey: ['image', node.id] }); - queryClient.invalidateQueries({ queryKey: ['project', projectId] }); onRefetch(); }, } diff --git a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx index a494fd4..c3c2a4c 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx @@ -11,8 +11,7 @@ import AutoLabelButton from './AutoLabelButton'; import { Folder, Image as ImageIcon } from 'lucide-react'; import { Spinner } from '../ui/spinner'; import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; -import { DndProvider, useDrag, useDrop } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; +import { useDrag, useDrop } from 'react-dnd'; import useFolderQuery from '@/queries/folders/useFolderQuery'; import MemoFileStatusIcon from './FileStatusIcon'; import moveNodeInTree from '@/utils/moveNodeInTree'; @@ -36,7 +35,7 @@ const MENU_ID = 'project-menu'; export default function ProjectStructure({ project }: { project: Project }) { const { setProject, setCategories, setFolderId } = useProjectStore(); const { image: selectedImage, setImage } = useCanvasStore(); - const { treeData, fetchNodeData, setTreeData } = useTreeData(project.id.toString()); + const { treeData, fetchNodeData, fetchContextFolderData, setTreeData } = useTreeData(project.id.toString()); const { data: categories } = useProjectCategoriesQuery(project.id); const { isLoading, refetch } = useFolderQuery(project.id.toString(), 0); @@ -142,6 +141,8 @@ export default function ProjectStructure({ project }: { project: Project }) { const handleContextMenu = (event: React.MouseEvent, node: FlatNode) => { event.preventDefault(); setContextNode(node); + const parentId = node.parent ? Number(node.parent.id) : null; + fetchContextFolderData(parentId); show({ event }); }; @@ -209,46 +210,44 @@ export default function ProjectStructure({ project }: { project: Project }) { }; return ( - -
-
-
-
-

{project.type}

-
- +
+
+
+

{project.type}

+
+ +
+ {isLoading || !treeData ? ( +
+ -
- {isLoading || !treeData ? ( -
- -
- ) : ( - - {Row} - - )} -
-
- -
+
+ ) : ( + + {Row} + + )} +
+
+
- + ); } diff --git a/frontend/src/hooks/useTreeData.ts b/frontend/src/hooks/useTreeData.ts index 1196b2a..c3a7528 100644 --- a/frontend/src/hooks/useTreeData.ts +++ b/frontend/src/hooks/useTreeData.ts @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import { getFolder } from '@/api/folderApi'; import buildTreeNodes from '@/utils/buildTreeNodes'; -function useFolder(projectId: string, folderId: number, enabled: boolean = folderId === 0) { +function useFolder(projectId: string, folderId: number, enabled: boolean) { return useQuery({ queryKey: ['folder', projectId, folderId], queryFn: () => getFolder(projectId, folderId), @@ -16,13 +16,26 @@ function useFolder(projectId: string, folderId: number, enabled: boolean = folde export default function useTreeData(projectId: string) { const [treeData, setTreeData] = useState(null); const [currentFolderId, setCurrentFolderId] = useState(null); + const [contextFolderId, setContextFolderId] = useState(null); - const { data: rootFolder, isLoading: isRootLoading } = useFolder(projectId, 0); + // 루트 폴더 데이터 + const { data: rootFolder, isLoading: isRootLoading } = useFolder(projectId, 0, true); + + // 현재 선택된 폴더 데이터 const { data: childFolder, isFetching: isChildLoading } = useFolder( projectId, currentFolderId || 0, currentFolderId !== null ); + + // 컨텍스트 메뉴에서 선택된 폴더 데이터 + const { data: contextFolder, isFetching: isContextLoading } = useFolder( + projectId, + contextFolderId || 0, + contextFolderId !== null + ); + + // 트리 데이터를 업데이트하는 함수 const updateTreeData = useCallback((folder: FolderResponse, isRoot: boolean = false) => { if (!folder) return; @@ -56,16 +69,25 @@ export default function useTreeData(projectId: string) { }); }, []); + // 루트 폴더 데이터 로드 useEffect(() => { if (!rootFolder) return; updateTreeData(rootFolder, true); }, [rootFolder, updateTreeData]); + // 현재 선택된 폴더 데이터 업데이트 useEffect(() => { if (!childFolder || currentFolderId === null) return; updateTreeData(childFolder); }, [childFolder, currentFolderId, updateTreeData]); + // 컨텍스트 메뉴에서 선택된 폴더 데이터 업데이트 + useEffect(() => { + if (!contextFolder || contextFolderId === null) return; + -updateTreeData(contextFolder); + }, [contextFolder, contextFolderId, updateTreeData]); + + // 현재 폴더 선택 시 폴더 ID 설정 함수 const fetchNodeData = useCallback( (node: TreeNode) => { if (currentFolderId === Number(node.id)) return; @@ -73,11 +95,20 @@ export default function useTreeData(projectId: string) { }, [currentFolderId] ); + // 컨텍스트 메뉴 선택 시 폴더 ID 설정 함수 + const fetchContextFolderData = useCallback( + (folderId: number | null) => { + if (contextFolderId === folderId) return; + setContextFolderId(folderId); + }, + [contextFolderId] + ); return { treeData, fetchNodeData, + fetchContextFolderData, setTreeData, - isLoading: isRootLoading || isChildLoading, + isLoading: isRootLoading || isChildLoading || isContextLoading, }; }