diff --git a/frontend/src/components/ImageUploadModal/ImageUploadForm.tsx b/frontend/src/components/ImageUploadModal/ImageUploadForm.tsx new file mode 100644 index 0000000..610cca7 --- /dev/null +++ b/frontend/src/components/ImageUploadModal/ImageUploadForm.tsx @@ -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; + uploadImageFolderFileMutation: UseMutationResult; + uploadImageFileMutation: UseMutationResult; + uploadImageFolderMutation: UseMutationResult; +} + +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([]); + const [inputKey, setInputKey] = useState(0); + const [isDragging, setIsDragging] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [isUploaded, setIsUploaded] = useState(false); + const [isFailed, setIsFailed] = useState(false); + const [progress, setProgress] = useState(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) => { + const newFiles = event.target.files; + + if (newFiles) { + setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]); + } + + event.target.value = ''; + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragging(true); + }; + + const handleDragLeave = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragging(false); + }; + + const handleDrop = (event: React.DragEvent) => { + 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 ( +
+ {!isUploading && ( +
+ + {isDragging ? ( +

+ {isFolderUpload ? '드래그한 폴더를 여기에 놓으세요' : '드래그한 파일을 여기에 놓으세요'} +

+ ) : ( +

+ {isFolderUpload + ? '폴더를 업로드하려면 여기를 클릭하거나 폴더를 드래그하여 여기에 놓으세요' + : '파일을 업로드하려면 여기를 클릭하거나 파일을 드래그하여 여기에 놓으세요'} +

+ )} +
+ )} + {files.length > 0 && ( +
    + + {({ index, style }) => ( +
  • + {files[index].webkitRelativePath || files[index].name} + {isUploading ? ( +
    + {isUploaded ? ( + + ) : isFailed ? ( + + ) : ( + + )} +
    + ) : ( + + )} +
  • + )} +
    +
+ )} + {isUploading ? ( + + ) : ( + + )} +
+ ); +} diff --git a/frontend/src/components/ImageUploadModal/index.tsx b/frontend/src/components/ImageUploadModal/index.tsx new file mode 100644 index 0000000..82a9d9f --- /dev/null +++ b/frontend/src/components/ImageUploadModal/index.tsx @@ -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(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 ( + + + + + + 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} /> + + + + ); +} diff --git a/frontend/src/components/ImageUploadPresignedModal/ImageUploadPresignedForm.tsx b/frontend/src/components/ImageUploadPresignedModal/ImageUploadPresignedForm.tsx index e5ae981..3b09573 100644 --- a/frontend/src/components/ImageUploadPresignedModal/ImageUploadPresignedForm.tsx +++ b/frontend/src/components/ImageUploadPresignedModal/ImageUploadPresignedForm.tsx @@ -6,12 +6,12 @@ import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react'; import useUploadImagePresignedQuery from '@/queries/projects/useUploadImagePresignedQuery.ts'; export default function ImageUploadPresignedForm({ - onClose, - onRefetch, - onFileCount, - projectId, - folderId, - }: { + onClose, + onRefetch, + onFileCount, + projectId, + folderId, +}: { onClose: () => void; onRefetch?: () => void; onFileCount: (fileCount: number) => void; @@ -26,11 +26,10 @@ export default function ImageUploadPresignedForm({ const [isUploading, setIsUploading] = useState(false); const [isUploaded, setIsUploaded] = useState(false); const [isFailed, setIsFailed] = useState(false); - const [uploadStatus, setUploadStatus] = useState<(boolean | null)[]>([]); // 각 파일의 성공/실패 여부 관리 + const [uploadStatus, setUploadStatus] = useState<(boolean | null)[]>([]); const uploadImageFile = useUploadImagePresignedQuery(); - const handleClose = () => { onClose(); }; @@ -51,7 +50,7 @@ export default function ImageUploadPresignedForm({ }); setFiles((prevFiles) => [...prevFiles, ...newImages]); - setUploadStatus((prevState) => [...prevState, ...newImages.map(()=> null)]); + setUploadStatus((prevState) => [...prevState, ...newImages.map(() => null)]); } event.target.value = ''; @@ -59,20 +58,38 @@ export default function ImageUploadPresignedForm({ const handleDragOver = (event: React.DragEvent) => { event.preventDefault(); + event.stopPropagation(); + setIsDragging(true); }; const handleDragLeave = (event: React.DragEvent) => { event.preventDefault(); + event.stopPropagation(); setIsDragging(false); }; - const handleDrop = () => { + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + 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) => { - setFiles(files.filter((_, i) => i != index)); + setFiles(files.filter((_, i) => i !== index)); + setUploadStatus((prevState) => prevState.filter((_, i) => i !== index)); }; const handleUpload = async () => { @@ -85,7 +102,6 @@ export default function ImageUploadPresignedForm({ folderId, files, progressCallback: (index: number) => { - // 업로드 성공하면 상태 업데이트 setUploadStatus((prevStatus) => { const newStatus = [...prevStatus]; newStatus[index] = true; // 업로드 성공 시 true @@ -100,18 +116,14 @@ export default function ImageUploadPresignedForm({ }, onError: () => { setIsFailed(true); - setUploadStatus((prevStatus) => - prevStatus.map((status) => (status === null ? false : status)) - ); // 실패 시 처리 + setUploadStatus((prevStatus) => prevStatus.map((status) => (status === null ? false : status))); // 실패 시 처리 }, - }, + } ); }; // 전체 진행 상황 계산 - const totalProgress = Math.round( - (uploadStatus.filter((status) => status !== null).length / files.length) * 100 - ); + const totalProgress = Math.round((uploadStatus.filter((status) => status !== null).length / files.length) * 100); useEffect(() => { onFileCount(files.length); @@ -123,8 +135,11 @@ export default function ImageUploadPresignedForm({
{isDragging ? (

드래그한 파일을 여기에 놓으세요

@@ -151,21 +163,43 @@ export default function ImageUploadPresignedForm({ {files.length > 0 && (
    {files.map((file, index) => ( -
  • +
  • {file.name} {isUploading ? (
    {uploadStatus[index] === true ? ( - + ) : uploadStatus[index] === false ? ( - + ) : ( - + )}
    ) : ( - )}
  • @@ -173,7 +207,10 @@ export default function ImageUploadPresignedForm({
)} {isUploading ? ( -