Merge branch 'fe/fix/upload' into 'fe/develop'
Refactor: 퍼센테이지 복구 See merge request s11-s-project/S11P21S002!301
This commit is contained in:
commit
d87b77f369
BIN
frontend/src/assets/icons/home_background.webp
Normal file
BIN
frontend/src/assets/icons/home_background.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 622 KiB |
BIN
frontend/src/assets/icons/web_light_rd_ctn@1x.png
Normal file
BIN
frontend/src/assets/icons/web_light_rd_ctn@1x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
@ -5,7 +5,6 @@ import useAuthStore from '@/stores/useAuthStore';
|
|||||||
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
import useUploadFiles from '@/hooks/useUploadFiles';
|
import useUploadFiles from '@/hooks/useUploadFiles';
|
||||||
import useUploadImagePresignedQuery from '@/queries/images/useUploadImagePresignedQuery';
|
|
||||||
import { unzipFilesWithPath, extractFilesRecursivelyWithPath } from '@/utils/fileUtils';
|
import { unzipFilesWithPath, extractFilesRecursivelyWithPath } from '@/utils/fileUtils';
|
||||||
|
|
||||||
interface ImagePreSignedFormProps {
|
interface ImagePreSignedFormProps {
|
||||||
@ -34,20 +33,16 @@ export default function ImagePreSignedForm({
|
|||||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||||
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
const [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||||
const [isFailed, setIsFailed] = useState<boolean>(false);
|
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||||
const [progress, setProgress] = useState<number>(0);
|
const [uploadStatus, setUploadStatus] = useState<('uploading' | 'success' | 'failed' | null)[]>([]);
|
||||||
const [uploadStatus, setUploadStatus] = useState<(boolean | null)[]>([]);
|
|
||||||
|
|
||||||
// Ensure to destructure the uploadFiles function properly from the hook
|
|
||||||
const { uploadFiles } = useUploadFiles();
|
const { uploadFiles } = useUploadFiles();
|
||||||
const uploadImageFile = useUploadImagePresignedQuery();
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
setInputKey((prevKey) => prevKey + 1);
|
setInputKey((prevKey) => prevKey + 1);
|
||||||
setIsUploading(false);
|
|
||||||
setIsUploaded(false);
|
|
||||||
setIsFailed(false);
|
setIsFailed(false);
|
||||||
setProgress(0);
|
setIsUploaded(false);
|
||||||
setUploadStatus([]);
|
setUploadStatus([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -92,9 +87,10 @@ export default function ImagePreSignedForm({
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
|
let processedFiles: { path: string; file: File }[] = [];
|
||||||
|
|
||||||
if (uploadType === 'folder') {
|
if (uploadType === 'folder') {
|
||||||
const droppedItems = event.dataTransfer.items;
|
const droppedItems = event.dataTransfer.items;
|
||||||
let processedFiles: { path: string; file: File }[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < droppedItems.length; i++) {
|
for (let i = 0; i < droppedItems.length; i++) {
|
||||||
const item = droppedItems[i];
|
const item = droppedItems[i];
|
||||||
@ -106,20 +102,17 @@ export default function ImagePreSignedForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFiles((prevFiles) => [...prevFiles, ...processedFiles]);
|
|
||||||
setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]);
|
|
||||||
} else {
|
} else {
|
||||||
const droppedFiles = event.dataTransfer.files;
|
const droppedFiles = event.dataTransfer.files;
|
||||||
if (droppedFiles) {
|
if (droppedFiles) {
|
||||||
const processedFiles: { path: string; file: File }[] = [];
|
|
||||||
for (const file of Array.from(droppedFiles)) {
|
for (const file of Array.from(droppedFiles)) {
|
||||||
processedFiles.push({ path: file.name, file });
|
processedFiles.push({ path: file.name, file });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setFiles((prevFiles) => [...prevFiles, ...processedFiles]);
|
setFiles((prevFiles) => [...prevFiles, ...processedFiles]);
|
||||||
setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]);
|
setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]);
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFile = (index: number) => {
|
const handleRemoveFile = (index: number) => {
|
||||||
@ -130,72 +123,51 @@ export default function ImagePreSignedForm({
|
|||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setIsUploaded(false);
|
|
||||||
setIsFailed(false);
|
setIsFailed(false);
|
||||||
|
setIsUploaded(false);
|
||||||
|
|
||||||
|
setUploadStatus(files.map(() => 'uploading'));
|
||||||
|
|
||||||
let finalFiles: { path: string; file: File }[] = [];
|
let finalFiles: { path: string; file: File }[] = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.file.type === 'application/zip' || file.file.type === 'application/x-zip-compressed') {
|
if (file.file.type === 'application/zip' || file.file.type === 'application/x-zip-compressed') {
|
||||||
console.log('업로드 전에 ZIP 파일 해제:', file.file.name);
|
|
||||||
const unzippedFiles = await unzipFilesWithPath(file.file);
|
const unzippedFiles = await unzipFilesWithPath(file.file);
|
||||||
console.log('해제된 파일:', unzippedFiles);
|
|
||||||
finalFiles = [...finalFiles, ...unzippedFiles];
|
finalFiles = [...finalFiles, ...unzippedFiles];
|
||||||
} else {
|
} else {
|
||||||
finalFiles.push(file);
|
finalFiles.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadType === 'file') {
|
|
||||||
uploadImageFile.mutate(
|
|
||||||
{
|
|
||||||
memberId,
|
|
||||||
projectId,
|
|
||||||
folderId,
|
|
||||||
files: finalFiles.map(({ file }) => file), // Extract only the file
|
|
||||||
progressCallback: (index: number) => {
|
|
||||||
setUploadStatus((prevStatus) => {
|
|
||||||
const newStatus = [...prevStatus];
|
|
||||||
newStatus[index] = true; // Mark as uploaded
|
|
||||||
return newStatus;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
handleRefetch();
|
|
||||||
setIsUploaded(true);
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
setIsFailed(true);
|
|
||||||
setUploadStatus((prevStatus) => prevStatus.map((status) => (status === null ? false : status)));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
await uploadFiles({
|
await uploadFiles({
|
||||||
files: finalFiles,
|
files: finalFiles,
|
||||||
projectId,
|
projectId,
|
||||||
folderId,
|
folderId,
|
||||||
memberId,
|
memberId,
|
||||||
onProgress: (progressValue: number) => {
|
onProgress: (progress) => {
|
||||||
setProgress(progressValue);
|
setUploadStatus((prevStatus) => {
|
||||||
|
const completedFiles = Math.round((progress / 100) * files.length);
|
||||||
|
const newStatus = prevStatus.map((status, index) => (index < completedFiles ? 'success' : status));
|
||||||
|
return newStatus;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
useSingleUpload: uploadType === 'file',
|
||||||
});
|
});
|
||||||
|
|
||||||
setUploadStatus(finalFiles.map(() => true));
|
setUploadStatus((prevStatus) => prevStatus.map(() => 'success'));
|
||||||
setIsUploaded(true);
|
setIsUploaded(true);
|
||||||
handleRefetch();
|
handleRefetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setUploadStatus((prevStatus) => prevStatus.map((status) => (status === 'uploading' ? 'failed' : status)));
|
||||||
setIsFailed(true);
|
setIsFailed(true);
|
||||||
setUploadStatus(finalFiles.map(() => false));
|
|
||||||
console.error('업로드 실패:', error);
|
console.error('업로드 실패:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const totalProgress = Math.round((uploadStatus.filter((status) => status !== null).length / files.length) * 100);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFileCount(files.length);
|
onFileCount(files.length);
|
||||||
}, [files, onFileCount]);
|
}, [files, onFileCount]);
|
||||||
@ -237,7 +209,6 @@ export default function ImagePreSignedForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
|
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
height={260}
|
height={260}
|
||||||
itemCount={files.length}
|
itemCount={files.length}
|
||||||
@ -245,38 +216,37 @@ export default function ImagePreSignedForm({
|
|||||||
width="100%"
|
width="100%"
|
||||||
>
|
>
|
||||||
{({ index, style }) => (
|
{({ index, style }) => (
|
||||||
<li
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center justify-between p-1"
|
className="flex items-center justify-between border-b border-gray-200 p-2"
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<span className="truncate">{files[index].path}</span>
|
<span className="truncate">{files[index].path}</span>
|
||||||
{isUploading ? (
|
<div className="flex items-center">
|
||||||
<div className="p-2">
|
{uploadStatus[index] === 'success' ? (
|
||||||
{uploadStatus[index] === true ? (
|
|
||||||
<CircleCheckBig
|
<CircleCheckBig
|
||||||
className="stroke-green-500"
|
className="stroke-green-500"
|
||||||
size={16}
|
size={16}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
) : uploadStatus[index] === false ? (
|
) : uploadStatus[index] === 'failed' ? (
|
||||||
<CircleX
|
<CircleX
|
||||||
className="stroke-red-500"
|
className="stroke-red-500"
|
||||||
size={16}
|
size={16}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : uploadStatus[index] === 'uploading' ? (
|
||||||
<CircleDashed
|
<CircleDashed
|
||||||
className="stroke-gray-500"
|
className="animate-spin stroke-gray-500"
|
||||||
size={16}
|
size={16}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
{!isUploading && (
|
||||||
) : (
|
|
||||||
<button
|
<button
|
||||||
className="cursor-pointer p-2"
|
className="ml-2 cursor-pointer p-1"
|
||||||
onClick={() => handleRemoveFile(index)}
|
onClick={() => handleRemoveFile(index)}
|
||||||
|
disabled={uploadStatus[index] === 'success'}
|
||||||
>
|
>
|
||||||
<X
|
<X
|
||||||
color="red"
|
color="red"
|
||||||
@ -285,18 +255,23 @@ export default function ImagePreSignedForm({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</li>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</FixedSizeList>
|
</FixedSizeList>
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
variant={isFailed ? 'red' : 'blue'}
|
variant={isFailed ? 'red' : 'blue'}
|
||||||
disabled={!isUploaded && !isFailed}
|
|
||||||
>
|
>
|
||||||
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
|
{isFailed
|
||||||
|
? '업로드 실패 (닫기)'
|
||||||
|
: isUploaded
|
||||||
|
? '업로드 완료 (닫기)'
|
||||||
|
: totalProgress === 0
|
||||||
|
? '업로드 준비 중...'
|
||||||
|
: `업로드 중... ${totalProgress}%`}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
@ -11,15 +11,18 @@ export default function useUploadFiles() {
|
|||||||
folderId,
|
folderId,
|
||||||
memberId,
|
memberId,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
useSingleUpload = false,
|
||||||
}: {
|
}: {
|
||||||
files: { path: string; file: File }[];
|
files: { path: string; file: File }[];
|
||||||
projectId: number;
|
projectId: number;
|
||||||
folderId: number;
|
folderId: number;
|
||||||
memberId: number;
|
memberId: number;
|
||||||
onProgress: (progress: number) => void;
|
onProgress: (progress: number) => void;
|
||||||
|
useSingleUpload?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const folderIdMap: { [path: string]: number } = { '': folderId };
|
const folderIdMap: { [path: string]: number } = { '': folderId };
|
||||||
|
|
||||||
|
if (!useSingleUpload) {
|
||||||
const foldersToCreate = Array.from(new Set(files.map(({ path }) => path.split('/').slice(0, -1).join('/'))));
|
const foldersToCreate = Array.from(new Set(files.map(({ path }) => path.split('/').slice(0, -1).join('/'))));
|
||||||
foldersToCreate.sort();
|
foldersToCreate.sort();
|
||||||
|
|
||||||
@ -42,10 +45,23 @@ export default function useUploadFiles() {
|
|||||||
folderIdMap[folderPath] = newFolder.id;
|
folderIdMap[folderPath] = newFolder.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let progress = 0;
|
let completedFiles = 0;
|
||||||
const totalFiles = files.length;
|
const totalFiles = files.length;
|
||||||
|
|
||||||
|
if (useSingleUpload) {
|
||||||
|
await uploadImageMutation.mutateAsync({
|
||||||
|
memberId,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
files: files.map(({ file }) => file),
|
||||||
|
progressCallback: (progressValue: number) => {
|
||||||
|
const progress = (progressValue / totalFiles) * 100;
|
||||||
|
onProgress(Math.round(progress));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
for (const { path, file } of files) {
|
for (const { path, file } of files) {
|
||||||
const folderPath = path.split('/').slice(0, -1).join('/');
|
const folderPath = path.split('/').slice(0, -1).join('/');
|
||||||
const targetFolderId = folderIdMap[folderPath] || folderId;
|
const targetFolderId = folderIdMap[folderPath] || folderId;
|
||||||
@ -55,11 +71,14 @@ export default function useUploadFiles() {
|
|||||||
projectId,
|
projectId,
|
||||||
folderId: targetFolderId,
|
folderId: targetFolderId,
|
||||||
files: [file],
|
files: [file],
|
||||||
progressCallback: (value) => {
|
progressCallback: (progressValue: number) => {
|
||||||
progress += value / totalFiles;
|
const progress = ((completedFiles + progressValue / 100) / totalFiles) * 100;
|
||||||
onProgress(Math.round(progress));
|
onProgress(Math.round(progress));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
completedFiles += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import GoogleLogo from '@/assets/icons/web_neutral_rd_ctn@1x.png';
|
import GoogleLogo from '@/assets/icons/web_light_rd_ctn@1x.png';
|
||||||
import useAuthStore from '@/stores/useAuthStore';
|
import useAuthStore from '@/stores/useAuthStore';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
@ -9,48 +9,45 @@ export default function Home() {
|
|||||||
const { accessToken } = useAuthStore();
|
const { accessToken } = useAuthStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center bg-gray-50 p-8">
|
<div className="flex h-full w-full flex-col items-center justify-center bg-gray-50">
|
||||||
<div className="mb-6 max-w-xl rounded-lg bg-white p-6 shadow-lg">
|
<div className="text-center">
|
||||||
<h2 className="mb-4 text-2xl font-bold text-gray-900">서비스 설명</h2>
|
<p className="text-5xl font-semibold leading-[60px] text-black">
|
||||||
<p className="mb-4 text-base text-gray-700">
|
<span className="text-primary">웹 기반 오토 레이블링</span> 플랫폼
|
||||||
본 서비스는 인공 지능(AI) 모델의 학습을 지원하기 위해 웹 기반의 자동 라벨링 도구를 개발하는 것을 목표로
|
<br />
|
||||||
합니다.
|
<span className="text-primary">WorLabel</span>에 오신 것을 환영합니다
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-4 text-base text-gray-700">
|
</div>
|
||||||
기존의 수동적인 방법으로는 대량의 학습 데이터를 처리하는데 시간과 비용이 많이 소모되었습니다. 그러나 본
|
<div className="mt-4 text-center">
|
||||||
서비스의 결과물인 Auto Labeler를 사용하면, 이러한 문제를 해결할 수 있을 것으로 기대됩니다.
|
<p className="text-xl font-light leading-[28px] text-black">
|
||||||
</p>
|
WorLabel로 레이블링 작업을 간소화하세요.
|
||||||
<p className="mb-4 text-base text-gray-700">
|
<br />
|
||||||
Auto Labeler는 웹 기반으로 동작하므로, 별도의 설치 과정 없이 인터넷 연결 환경에서 쉽게 사용할 수 있습니다.
|
브라우저에서 직접 레이블링을 자동화하여 빠르고 효율적인 워크플로우를 경험하세요.
|
||||||
또한, 사용자 친화적인 인터페이스를 제공하여 비전문가도 손쉽게 이용할 수 있도록 설계될 예정입니다.
|
|
||||||
</p>
|
|
||||||
<p className="text-base text-gray-700">
|
|
||||||
본 서비스는 특히 학습 데이터 구축 과정의 효율성과 정확도를 향상시키는 데 중점을 두고 있습니다.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
{!accessToken ? (
|
{!accessToken ? (
|
||||||
<a
|
<a
|
||||||
href={`${BASE_URL}/login/oauth2/authorization/google`}
|
href={`${BASE_URL}/login/oauth2/authorization/google`}
|
||||||
className="mb-4 transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-gray-300 active:opacity-80"
|
className="flex items-center justify-center transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-gray-300 active:opacity-80"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={GoogleLogo}
|
src={GoogleLogo}
|
||||||
alt="Sign in with Google"
|
alt="Sign in with Google"
|
||||||
className="h-auto w-full"
|
className="h-auto w-full"
|
||||||
/>
|
/>
|
||||||
</a> // 404 에러 방지
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<>
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="blue"
|
variant="blue"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
className="mt-8"
|
||||||
>
|
>
|
||||||
<Link to="/browse">시작하기</Link>
|
<Link to="/browse">시작하기</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user