Merge branch 'fe/refactor/upload' into 'fe/develop'
Refacotr: 업로드 부분 리팩토링 및 드래그 앱 드랍 살림. See merge request s11-s-project/S11P21S002!295
This commit is contained in:
commit
2fcb333d65
271
frontend/src/components/ImageUploadModal/ImageUploadForm.tsx
Normal file
271
frontend/src/components/ImageUploadModal/ImageUploadForm.tsx
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import useAuthStore from '@/stores/useAuthStore';
|
||||||
|
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||||
|
import { FixedSizeList } from 'react-window';
|
||||||
|
import { UseMutationResult } from '@tanstack/react-query';
|
||||||
|
import { UploadZipParams, UploadFolderParams } from '@/types/uploadTypes';
|
||||||
|
|
||||||
|
interface UploadFormProps {
|
||||||
|
onClose: () => void;
|
||||||
|
onRefetch?: () => void;
|
||||||
|
onFileCount: (fileCount: number) => void;
|
||||||
|
projectId: number;
|
||||||
|
folderId: number;
|
||||||
|
isFolderUpload?: boolean;
|
||||||
|
isZipUpload?: boolean;
|
||||||
|
isFolderBackendUpload?: boolean;
|
||||||
|
uploadImageZipMutation: UseMutationResult<unknown, Error, UploadZipParams, unknown>;
|
||||||
|
uploadImageFolderFileMutation: UseMutationResult<unknown, Error, UploadFolderParams, unknown>;
|
||||||
|
uploadImageFileMutation: UseMutationResult<unknown, Error, UploadFolderParams, unknown>;
|
||||||
|
uploadImageFolderMutation: UseMutationResult<unknown, Error, UploadFolderParams, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImageUploadForm({
|
||||||
|
onClose,
|
||||||
|
onRefetch,
|
||||||
|
onFileCount,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
isFolderUpload = false,
|
||||||
|
isZipUpload = false,
|
||||||
|
isFolderBackendUpload = false,
|
||||||
|
uploadImageZipMutation,
|
||||||
|
uploadImageFolderFileMutation,
|
||||||
|
uploadImageFileMutation,
|
||||||
|
uploadImageFolderMutation,
|
||||||
|
}: UploadFormProps) {
|
||||||
|
const profile = useAuthStore((state) => state.profile);
|
||||||
|
const memberId = profile?.id || 0;
|
||||||
|
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [inputKey, setInputKey] = useState<number>(0);
|
||||||
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||||
|
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||||
|
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||||
|
const [progress, setProgress] = useState<number>(0);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setFiles([]);
|
||||||
|
setInputKey((prevKey) => prevKey + 1);
|
||||||
|
setIsUploading(false);
|
||||||
|
setIsUploaded(false);
|
||||||
|
setIsFailed(false);
|
||||||
|
setProgress(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefetch = () => {
|
||||||
|
if (onRefetch) {
|
||||||
|
onRefetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newFiles = event.target.files;
|
||||||
|
|
||||||
|
if (newFiles) {
|
||||||
|
setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsDragging(false);
|
||||||
|
const droppedFiles = event.dataTransfer.files;
|
||||||
|
if (droppedFiles) {
|
||||||
|
setFiles((prevFiles) => [...prevFiles, ...Array.from(droppedFiles)]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = (index: number) => {
|
||||||
|
if (isFolderUpload) {
|
||||||
|
setFiles([]);
|
||||||
|
setInputKey((prevKey) => prevKey + 1);
|
||||||
|
} else {
|
||||||
|
setFiles(files.filter((_, i) => i !== index));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = () => {
|
||||||
|
if (files.length > 0) {
|
||||||
|
setIsUploading(true);
|
||||||
|
setIsUploaded(false);
|
||||||
|
setIsFailed(false);
|
||||||
|
|
||||||
|
const progressCallback = (progress: number) => {
|
||||||
|
setProgress(progress);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isZipUpload) {
|
||||||
|
const variables: UploadZipParams = {
|
||||||
|
memberId,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
file: files[0],
|
||||||
|
progressCallback,
|
||||||
|
};
|
||||||
|
uploadImageZipMutation.mutate(variables, {
|
||||||
|
onSuccess: () => {
|
||||||
|
handleRefetch();
|
||||||
|
setIsUploaded(true);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setIsFailed(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const variables: UploadFolderParams = {
|
||||||
|
memberId,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
files,
|
||||||
|
progressCallback,
|
||||||
|
};
|
||||||
|
const mutation = isFolderBackendUpload
|
||||||
|
? uploadImageFolderMutation
|
||||||
|
: isFolderUpload
|
||||||
|
? uploadImageFolderFileMutation
|
||||||
|
: uploadImageFileMutation;
|
||||||
|
|
||||||
|
mutation.mutate(variables, {
|
||||||
|
onSuccess: () => {
|
||||||
|
handleRefetch();
|
||||||
|
setIsUploaded(true);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setIsFailed(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFileCount(files.length);
|
||||||
|
}, [files, onFileCount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{!isUploading && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-[200px] w-full cursor-pointer items-center justify-center rounded-lg border-2 text-center',
|
||||||
|
isDragging ? 'border-solid border-primary bg-blue-200' : 'border-dashed border-gray-500 bg-gray-100'
|
||||||
|
)}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
key={inputKey}
|
||||||
|
type="file"
|
||||||
|
{...(isFolderUpload ? { webkitdirectory: '' } : { multiple: !isZipUpload })}
|
||||||
|
accept={isZipUpload ? '.zip' : undefined}
|
||||||
|
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{isDragging ? (
|
||||||
|
<p className="text-primary">
|
||||||
|
{isFolderUpload ? '드래그한 폴더를 여기에 놓으세요' : '드래그한 파일을 여기에 놓으세요'}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">
|
||||||
|
{isFolderUpload
|
||||||
|
? '폴더를 업로드하려면 여기를 클릭하거나 폴더를 드래그하여 여기에 놓으세요'
|
||||||
|
: '파일을 업로드하려면 여기를 클릭하거나 파일을 드래그하여 여기에 놓으세요'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{files.length > 0 && (
|
||||||
|
<ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
|
||||||
|
<FixedSizeList
|
||||||
|
height={260}
|
||||||
|
itemCount={files.length}
|
||||||
|
itemSize={40}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
{({ index, style }) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between p-1"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<span className="truncate">{files[index].webkitRelativePath || files[index].name}</span>
|
||||||
|
{isUploading ? (
|
||||||
|
<div className="p-2">
|
||||||
|
{isUploaded ? (
|
||||||
|
<CircleCheckBig
|
||||||
|
className="stroke-green-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
) : isFailed ? (
|
||||||
|
<CircleX
|
||||||
|
className="stroke-red-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CircleDashed
|
||||||
|
className="stroke-gray-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="cursor-pointer p-2"
|
||||||
|
onClick={() => handleRemoveFile(index)}
|
||||||
|
>
|
||||||
|
<X
|
||||||
|
color="red"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</FixedSizeList>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{isUploading ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleClose}
|
||||||
|
variant={isFailed ? 'red' : 'blue'}
|
||||||
|
disabled={!isUploaded && !isFailed}
|
||||||
|
>
|
||||||
|
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleUpload}
|
||||||
|
variant="blue"
|
||||||
|
disabled={files.length === 0}
|
||||||
|
>
|
||||||
|
업로드
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
70
frontend/src/components/ImageUploadModal/index.tsx
Normal file
70
frontend/src/components/ImageUploadModal/index.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import ImageUploadForm from './ImageUploadForm';
|
||||||
|
import useUploadImageFileQuery from '@/queries/projects/useUploadImageFileQuery';
|
||||||
|
import useUploadImageFolderFileQuery from '@/queries/projects/useUploadImageFolderFileQuery';
|
||||||
|
import useUploadImageZipQuery from '@/queries/projects/useUploadImageZipQuery';
|
||||||
|
import useUploadImageFolderQuery from '@/queries/projects/useUploadImageFolderQuery';
|
||||||
|
|
||||||
|
interface ImageUploadModalProps {
|
||||||
|
projectId: number;
|
||||||
|
folderId: number;
|
||||||
|
isFolderUpload?: boolean;
|
||||||
|
isZipUpload?: boolean;
|
||||||
|
isFolderBackendUpload?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImageUploadModal({
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
isFolderUpload = false,
|
||||||
|
isZipUpload = false,
|
||||||
|
isFolderBackendUpload = false,
|
||||||
|
}: ImageUploadModalProps) {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||||
|
|
||||||
|
const handleOpen = () => setIsOpen(true);
|
||||||
|
const handleClose = () => setIsOpen(false);
|
||||||
|
const handleFileCount = (fileCount: number) => {
|
||||||
|
setFileCount(fileCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadImageZipMutation = useUploadImageZipQuery();
|
||||||
|
const uploadImageFolderFileMutation = useUploadImageFolderFileQuery();
|
||||||
|
const uploadImageFileMutation = useUploadImageFileQuery();
|
||||||
|
const uploadImageFolderMutation = useUploadImageFolderQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center p-2"
|
||||||
|
onClick={handleOpen}
|
||||||
|
>
|
||||||
|
<Plus size={20} />
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
|
||||||
|
<ImageUploadForm
|
||||||
|
onClose={handleClose}
|
||||||
|
onFileCount={handleFileCount}
|
||||||
|
projectId={projectId}
|
||||||
|
folderId={folderId}
|
||||||
|
isFolderUpload={isFolderUpload}
|
||||||
|
isZipUpload={isZipUpload}
|
||||||
|
isFolderBackendUpload={isFolderBackendUpload}
|
||||||
|
uploadImageZipMutation={uploadImageZipMutation}
|
||||||
|
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
|
||||||
|
uploadImageFileMutation={uploadImageFileMutation}
|
||||||
|
uploadImageFolderMutation={uploadImageFolderMutation}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@ -11,7 +11,7 @@ export default function ImageUploadPresignedForm({
|
|||||||
onFileCount,
|
onFileCount,
|
||||||
projectId,
|
projectId,
|
||||||
folderId,
|
folderId,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRefetch?: () => void;
|
onRefetch?: () => void;
|
||||||
onFileCount: (fileCount: number) => void;
|
onFileCount: (fileCount: number) => void;
|
||||||
@ -26,11 +26,10 @@ export default function ImageUploadPresignedForm({
|
|||||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||||
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 [uploadStatus, setUploadStatus] = useState<(boolean | null)[]>([]); // 각 파일의 성공/실패 여부 관리
|
const [uploadStatus, setUploadStatus] = useState<(boolean | null)[]>([]);
|
||||||
|
|
||||||
const uploadImageFile = useUploadImagePresignedQuery();
|
const uploadImageFile = useUploadImagePresignedQuery();
|
||||||
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@ -51,7 +50,7 @@ export default function ImageUploadPresignedForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setFiles((prevFiles) => [...prevFiles, ...newImages]);
|
setFiles((prevFiles) => [...prevFiles, ...newImages]);
|
||||||
setUploadStatus((prevState) => [...prevState, ...newImages.map(()=> null)]);
|
setUploadStatus((prevState) => [...prevState, ...newImages.map(() => null)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
@ -59,20 +58,38 @@ export default function ImageUploadPresignedForm({
|
|||||||
|
|
||||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
|
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = () => {
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
|
const droppedFiles = event.dataTransfer.files;
|
||||||
|
if (droppedFiles) {
|
||||||
|
const newImages = Array.from(droppedFiles).filter((file) => {
|
||||||
|
const fileExtension = file.name.split('.').pop()?.toLowerCase() ?? '';
|
||||||
|
return ['jpg', 'png', 'jpeg'].includes(fileExtension);
|
||||||
|
});
|
||||||
|
|
||||||
|
setFiles((prevFiles) => [...prevFiles, ...newImages]);
|
||||||
|
setUploadStatus((prevState) => [...prevState, ...newImages.map(() => null)]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFile = (index: number) => {
|
const handleRemoveFile = (index: number) => {
|
||||||
setFiles(files.filter((_, i) => i != index));
|
setFiles(files.filter((_, i) => i !== index));
|
||||||
|
setUploadStatus((prevState) => prevState.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
@ -85,7 +102,6 @@ export default function ImageUploadPresignedForm({
|
|||||||
folderId,
|
folderId,
|
||||||
files,
|
files,
|
||||||
progressCallback: (index: number) => {
|
progressCallback: (index: number) => {
|
||||||
// 업로드 성공하면 상태 업데이트
|
|
||||||
setUploadStatus((prevStatus) => {
|
setUploadStatus((prevStatus) => {
|
||||||
const newStatus = [...prevStatus];
|
const newStatus = [...prevStatus];
|
||||||
newStatus[index] = true; // 업로드 성공 시 true
|
newStatus[index] = true; // 업로드 성공 시 true
|
||||||
@ -100,18 +116,14 @@ export default function ImageUploadPresignedForm({
|
|||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setIsFailed(true);
|
setIsFailed(true);
|
||||||
setUploadStatus((prevStatus) =>
|
setUploadStatus((prevStatus) => prevStatus.map((status) => (status === null ? false : status))); // 실패 시 처리
|
||||||
prevStatus.map((status) => (status === null ? false : status))
|
|
||||||
); // 실패 시 처리
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 전체 진행 상황 계산
|
// 전체 진행 상황 계산
|
||||||
const totalProgress = Math.round(
|
const totalProgress = Math.round((uploadStatus.filter((status) => status !== null).length / files.length) * 100);
|
||||||
(uploadStatus.filter((status) => status !== null).length / files.length) * 100
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFileCount(files.length);
|
onFileCount(files.length);
|
||||||
@ -123,8 +135,11 @@ export default function ImageUploadPresignedForm({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-[200px] w-full cursor-pointer items-center justify-center rounded-lg border-2 text-center',
|
'relative flex h-[200px] w-full cursor-pointer items-center justify-center rounded-lg border-2 text-center',
|
||||||
isDragging ? 'border-solid border-primary bg-blue-200' : 'border-dashed border-gray-500 bg-gray-100',
|
isDragging ? 'border-solid border-primary bg-blue-200' : 'border-dashed border-gray-500 bg-gray-100'
|
||||||
)}
|
)}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@ -133,9 +148,6 @@ export default function ImageUploadPresignedForm({
|
|||||||
multiple
|
multiple
|
||||||
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}
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
/>
|
/>
|
||||||
{isDragging ? (
|
{isDragging ? (
|
||||||
<p className="text-primary">드래그한 파일을 여기에 놓으세요</p>
|
<p className="text-primary">드래그한 파일을 여기에 놓으세요</p>
|
||||||
@ -151,21 +163,43 @@ export default function ImageUploadPresignedForm({
|
|||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
|
<ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
|
||||||
{files.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
<li key={index} className="flex items-center justify-between p-1">
|
<li
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between p-1"
|
||||||
|
>
|
||||||
<span className="truncate">{file.name}</span>
|
<span className="truncate">{file.name}</span>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{uploadStatus[index] === true ? (
|
{uploadStatus[index] === true ? (
|
||||||
<CircleCheckBig className="stroke-green-500" size={16} strokeWidth="2" />
|
<CircleCheckBig
|
||||||
|
className="stroke-green-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
) : uploadStatus[index] === false ? (
|
) : uploadStatus[index] === false ? (
|
||||||
<CircleX className="stroke-red-500" size={16} strokeWidth="2" />
|
<CircleX
|
||||||
|
className="stroke-red-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CircleDashed className="stroke-gray-500" size={16} strokeWidth="2" />
|
<CircleDashed
|
||||||
|
className="stroke-gray-500"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button className={'cursor-pointer p-2'} onClick={() => handleRemoveFile(index)}>
|
<button
|
||||||
<X color="red" size={16} strokeWidth="2" />
|
className={'cursor-pointer p-2'}
|
||||||
|
onClick={() => handleRemoveFile(index)}
|
||||||
|
>
|
||||||
|
<X
|
||||||
|
color="red"
|
||||||
|
size={16}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
@ -173,7 +207,10 @@ export default function ImageUploadPresignedForm({
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<Button onClick={handleClose} variant={isFailed ? 'red' : 'blue'}>
|
<Button
|
||||||
|
onClick={handleClose}
|
||||||
|
variant={isFailed ? 'red' : 'blue'}
|
||||||
|
>
|
||||||
{isFailed
|
{isFailed
|
||||||
? '업로드 실패 (닫기)'
|
? '업로드 실패 (닫기)'
|
||||||
: isUploaded
|
: isUploaded
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Menu } from 'lucide-react';
|
import { Menu } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -7,12 +8,12 @@ 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 React from 'react';
|
import ImageUploadForm from '../ImageUploadModal/ImageUploadForm';
|
||||||
import ImageUploadFileForm from '../ImageUploadFileModal/ImageUploadFileForm';
|
import ImageUploadPresignedForm from '../ImageUploadPresignedModal/ImageUploadPresignedForm';
|
||||||
import ImageUploadFolderFileForm from '../ImageUploadFolderFileModal/ImageUploadFolderFileForm';
|
import useUploadImageFileQuery from '@/queries/projects/useUploadImageFileQuery';
|
||||||
import ImageUploadFolderForm from '../ImageUploadFolderModal/ImageUploadFolderForm';
|
import useUploadImageFolderFileQuery from '@/queries/projects/useUploadImageFolderFileQuery';
|
||||||
import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
|
import useUploadImageZipQuery from '@/queries/projects/useUploadImageZipQuery';
|
||||||
import ImageUploadPresignedForm from '../ImageUploadPresignedModal/ImageUploadPresignedForm.tsx';
|
import useUploadImageFolderQuery from '@/queries/projects/useUploadImageFolderQuery';
|
||||||
|
|
||||||
export default function WorkspaceDropdownMenu({
|
export default function WorkspaceDropdownMenu({
|
||||||
projectId,
|
projectId,
|
||||||
@ -31,6 +32,19 @@ export default function WorkspaceDropdownMenu({
|
|||||||
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
||||||
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
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 handleFileCount = (fileCount: number) => {
|
||||||
|
setFileCount(fileCount);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -38,13 +52,7 @@ export default function WorkspaceDropdownMenu({
|
|||||||
<Menu size={16} />
|
<Menu size={16} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56">
|
<DropdownMenuContent className="w-56">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem onClick={() => console.log('프로젝트 이름 수정')}>프로젝트 이름 수정</DropdownMenuItem>
|
||||||
onClick={() => {
|
|
||||||
console.log('프로젝트 이름 수정');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
프로젝트 이름 수정
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => setIsOpenUploadFile(true)}>파일 업로드</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => setIsOpenUploadFile(true)}>파일 업로드</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setIsOpenUploadPresigned(true)}>
|
<DropdownMenuItem onClick={() => setIsOpenUploadPresigned(true)}>
|
||||||
@ -67,12 +75,16 @@ export default function WorkspaceDropdownMenu({
|
|||||||
<DialogTrigger asChild></DialogTrigger>
|
<DialogTrigger asChild></DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
|
<DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
|
||||||
<ImageUploadFileForm
|
<ImageUploadForm
|
||||||
onClose={() => setIsOpenUploadFile(false)}
|
onClose={handleCloseUploadFile}
|
||||||
onRefetch={onRefetch}
|
onRefetch={onRefetch}
|
||||||
onFileCount={(fileCount: number) => setFileCount(fileCount)}
|
onFileCount={(fileCount: number) => setFileCount(fileCount)}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
folderId={folderId}
|
folderId={folderId}
|
||||||
|
uploadImageZipMutation={uploadImageZipMutation}
|
||||||
|
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
|
||||||
|
uploadImageFileMutation={uploadImageFileMutation}
|
||||||
|
uploadImageFolderMutation={uploadImageFolderMutation}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -103,11 +115,17 @@ export default function WorkspaceDropdownMenu({
|
|||||||
<DialogTrigger asChild></DialogTrigger>
|
<DialogTrigger asChild></DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader title="폴더 업로드 (파일 업로드 API 이용)" />
|
<DialogHeader title="폴더 업로드 (파일 업로드 API 이용)" />
|
||||||
<ImageUploadFolderFileForm
|
<ImageUploadForm
|
||||||
onClose={() => setIsOpenUploadFolderFile(false)}
|
onClose={handleCloseUploadFolderFile}
|
||||||
onRefetch={onRefetch}
|
onRefetch={onRefetch}
|
||||||
|
onFileCount={handleFileCount}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
folderId={folderId}
|
folderId={folderId}
|
||||||
|
isFolderUpload={true}
|
||||||
|
uploadImageZipMutation={uploadImageZipMutation}
|
||||||
|
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
|
||||||
|
uploadImageFileMutation={uploadImageFileMutation}
|
||||||
|
uploadImageFolderMutation={uploadImageFolderMutation}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -119,11 +137,17 @@ export default function WorkspaceDropdownMenu({
|
|||||||
<DialogTrigger asChild></DialogTrigger>
|
<DialogTrigger asChild></DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader title="폴더 업로드 (백엔드 구현 필요)" />
|
<DialogHeader title="폴더 업로드 (백엔드 구현 필요)" />
|
||||||
<ImageUploadFolderForm
|
<ImageUploadForm
|
||||||
onClose={() => setIsOpenUploadFolder(false)}
|
onClose={handleCloseUploadFolder}
|
||||||
onRefetch={onRefetch}
|
onRefetch={onRefetch}
|
||||||
|
onFileCount={handleFileCount}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
folderId={folderId}
|
folderId={folderId}
|
||||||
|
isFolderBackendUpload={true}
|
||||||
|
uploadImageZipMutation={uploadImageZipMutation}
|
||||||
|
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
|
||||||
|
uploadImageFileMutation={uploadImageFileMutation}
|
||||||
|
uploadImageFolderMutation={uploadImageFolderMutation}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -135,11 +159,17 @@ export default function WorkspaceDropdownMenu({
|
|||||||
<DialogTrigger asChild></DialogTrigger>
|
<DialogTrigger asChild></DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader title="폴더 압축파일 업로드" />
|
<DialogHeader title="폴더 압축파일 업로드" />
|
||||||
<ImageUploadZipForm
|
<ImageUploadForm
|
||||||
onClose={() => setIsOpenUploadZip(false)}
|
onClose={handleCloseUploadZip}
|
||||||
onRefetch={onRefetch}
|
onRefetch={onRefetch}
|
||||||
|
onFileCount={handleFileCount}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
folderId={folderId}
|
folderId={folderId}
|
||||||
|
isZipUpload={true}
|
||||||
|
uploadImageZipMutation={uploadImageZipMutation}
|
||||||
|
uploadImageFolderFileMutation={uploadImageFolderFileMutation}
|
||||||
|
uploadImageFileMutation={uploadImageFileMutation}
|
||||||
|
uploadImageFolderMutation={uploadImageFolderMutation}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -1,20 +1,10 @@
|
|||||||
import { uploadImageFolderFile } from '@/api/imageApi';
|
import { uploadImageFolderFile } from '@/api/imageApi';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { UploadFolderParams } from '@/types/uploadTypes';
|
||||||
|
|
||||||
export default function useUploadImageFolderFileQuery() {
|
export default function useUploadImageFolderFileQuery() {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({
|
mutationFn: ({ memberId, projectId, folderId, files, progressCallback }: UploadFolderParams) =>
|
||||||
memberId,
|
uploadImageFolderFile(memberId, projectId, folderId, files, progressCallback),
|
||||||
projectId,
|
|
||||||
folderId,
|
|
||||||
files,
|
|
||||||
progressCallback,
|
|
||||||
}: {
|
|
||||||
memberId: number;
|
|
||||||
projectId: number;
|
|
||||||
folderId: number;
|
|
||||||
files: File[];
|
|
||||||
progressCallback: (progress: number) => void;
|
|
||||||
}) => uploadImageFolderFile(memberId, projectId, folderId, files, progressCallback),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,10 @@
|
|||||||
import { uploadImageZip } from '@/api/imageApi';
|
import { uploadImageZip } from '@/api/imageApi';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { UploadZipParams } from '@/types/uploadTypes';
|
||||||
|
|
||||||
export default function useUploadImageZipQuery() {
|
export default function useUploadImageZipQuery() {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({
|
mutationFn: ({ memberId, projectId, folderId, file, progressCallback }: UploadZipParams) =>
|
||||||
memberId,
|
uploadImageZip(memberId, projectId, folderId, file, progressCallback),
|
||||||
projectId,
|
|
||||||
folderId,
|
|
||||||
file,
|
|
||||||
progressCallback,
|
|
||||||
}: {
|
|
||||||
memberId: number;
|
|
||||||
projectId: number;
|
|
||||||
folderId: number;
|
|
||||||
file: File;
|
|
||||||
progressCallback: (progress: number) => void;
|
|
||||||
}) => uploadImageZip(memberId, projectId, folderId, file, progressCallback),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,4 @@ export * from './projectTypes';
|
|||||||
export * from './labelTypes';
|
export * from './labelTypes';
|
||||||
export * from './fileTypes';
|
export * from './fileTypes';
|
||||||
export * from './reportTypes';
|
export * from './reportTypes';
|
||||||
|
export * from './uploadTypes';
|
||||||
|
15
frontend/src/types/uploadTypes.ts
Normal file
15
frontend/src/types/uploadTypes.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export interface UploadZipParams {
|
||||||
|
memberId: number;
|
||||||
|
projectId: number;
|
||||||
|
folderId: number;
|
||||||
|
file: File;
|
||||||
|
progressCallback: (progress: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadFolderParams {
|
||||||
|
memberId: number;
|
||||||
|
projectId: number;
|
||||||
|
folderId: number;
|
||||||
|
files: File[];
|
||||||
|
progressCallback: (progress: number) => void;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user