Merge branch 'fe/refactor/upload' into 'fe/develop'
Refactor: 파일 업로드 presinged 수정 See merge request s11-s-project/S11P21S002!299
This commit is contained in:
commit
1b93d67f34
@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
</DndProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
@ -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<boolean>(false);
|
||||
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||
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 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({
|
||||
<span className="truncate">{files[index].path}</span>
|
||||
{isUploading ? (
|
||||
<div className="p-2">
|
||||
{isUploaded ? (
|
||||
{uploadStatus[index] === true ? (
|
||||
<CircleCheckBig
|
||||
className="stroke-green-500"
|
||||
size={16}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
) : isFailed ? (
|
||||
) : uploadStatus[index] === false ? (
|
||||
<CircleX
|
||||
className="stroke-red-500"
|
||||
size={16}
|
||||
|
@ -8,12 +8,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
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';
|
||||
|
||||
export default function WorkspaceDropdownMenu({
|
||||
@ -25,25 +19,11 @@ export default function WorkspaceDropdownMenu({
|
||||
folderId: number;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false);
|
||||
const [isOpenUpload, setIsOpenUpload] = React.useState<boolean>(false);
|
||||
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||
const [isOpenUploadPresigned, setIsOpenUploadPresigned] = React.useState<boolean>(false);
|
||||
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 [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({
|
||||
<Menu size={16} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuItem onClick={() => console.log('프로젝트 이름 수정')}>프로젝트 이름 수정</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setUploadType('file');
|
||||
setIsOpenUpload(true);
|
||||
}}
|
||||
>
|
||||
파일 업로드
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setIsOpenUploadFile(true)}>파일 업로드</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsOpenUploadPresigned(true)}>
|
||||
파일 업로드 (PresignedUrl 이용)
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setUploadType('folder');
|
||||
setIsOpenUpload(true);
|
||||
}}
|
||||
>
|
||||
폴더 업로드
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsOpenUploadFolderFile(true)}>
|
||||
폴더 업로드 (파일 업로드 API 이용)
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setUploadType('zip');
|
||||
setIsOpenUpload(true);
|
||||
}}
|
||||
>
|
||||
압축 파일 업로드
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsOpenUploadFolder(true)}>
|
||||
폴더 업로드 (백엔드 구현 필요)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsOpenUploadZip(true)}>폴더 압축파일 업로드</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setIsOpenTestUpload(true)}>
|
||||
테스트 업로드 (PresignedUrl 이용)
|
||||
</DropdownMenuItem>{' '}
|
||||
{/* 새로운 메뉴 항목 추가 */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* 기존 Dialogs */}
|
||||
<Dialog
|
||||
open={isOpenUploadFile}
|
||||
onOpenChange={setIsOpenUploadFile}
|
||||
>
|
||||
<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}
|
||||
open={isOpenUpload}
|
||||
onOpenChange={setIsOpenUpload}
|
||||
>
|
||||
<DialogTrigger asChild></DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<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
|
||||
onClose={handleCloseTestUpload}
|
||||
onClose={handleCloseUpload}
|
||||
onRefetch={onRefetch}
|
||||
onFileCount={(fileCount: number) => setFileCount(fileCount)}
|
||||
onFileCount={handleFileCount}
|
||||
projectId={projectId}
|
||||
folderId={folderId}
|
||||
uploadType="folder" // zip flie 가능
|
||||
uploadType={uploadType}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -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();
|
||||
},
|
||||
}
|
||||
|
@ -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 (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div
|
||||
className="box-border flex h-full min-h-0 flex-col overflow-x-hidden bg-gray-50"
|
||||
ref={containerRef}
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<WorkspaceDropdownMenu
|
||||
projectId={project.id}
|
||||
folderId={0}
|
||||
onRefetch={refetch}
|
||||
<div
|
||||
className="box-border flex h-full min-h-0 flex-col overflow-x-hidden bg-gray-50"
|
||||
ref={containerRef}
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<WorkspaceDropdownMenu
|
||||
projectId={project.id}
|
||||
folderId={0}
|
||||
onRefetch={refetch}
|
||||
/>
|
||||
</header>
|
||||
{isLoading || !treeData ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Spinner
|
||||
show={true}
|
||||
size={'large'}
|
||||
/>
|
||||
</header>
|
||||
{isLoading || !treeData ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Spinner
|
||||
show={true}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<List
|
||||
height={Math.min(flatData.length * 20, containerHeight)}
|
||||
itemCount={flatData.length}
|
||||
itemSize={20}
|
||||
width={'100%'}
|
||||
itemData={flatData}
|
||||
itemKey={getItemKey}
|
||||
className="flex-1 overflow-x-hidden"
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<AutoLabelButton projectId={project.id} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<List
|
||||
height={Math.min(flatData.length * 20, containerHeight)}
|
||||
itemCount={flatData.length}
|
||||
itemSize={20}
|
||||
width={'100%'}
|
||||
itemData={flatData}
|
||||
itemKey={getItemKey}
|
||||
className="flex-1 overflow-x-hidden"
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<AutoLabelButton projectId={project.id} />
|
||||
</div>
|
||||
|
||||
<ProjectContextMenu
|
||||
@ -263,6 +262,6 @@ export default function ProjectStructure({ project }: { project: Project }) {
|
||||
}
|
||||
onRefetch={refetch}
|
||||
/>
|
||||
</DndProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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<TreeNode | 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(
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user