Merge branch 'fe/refactor/upload' into 'fe/develop'

Refactor: 파일 업로드 presinged 수정

See merge request s11-s-project/S11P21S002!299
This commit is contained in:
조현수 2024-10-07 16:40:10 +09:00
commit 1b93d67f34
6 changed files with 181 additions and 217 deletions

View File

@ -2,14 +2,18 @@ import { RouterProvider } from 'react-router-dom';
import router from './router'; import router from './router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from './components/ui/toaster'; import { Toaster } from './components/ui/toaster';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
function App() { function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RouterProvider router={router} /> <DndProvider backend={HTML5Backend}>
<Toaster /> <RouterProvider router={router} />
<Toaster />
</DndProvider>
</QueryClientProvider> </QueryClientProvider>
); );
} }

View File

@ -5,6 +5,7 @@ import useAuthStore from '@/stores/useAuthStore';
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react'; import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import useUploadFiles from '@/hooks/useUploadFiles'; import useUploadFiles from '@/hooks/useUploadFiles';
import useUploadImagePresignedQuery from '@/queries/images/useUploadImagePresignedQuery';
import { unzipFilesWithPath, extractFilesRecursivelyWithPath } from '@/utils/fileUtils'; import { unzipFilesWithPath, extractFilesRecursivelyWithPath } from '@/utils/fileUtils';
interface ImagePreSignedFormProps { interface ImagePreSignedFormProps {
@ -34,9 +35,11 @@ export default function ImagePreSignedForm({
const [isUploaded, setIsUploaded] = useState<boolean>(false); const [isUploaded, setIsUploaded] = useState<boolean>(false);
const [isFailed, setIsFailed] = useState<boolean>(false); const [isFailed, setIsFailed] = useState<boolean>(false);
const [progress, setProgress] = useState<number>(0); const [progress, setProgress] = useState<number>(0);
const [uploadStatus, setUploadStatus] = useState<(boolean | null)[]>([]);
// Ensure to destructure the uploadFiles function properly from the hook
const { uploadFiles } = useUploadFiles(); const { uploadFiles } = useUploadFiles();
const uploadImageFile = useUploadImagePresignedQuery();
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
setFiles([]); setFiles([]);
@ -45,6 +48,7 @@ export default function ImagePreSignedForm({
setIsUploaded(false); setIsUploaded(false);
setIsFailed(false); setIsFailed(false);
setProgress(0); setProgress(0);
setUploadStatus([]);
}; };
const handleRefetch = () => { const handleRefetch = () => {
@ -65,6 +69,7 @@ export default function ImagePreSignedForm({
} }
setFiles((prevFiles) => [...prevFiles, ...processedFiles]); setFiles((prevFiles) => [...prevFiles, ...processedFiles]);
setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]);
} }
event.target.value = ''; event.target.value = '';
@ -103,6 +108,7 @@ export default function ImagePreSignedForm({
} }
setFiles((prevFiles) => [...prevFiles, ...processedFiles]); setFiles((prevFiles) => [...prevFiles, ...processedFiles]);
setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]);
} else { } else {
const droppedFiles = event.dataTransfer.files; const droppedFiles = event.dataTransfer.files;
if (droppedFiles) { if (droppedFiles) {
@ -111,12 +117,14 @@ export default function ImagePreSignedForm({
processedFiles.push({ path: file.name, file }); processedFiles.push({ path: file.name, file });
} }
setFiles((prevFiles) => [...prevFiles, ...processedFiles]); setFiles((prevFiles) => [...prevFiles, ...processedFiles]);
setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]);
} }
} }
}; };
const handleRemoveFile = (index: number) => { const handleRemoveFile = (index: number) => {
setFiles(files.filter((_, i) => i !== index)); setFiles(files.filter((_, i) => i !== index));
setUploadStatus((prevStatus) => prevStatus.filter((_, i) => i !== index));
}; };
const handleUpload = async () => { const handleUpload = async () => {
@ -138,19 +146,52 @@ export default function ImagePreSignedForm({
} }
} }
try { if (uploadType === 'file') {
await uploadFiles({ uploadImageFile.mutate(
files: finalFiles, {
projectId, memberId,
folderId, projectId,
memberId, folderId,
onProgress: (progressValue) => setProgress(progressValue), files: finalFiles.map(({ file }) => file), // Extract only the file
}); progressCallback: (index: number) => {
setIsUploaded(true); setUploadStatus((prevStatus) => {
handleRefetch(); const newStatus = [...prevStatus];
} catch (error) { newStatus[index] = true; // Mark as uploaded
setIsFailed(true); return newStatus;
console.error('업로드 실패:', error); });
},
},
{
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" type="file"
webkitdirectory={uploadType === 'folder' ? '' : undefined} webkitdirectory={uploadType === 'folder' ? '' : undefined}
multiple={uploadType !== 'zip'} 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" className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
onChange={handleChange} onChange={handleChange}
/> />
@ -212,13 +253,13 @@ export default function ImagePreSignedForm({
<span className="truncate">{files[index].path}</span> <span className="truncate">{files[index].path}</span>
{isUploading ? ( {isUploading ? (
<div className="p-2"> <div className="p-2">
{isUploaded ? ( {uploadStatus[index] === true ? (
<CircleCheckBig <CircleCheckBig
className="stroke-green-500" className="stroke-green-500"
size={16} size={16}
strokeWidth="2" strokeWidth="2"
/> />
) : isFailed ? ( ) : uploadStatus[index] === false ? (
<CircleX <CircleX
className="stroke-red-500" className="stroke-red-500"
size={16} size={16}

View File

@ -8,12 +8,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '../ui/dropdown-menu'; } from '../ui/dropdown-menu';
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom'; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
import ImageUploadForm from '../ImageUploadModal/ImageUploadForm';
import ImageUploadPresignedForm from '../ImageUploadPresignedModal/ImageUploadPresignedForm';
import useUploadImageFileQuery from '@/queries/projects/useUploadImageFileQuery';
import useUploadImageFolderFileQuery from '@/queries/projects/useUploadImageFolderFileQuery';
import useUploadImageZipQuery from '@/queries/projects/useUploadImageZipQuery';
import useUploadImageFolderQuery from '@/queries/projects/useUploadImageFolderQuery';
import ImagePreSignedForm from '../ImagePreSignedForm'; import ImagePreSignedForm from '../ImagePreSignedForm';
export default function WorkspaceDropdownMenu({ export default function WorkspaceDropdownMenu({
@ -25,25 +19,11 @@ export default function WorkspaceDropdownMenu({
folderId: number; folderId: number;
onRefetch: () => void; onRefetch: () => void;
}) { }) {
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false); const [isOpenUpload, setIsOpenUpload] = React.useState<boolean>(false);
const [fileCount, setFileCount] = React.useState<number>(0); const [fileCount, setFileCount] = React.useState<number>(0);
const [isOpenUploadPresigned, setIsOpenUploadPresigned] = React.useState<boolean>(false); const [uploadType, setUploadType] = React.useState<'file' | 'folder' | 'zip'>('file');
const [presignedCount, setPresignedCount] = React.useState<number>(0);
const [isOpenUploadFolderFile, setIsOpenUploadFolderFile] = React.useState<boolean>(false);
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
const [isOpenTestUpload, setIsOpenTestUpload] = React.useState<boolean>(false);
const uploadImageZipMutation = useUploadImageZipQuery(); const handleCloseUpload = () => setIsOpenUpload(false);
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 handleFileCount = (fileCount: number) => { const handleFileCount = (fileCount: number) => {
setFileCount(fileCount); setFileCount(fileCount);
@ -56,148 +36,59 @@ export default function WorkspaceDropdownMenu({
<Menu size={16} /> <Menu size={16} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56"> <DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={() => console.log('프로젝트 이름 수정')}> </DropdownMenuItem> <DropdownMenuItem
onClick={() => {
setUploadType('file');
setIsOpenUpload(true);
}}
>
</DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setIsOpenUploadFile(true)}> </DropdownMenuItem> <DropdownMenuItem
<DropdownMenuItem onClick={() => setIsOpenUploadPresigned(true)}> onClick={() => {
(PresignedUrl ) setUploadType('folder');
setIsOpenUpload(true);
}}
>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenUploadFolderFile(true)}> <DropdownMenuSeparator />
( API ) <DropdownMenuItem
onClick={() => {
setUploadType('zip');
setIsOpenUpload(true);
}}
>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenUploadFolder(true)}>
( )
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenUploadZip(true)}> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsOpenTestUpload(true)}>
(PresignedUrl )
</DropdownMenuItem>{' '}
{/* 새로운 메뉴 항목 추가 */}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* 기존 Dialogs */}
<Dialog <Dialog
open={isOpenUploadFile} open={isOpenUpload}
onOpenChange={setIsOpenUploadFile} onOpenChange={setIsOpenUpload}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
<ImageUploadForm
onClose={handleCloseUploadFile}
onRefetch={onRefetch}
onFileCount={(fileCount: number) => setFileCount(fileCount)}
projectId={projectId}
folderId={folderId}
uploadImageZipMutation={uploadImageZipMutation}
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
uploadImageFileMutation={uploadImageFileMutation}
uploadImageFolderMutation={uploadImageFolderMutation}
/>
</DialogContent>
</Dialog>
<Dialog
open={isOpenUploadPresigned}
onOpenChange={setIsOpenUploadPresigned}
> >
<DialogTrigger asChild></DialogTrigger> <DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader <DialogHeader
title={presignedCount > 0 ? `파일 업로드 PreSigned (${presignedCount})` : '파일 업로드 PreSigned'} title={
fileCount > 0
? `파일 업로드 (${fileCount})`
: uploadType === 'file'
? '파일 업로드'
: uploadType === 'folder'
? '폴더 업로드'
: '압축 파일 업로드'
}
/> />
<ImageUploadPresignedForm
onClose={() => setIsOpenUploadPresigned(false)}
onRefetch={onRefetch}
onFileCount={(fileCount: number) => setPresignedCount(fileCount)}
projectId={projectId}
folderId={folderId}
/>
</DialogContent>
</Dialog>
<Dialog
open={isOpenUploadFolderFile}
onOpenChange={setIsOpenUploadFolderFile}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 업로드 (파일 업로드 API 이용)" />
<ImageUploadForm
onClose={handleCloseUploadFolderFile}
onRefetch={onRefetch}
onFileCount={handleFileCount}
projectId={projectId}
folderId={folderId}
isFolderUpload={true}
uploadImageZipMutation={uploadImageZipMutation}
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
uploadImageFileMutation={uploadImageFileMutation}
uploadImageFolderMutation={uploadImageFolderMutation}
/>
</DialogContent>
</Dialog>
<Dialog
open={isOpenUploadFolder}
onOpenChange={setIsOpenUploadFolder}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 업로드 (백엔드 구현 필요)" />
<ImageUploadForm
onClose={handleCloseUploadFolder}
onRefetch={onRefetch}
onFileCount={handleFileCount}
projectId={projectId}
folderId={folderId}
isFolderBackendUpload={true}
uploadImageZipMutation={uploadImageZipMutation}
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
uploadImageFileMutation={uploadImageFileMutation}
uploadImageFolderMutation={uploadImageFolderMutation}
/>
</DialogContent>
</Dialog>
<Dialog
open={isOpenUploadZip}
onOpenChange={setIsOpenUploadZip}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 압축파일 업로드" />
<ImageUploadForm
onClose={handleCloseUploadZip}
onRefetch={onRefetch}
onFileCount={handleFileCount}
projectId={projectId}
folderId={folderId}
isZipUpload={true}
uploadImageZipMutation={uploadImageZipMutation}
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
uploadImageFileMutation={uploadImageFileMutation}
uploadImageFolderMutation={uploadImageFolderMutation}
/>
</DialogContent>
</Dialog>
{/* 테스트 업로드 Dialog */}
<Dialog
open={isOpenTestUpload}
onOpenChange={setIsOpenTestUpload}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader title="테스트 업로드 (PresignedUrl 이용)" />
<ImagePreSignedForm <ImagePreSignedForm
onClose={handleCloseTestUpload} onClose={handleCloseUpload}
onRefetch={onRefetch} onRefetch={onRefetch}
onFileCount={(fileCount: number) => setFileCount(fileCount)} onFileCount={handleFileCount}
projectId={projectId} projectId={projectId}
folderId={folderId} folderId={folderId}
uploadType="folder" // zip flie 가능 uploadType={uploadType}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -96,7 +96,6 @@ export default function ProjectContextMenu({ projectId, folderId, node, onRefetc
} }
} }
}; };
const handleDelete = () => { const handleDelete = () => {
if (node?.type === 'folder') { if (node?.type === 'folder') {
deleteFolderMutation.mutate( deleteFolderMutation.mutate(
@ -104,7 +103,7 @@ export default function ProjectContextMenu({ projectId, folderId, node, onRefetc
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] }); queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] });
queryClient.invalidateQueries({ queryKey: ['project', projectId] }); queryClient.invalidateQueries({ queryKey: ['folder', projectId, node.id] });
onRefetch(); onRefetch();
}, },
} }
@ -116,7 +115,6 @@ export default function ProjectContextMenu({ projectId, folderId, node, onRefetc
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] }); queryClient.invalidateQueries({ queryKey: ['folder', projectId, folderId] });
queryClient.invalidateQueries({ queryKey: ['image', node.id] }); queryClient.invalidateQueries({ queryKey: ['image', node.id] });
queryClient.invalidateQueries({ queryKey: ['project', projectId] });
onRefetch(); onRefetch();
}, },
} }

View File

@ -11,8 +11,7 @@ import AutoLabelButton from './AutoLabelButton';
import { Folder, Image as ImageIcon } from 'lucide-react'; import { Folder, Image as ImageIcon } from 'lucide-react';
import { Spinner } from '../ui/spinner'; import { Spinner } from '../ui/spinner';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import useFolderQuery from '@/queries/folders/useFolderQuery'; import useFolderQuery from '@/queries/folders/useFolderQuery';
import MemoFileStatusIcon from './FileStatusIcon'; import MemoFileStatusIcon from './FileStatusIcon';
import moveNodeInTree from '@/utils/moveNodeInTree'; import moveNodeInTree from '@/utils/moveNodeInTree';
@ -36,7 +35,7 @@ const MENU_ID = 'project-menu';
export default function ProjectStructure({ project }: { project: Project }) { export default function ProjectStructure({ project }: { project: Project }) {
const { setProject, setCategories, setFolderId } = useProjectStore(); const { setProject, setCategories, setFolderId } = useProjectStore();
const { image: selectedImage, setImage } = useCanvasStore(); 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 { data: categories } = useProjectCategoriesQuery(project.id);
const { isLoading, refetch } = useFolderQuery(project.id.toString(), 0); 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) => { const handleContextMenu = (event: React.MouseEvent, node: FlatNode) => {
event.preventDefault(); event.preventDefault();
setContextNode(node); setContextNode(node);
const parentId = node.parent ? Number(node.parent.id) : null;
fetchContextFolderData(parentId);
show({ event }); show({ event });
}; };
@ -209,46 +210,44 @@ export default function ProjectStructure({ project }: { project: Project }) {
}; };
return ( return (
<DndProvider backend={HTML5Backend}> <div
<div className="box-border flex h-full min-h-0 flex-col overflow-x-hidden bg-gray-50"
className="box-border flex h-full min-h-0 flex-col overflow-x-hidden bg-gray-50" ref={containerRef}
ref={containerRef} >
> <div className="flex h-full flex-col gap-2 overflow-hidden px-1 pb-2">
<div className="flex h-full flex-col gap-2 overflow-hidden px-1 pb-2"> <header className="flex w-full items-center gap-2 rounded-md bg-white p-2 shadow-sm">
<header className="flex w-full items-center gap-2 rounded-md bg-white p-2 shadow-sm"> <div className="flex w-full min-w-0 items-center gap-1 pr-1">
<div className="flex w-full min-w-0 items-center gap-1 pr-1"> <h2 className="caption overflow-hidden text-ellipsis whitespace-nowrap text-gray-600">{project.type}</h2>
<h2 className="caption overflow-hidden text-ellipsis whitespace-nowrap text-gray-600">{project.type}</h2> </div>
</div> <WorkspaceDropdownMenu
<WorkspaceDropdownMenu projectId={project.id}
projectId={project.id} folderId={0}
folderId={0} onRefetch={refetch}
onRefetch={refetch} />
</header>
{isLoading || !treeData ? (
<div className="flex h-full items-center justify-center">
<Spinner
show={true}
size={'large'}
/> />
</header> </div>
{isLoading || !treeData ? ( ) : (
<div className="flex h-full items-center justify-center"> <List
<Spinner height={Math.min(flatData.length * 20, containerHeight)}
show={true} itemCount={flatData.length}
size={'large'} itemSize={20}
/> width={'100%'}
</div> itemData={flatData}
) : ( itemKey={getItemKey}
<List className="flex-1 overflow-x-hidden"
height={Math.min(flatData.length * 20, containerHeight)} >
itemCount={flatData.length} {Row}
itemSize={20} </List>
width={'100%'} )}
itemData={flatData} </div>
itemKey={getItemKey} <div className="flex">
className="flex-1 overflow-x-hidden" <AutoLabelButton projectId={project.id} />
>
{Row}
</List>
)}
</div>
<div className="flex">
<AutoLabelButton projectId={project.id} />
</div>
</div> </div>
<ProjectContextMenu <ProjectContextMenu
@ -263,6 +262,6 @@ export default function ProjectStructure({ project }: { project: Project }) {
} }
onRefetch={refetch} onRefetch={refetch}
/> />
</DndProvider> </div>
); );
} }

View File

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