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:
commit
5baed15fdb
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
35
frontend/src/components/ImageFolderUploadModal/index.tsx
Normal file
35
frontend/src/components/ImageFolderUploadModal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
41
frontend/src/components/ImageFolderZipUploadModal/index.tsx
Normal file
41
frontend/src/components/ImageFolderZipUploadModal/index.tsx
Normal 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
7
frontend/src/index.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import 'react';
|
||||
|
||||
declare module 'react' {
|
||||
interface InputHTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
|
||||
webkitdirectory?: string;
|
||||
}
|
||||
}
|
21
frontend/src/pages/ImageFolderUploadTest.tsx
Normal file
21
frontend/src/pages/ImageFolderUploadTest.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -273,3 +273,10 @@ export interface ErrorResponse {
|
||||
message: string;
|
||||
isSuccess: boolean;
|
||||
}
|
||||
|
||||
export interface ImageFolderRequest {
|
||||
memberId: number;
|
||||
projectId: number;
|
||||
parentId: number;
|
||||
files: File[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user