Merge branch 'fe/feat/folder-upload' into 'fe/develop'

Feat: 폴더 업로드 구현, 폴더 압축파일 업로드 구현 - S11P21S002-191

See merge request s11-s-project/S11P21S002!117
This commit is contained in:
홍창기 2024-09-22 13:10:52 +09:00
commit 5baed15fdb
9 changed files with 451 additions and 1 deletions

View File

@ -39,7 +39,7 @@ export async function changeImageStatus(
.then(({ data }) => data);
}
export async function uploadImageList(projectId: number, folderId: number, memberId: number, imageList: string[]) {
export async function uploadImageList(projectId: number, folderId: number, memberId: number, imageList: File[]) {
return api
.post(
`/projects/${projectId}/folders/${folderId}/images`,
@ -50,3 +50,48 @@ export async function uploadImageList(projectId: number, folderId: number, membe
)
.then(({ data }) => data);
}
export async function uploadImageFolder(memberId: number, projectId: number, files: File[], parentId: number = 0) {
const formData = new FormData();
files.forEach((file) => {
formData.append('files', file);
});
return api
.post(
`/projects/${projectId}/folders/${0}/images/upload`,
{ folderZip: files, parentId }
// {
// params: { memberId },
// }
)
.then(({ data }) => data)
.catch((error) => {
return Promise.reject(error);
});
}
export async function uploadImageFolderZip(memberId: number, projectId: number, file: File, parentId: number = 0) {
const formData = new FormData();
formData.append('folderZip', file);
formData.append('parentId', parentId.toString());
// const jsonData = {
// parentId,
// };
// const blob = new Blob([JSON.stringify(jsonData)], { type: 'application/json' });
// formData.append('parentId', blob);
return api
.post(
`/projects/${projectId}/folders/${0}/images/upload`,
formData
// {
// params: { memberId },
// }
)
.then(({ data }) => data)
.catch((error) => {
return Promise.reject(error);
});
}

View File

@ -0,0 +1,146 @@
import { useState } from 'react';
import { Button } from '../ui/button';
import { cn } from '@/lib/utils';
import { uploadImageFolder } from '@/api/imageApi';
import useAuthStore from '@/stores/useAuthStore';
import { X } from 'lucide-react';
export default function ImageFolderUploadForm({
onClose,
projectId,
parentId,
}: {
onClose: () => void;
projectId: number;
parentId: number;
}) {
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const [files, setFiles] = useState<File[]>([]);
const [isDragging, setIsDragging] = useState<boolean>(false);
const [isUploading, setIsUploading] = useState<boolean>(false);
const [progress, setProgress] = useState<number>(0);
const [isFailed, setIsFailed] = useState<boolean>(false);
const handleClose = () => {
onClose();
};
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 = (index: number) => {
setFiles(files.filter((_, i) => i != index));
};
const handleUpload = async () => {
setIsUploading(true);
setProgress(0);
await uploadImageFolder(memberId, projectId, files, parentId)
.then(() => {
setProgress(100);
})
.catch(() => {
setProgress(100);
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
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 && (
<ul className="m-0 list-none p-0">
{files.map((file, index) => (
<li
key={index}
className={cn('flex items-center justify-between p-1')}
>
<span className="truncate">{file.webkitRelativePath || file.name}</span>
<button
className={'cursor-pointer p-2'}
onClick={() => handleRemoveFile(index)}
>
<X
color="red"
size={16}
strokeWidth="2"
/>
</button>
</li>
))}
</ul>
)}
{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={progress != 100}
>
{progress === 100 ? (isFailed ? '업로드 실패 (닫기)' : '업로드 완료 (닫기)') : `업로드 중... ${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 ImageFolderUploadForm from './ImageFolderUploadForm';
export default function ImageFolderUploadModal({ projectId, parentId = 0 }: { projectId: number; parentId: 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="폴더 업로드" />
<ImageFolderUploadForm
onClose={handleClose}
projectId={projectId}
parentId={parentId}
/>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,142 @@
import { useState } from 'react';
import { Button } from '../ui/button';
import { cn } from '@/lib/utils';
import { uploadImageFolderZip } from '@/api/imageApi';
import useAuthStore from '@/stores/useAuthStore';
import { X } from 'lucide-react';
export default function ImageFolderZipUploadForm({
onClose,
projectId,
parentId,
}: {
onClose: () => void;
projectId: number;
parentId: number;
}) {
const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0;
const [file, setFile] = useState<File>();
const [isDragging, setIsDragging] = useState<boolean>(false);
const [isUploading, setIsUploading] = useState<boolean>(false);
const [progress, setProgress] = useState<number>(0);
const [isFailed, setIsFailed] = useState<boolean>(false);
const handleClose = () => {
onClose();
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newFiles = event.target.files;
if (newFiles) {
setFile(newFiles[0]);
}
};
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 = () => {
setFile(undefined);
};
const handleUpload = async () => {
if (file) {
setIsUploading(true);
setProgress(0);
await uploadImageFolderZip(memberId, projectId, file, parentId)
.then(() => {
setProgress(100);
})
.catch(() => {
setProgress(100);
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
type="file"
accept=".zip"
// 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>
)}
{file && (
<div className={'flex items-center justify-between p-1'}>
<span className="truncate">{file.webkitRelativePath || file.name}</span>
<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={progress != 100}
>
{progress === 100 ? (isFailed ? '업로드 실패 (닫기)' : '업로드 완료 (닫기)') : `업로드 중... ${progress}%`}
</Button>
) : (
<Button
onClick={handleUpload}
variant="outlinePrimary"
disabled={!file}
>
</Button>
)}
</div>
);
}

View File

@ -0,0 +1,41 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
import { Plus } from 'lucide-react';
import ImageFolderZipUploadForm from './ImageFolderZipUploadForm';
export default function ImageFolderZipUploadModal({
projectId,
parentId = 0,
}: {
projectId: number;
parentId: 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="폴더 압축파일 업로드" />
<ImageFolderZipUploadForm
onClose={handleClose}
projectId={projectId}
parentId={parentId}
/>
</DialogContent>
</Dialog>
);
}

7
frontend/src/index.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import 'react';
declare module 'react' {
interface InputHTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
webkitdirectory?: string;
}
}

View File

@ -0,0 +1,21 @@
import ImageFolderUploadModal from '@/components/ImageFolderUploadModal';
import ImageFolderZipUploadModal from '@/components/ImageFolderZipUploadModal';
import { useParams } from 'react-router-dom';
export default function ImageFolderUploadTest() {
const params = useParams<{ workspaceId: string; projectId: string }>();
const projectId = Number(params.projectId);
return (
<div className="min-h-screen w-full">
<ImageFolderUploadModal
projectId={projectId}
parentId={0}
/>
<ImageFolderZipUploadModal
projectId={projectId}
parentId={0}
/>
</div>
);
}

View File

@ -14,6 +14,7 @@ import WorkspaceBrowseIndex from '@/pages/WorkspaceBrowseIndex';
import AdminIndex from '@/pages/AdminIndex';
import LabelCanvas from '@/pages/LabelCanvas';
import ReviewDetail from '@/components/ReviewDetail';
import ImageFolderUploadTest from '@/pages/ImageFolderUploadTest';
export const webPath = {
home: () => '/',
@ -21,6 +22,7 @@ export const webPath = {
workspace: () => '/workspace',
admin: () => `/admin`,
oauthCallback: () => '/redirect/oauth2',
imageFolderUploadTest: () => '/imagefolderuploadtest',
};
const router = createBrowserRouter([
@ -106,6 +108,10 @@ const router = createBrowserRouter([
</Suspense>
),
},
{
path: `${webPath.imageFolderUploadTest()}/:projectId`,
element: <ImageFolderUploadTest />,
},
]);
export default router;

View File

@ -273,3 +273,10 @@ export interface ErrorResponse {
message: string;
isSuccess: boolean;
}
export interface ImageFolderRequest {
memberId: number;
projectId: number;
parentId: number;
files: File[];
}