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

Refactor: 폴더 업로드 시 같은 폴더 다시 추가하면 선택 안되는 문제 수정, 파일 목록 대신 폴더 이름만 보이도록 변경 - S11P21S002-243

See merge request s11-s-project/S11P21S002!246
This commit is contained in:
정현조 2024-09-30 14:32:04 +09:00
commit 8cd7d545fb
7 changed files with 341 additions and 51 deletions

View File

@ -56,7 +56,7 @@ export async function uploadImageFile(
.then(({ data }) => data);
}
export async function uploadImageFolder(
export async function uploadImageFolderFile(
memberId: number,
projectId: number,
folderId: number,
@ -81,6 +81,31 @@ export async function uploadImageFolder(
.then(({ data }) => data);
}
export async function uploadImageFolder(
memberId: number,
projectId: number,
folderId: number,
files: File[],
processCallback: (progress: number) => void
) {
const formData = new FormData();
files.forEach((file) => {
formData.append('imageList', file);
});
return api
.post(`/projects/${projectId}/folders/${folderId}/images/folder`, formData, {
params: { memberId },
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
processCallback(progress);
}
},
})
.then(({ data }) => data);
}
export async function uploadImageZip(
memberId: number,
projectId: number,

View File

@ -0,0 +1,190 @@
import { useState } 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 useUploadImageFolderFileQuery from '@/queries/projects/useUploadImageFolderFileQuery';
export default function ImageUploadFolderFileForm({
onClose,
onRefetch,
projectId,
folderId,
}: {
onClose: () => void;
onRefetch?: () => void;
projectId: number;
folderId: number;
}) {
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 uploadImageFolderFile = useUploadImageFolderFileQuery();
const handleClose = () => {
onClose();
};
const handleRefetch = () => {
if (onRefetch) {
onRefetch();
}
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newFiles = event.target.files;
if (newFiles) {
setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]);
}
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragging(false);
};
const handleDrop = () => {
setIsDragging(false);
};
const handleRemoveFile = () => {
setFiles([]);
setInputKey((prevKey) => prevKey + 1);
};
const handleUpload = async () => {
setIsUploading(true);
uploadImageFolderFile.mutate(
{
memberId,
projectId,
folderId,
files,
progressCallback: (progress: number) => {
setProgress(progress);
},
},
{
onSuccess: () => {
handleRefetch();
setIsUploaded(true);
},
onError: () => {
setIsFailed(true);
},
}
);
};
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'
)}
>
<input
key={inputKey}
type="file"
webkitdirectory=""
// multiple
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
onChange={handleChange}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
/>
{isDragging ? (
<p className="text-primary"> </p>
) : (
<p className="text-gray-500">
<br />
</p>
)}
</div>
)}
{files.length > 0 && (
<div className={'flex items-center justify-between p-1'}>
<span className="truncate">
{files[0].webkitRelativePath.substring(0, files[0].webkitRelativePath.indexOf('/'))} {}
</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}
>
<X
color="red"
size={16}
strokeWidth="2"
/>
</button>
)}
</div>
)}
{isUploading ? (
<Button
onClick={handleClose}
variant="outlinePrimary"
className={
isFailed
? 'border-red-500 text-red-500 hover:bg-red-500 dark:border-red-500 dark:text-red-500 dark:hover:bg-red-500'
: ''
}
disabled={!isUploaded && !isFailed}
>
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
</Button>
) : (
<Button
onClick={handleUpload}
variant="outlinePrimary"
disabled={files.length === 0}
>
</Button>
)}
</div>
);
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
import { Plus } from 'lucide-react';
import ImageUploadFolderFileForm from './ImageUploadFolderFileForm';
export default function ImageUploadFolderFileModal({ projectId, folderId }: { projectId: number; folderId: number }) {
const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
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="폴더 업로드 (파일 업로드 API 이용)" />
<ImageUploadFolderFileForm
onClose={handleClose}
projectId={projectId}
folderId={folderId}
/>
</DialogContent>
</Dialog>
);
}

View File

@ -20,6 +20,7 @@ export default function ImageUploadFolderForm({
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);
@ -44,8 +45,6 @@ export default function ImageUploadFolderForm({
if (newFiles) {
setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]);
}
event.target.value = '';
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
@ -62,8 +61,9 @@ export default function ImageUploadFolderForm({
setIsDragging(false);
};
const handleRemoveFile = (index: number) => {
setFiles(files.filter((_, i) => i != index));
const handleRemoveFile = () => {
setFiles([]);
setInputKey((prevKey) => prevKey + 1);
};
const handleUpload = async () => {
@ -101,6 +101,7 @@ export default function ImageUploadFolderForm({
)}
>
<input
key={inputKey}
type="file"
webkitdirectory=""
// multiple
@ -122,50 +123,45 @@ export default function ImageUploadFolderForm({
</div>
)}
{files.length > 0 && (
<ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
{files.map((file, index) => (
<li
key={index}
className="flex items-center justify-between p-1"
>
<span className="truncate">{file.webkitRelativePath || file.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>
<div className={'flex items-center justify-between p-1'}>
<span className="truncate">
{files[0].webkitRelativePath.substring(0, files[0].webkitRelativePath.indexOf('/'))} {}
</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"
/>
) : (
<button
className={'cursor-pointer p-2'}
onClick={() => handleRemoveFile(index)}
>
<X
color="red"
size={16}
strokeWidth="2"
/>
</button>
<CircleDashed
className="stroke-gray-500"
size={16}
strokeWidth="2"
/>
)}
</li>
))}
</ul>
</div>
) : (
<button
className={'cursor-pointer p-2'}
onClick={handleRemoveFile}
>
<X
color="red"
size={16}
strokeWidth="2"
/>
</button>
)}
</div>
)}
{isUploading ? (
<Button

View File

@ -23,7 +23,7 @@ export default function ImageUploadFolderModal({ projectId, folderId }: { projec
</button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 업로드" />
<DialogHeader title="폴더 업로드 (백엔드 구현 필요)" />
<ImageUploadFolderForm
onClose={handleClose}
projectId={projectId}

View File

@ -9,6 +9,7 @@ import {
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
import ImageUploadFileForm from '../ImageUploadFileModal/ImageUploadFileForm';
import React from 'react';
import ImageUploadFolderFileForm from '../ImageUploadFolderFileModal/ImageUploadFolderFileForm';
import ImageUploadFolderForm from '../ImageUploadFolderModal/ImageUploadFolderForm';
import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
@ -23,6 +24,7 @@ export default function WorkspaceDropdownMenu({
}) {
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false);
const [fileCount, setFileCount] = 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);
@ -32,6 +34,12 @@ export default function WorkspaceDropdownMenu({
setIsOpenUploadFile(false);
};
const handleOpenUploadFolderFile = () => setIsOpenUploadFolderFile(true);
const handleCloseUploadFolderFile = () => {
setIsOpenUploadFolderFile(false);
};
const handleOpenUploadFolder = () => setIsOpenUploadFolder(true);
const handleCloseUploadFolder = () => {
@ -45,7 +53,6 @@ export default function WorkspaceDropdownMenu({
};
const handleFileCount = (fileCount: number) => {
console.log(fileCount);
setFileCount(fileCount);
};
@ -68,7 +75,8 @@ export default function WorkspaceDropdownMenu({
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleOpenUploadFile}> </DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenUploadFolder}> ()</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenUploadFolderFile}> ( API )</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenUploadFolder}> ( )</DropdownMenuItem>
<DropdownMenuItem onClick={handleOpenUploadZip}> </DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -90,13 +98,29 @@ export default function WorkspaceDropdownMenu({
</DialogContent>
</Dialog>
<Dialog
open={isOpenUploadFolderFile}
onOpenChange={setIsOpenUploadFolderFile}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 업로드 (파일 업로드 API 이용)" />
<ImageUploadFolderFileForm
onClose={handleCloseUploadFolderFile}
onRefetch={onRefetch}
projectId={projectId}
folderId={folderId}
/>
</DialogContent>
</Dialog>
<Dialog
open={isOpenUploadFolder}
onOpenChange={setIsOpenUploadFolder}
>
<DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader title="폴더 업로드 (임시)" />
<DialogHeader title="폴더 업로드 (백엔드 구현 필요)" />
<ImageUploadFolderForm
onClose={handleCloseUploadFolder}
onRefetch={onRefetch}

View File

@ -0,0 +1,20 @@
import { uploadImageFolderFile } from '@/api/imageApi';
import { useMutation } from '@tanstack/react-query';
export default function useUploadImageFolderFileQuery() {
return useMutation({
mutationFn: ({
memberId,
projectId,
folderId,
files,
progressCallback,
}: {
memberId: number;
projectId: number;
folderId: number;
files: File[];
progressCallback: (progress: number) => void;
}) => uploadImageFolderFile(memberId, projectId, folderId, files, progressCallback),
});
}