diff --git a/frontend/src/api/imageApi.ts b/frontend/src/api/imageApi.ts index 57b87f1..7c315ac 100644 --- a/frontend/src/api/imageApi.ts +++ b/frontend/src/api/imageApi.ts @@ -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); + }); +} diff --git a/frontend/src/components/ImageFolderUploadModal/ImageFolderUploadForm.tsx b/frontend/src/components/ImageFolderUploadModal/ImageFolderUploadForm.tsx new file mode 100644 index 0000000..f73f227 --- /dev/null +++ b/frontend/src/components/ImageFolderUploadModal/ImageFolderUploadForm.tsx @@ -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([]); + const [isDragging, setIsDragging] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [progress, setProgress] = useState(0); + const [isFailed, setIsFailed] = useState(false); + + const handleClose = () => { + onClose(); + }; + + const handleChange = (event: React.ChangeEvent) => { + const newFiles = event.target.files; + + if (newFiles) { + setFiles((prevFiles) => [...prevFiles, ...Array.from(newFiles)]); + } + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (event: React.DragEvent) => { + 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 ( +
+ {!isUploading && ( +
+ + {isDragging ? ( +

드래그한 폴더를 여기에 놓으세요

+ ) : ( +

+ 폴더를 업로드하려면 여기를 클릭하거나 +
+ 폴더를 드래그하여 여기에 놓으세요 +

+ )} +
+ )} + {files.length > 0 && ( +
    + {files.map((file, index) => ( +
  • + {file.webkitRelativePath || file.name} + +
  • + ))} +
+ )} + {isUploading ? ( + + ) : ( + + )} +
+ ); +} diff --git a/frontend/src/components/ImageFolderUploadModal/index.tsx b/frontend/src/components/ImageFolderUploadModal/index.tsx new file mode 100644 index 0000000..2a98405 --- /dev/null +++ b/frontend/src/components/ImageFolderUploadModal/index.tsx @@ -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 ( + + + + + + + + + + ); +} diff --git a/frontend/src/components/ImageFolderZipUploadModal/ImageFolderZipUploadForm.tsx b/frontend/src/components/ImageFolderZipUploadModal/ImageFolderZipUploadForm.tsx new file mode 100644 index 0000000..39ac97c --- /dev/null +++ b/frontend/src/components/ImageFolderZipUploadModal/ImageFolderZipUploadForm.tsx @@ -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(); + const [isDragging, setIsDragging] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [progress, setProgress] = useState(0); + const [isFailed, setIsFailed] = useState(false); + + const handleClose = () => { + onClose(); + }; + + const handleChange = (event: React.ChangeEvent) => { + const newFiles = event.target.files; + + if (newFiles) { + setFile(newFiles[0]); + } + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (event: React.DragEvent) => { + 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 ( +
+ {!isUploading && ( +
+ + {isDragging ? ( +

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

+ ) : ( +

+ 파일을 업로드하려면 여기를 클릭하거나 +
+ 파일을 드래그하여 여기에 놓으세요 +

+ )} +
+ )} + {file && ( +
+ {file.webkitRelativePath || file.name} + +
+ )} + {isUploading ? ( + + ) : ( + + )} +
+ ); +} diff --git a/frontend/src/components/ImageFolderZipUploadModal/index.tsx b/frontend/src/components/ImageFolderZipUploadModal/index.tsx new file mode 100644 index 0000000..d143280 --- /dev/null +++ b/frontend/src/components/ImageFolderZipUploadModal/index.tsx @@ -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 ( + + + + + + + + + + ); +} diff --git a/frontend/src/index.d.ts b/frontend/src/index.d.ts new file mode 100644 index 0000000..6df8161 --- /dev/null +++ b/frontend/src/index.d.ts @@ -0,0 +1,7 @@ +import 'react'; + +declare module 'react' { + interface InputHTMLAttributes extends AriaAttributes, DOMAttributes { + webkitdirectory?: string; + } +} diff --git a/frontend/src/pages/ImageFolderUploadTest.tsx b/frontend/src/pages/ImageFolderUploadTest.tsx new file mode 100644 index 0000000..266ccf6 --- /dev/null +++ b/frontend/src/pages/ImageFolderUploadTest.tsx @@ -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 ( +
+ + +
+ ); +} diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 7b33116..dbf88a0 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -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([ ), }, + { + path: `${webPath.imageFolderUploadTest()}/:projectId`, + element: , + }, ]); export default router; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 91eb091..f4486cc 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -273,3 +273,10 @@ export interface ErrorResponse { message: string; isSuccess: boolean; } + +export interface ImageFolderRequest { + memberId: number; + projectId: number; + parentId: number; + files: File[]; +}