Merge branch 'fe/fix/upload' into 'fe/develop'

Refactor: 퍼센테이지 복구

See merge request s11-s-project/S11P21S002!301
This commit is contained in:
조현수 2024-10-08 09:09:13 +09:00
commit d87b77f369
5 changed files with 154 additions and 163 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -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]);
setUploadStatus((prevStatus) => [...prevStatus, ...processedFiles.map(() => null)]);
} }
} }
setFiles((prevFiles) => [...prevFiles, ...processedFiles]);
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') { try {
uploadImageFile.mutate( await uploadFiles({
{ files: finalFiles,
memberId, projectId,
projectId, folderId,
folderId, memberId,
files: finalFiles.map(({ file }) => file), // Extract only the file onProgress: (progress) => {
progressCallback: (index: number) => { setUploadStatus((prevStatus) => {
setUploadStatus((prevStatus) => { const completedFiles = Math.round((progress / 100) * files.length);
const newStatus = [...prevStatus]; const newStatus = prevStatus.map((status, index) => (index < completedFiles ? 'success' : status));
newStatus[index] = true; // Mark as uploaded return newStatus;
return newStatus; });
});
},
}, },
{ useSingleUpload: uploadType === 'file',
onSuccess: () => { });
handleRefetch();
setIsUploaded(true);
},
onError: () => {
setIsFailed(true);
setUploadStatus((prevStatus) => prevStatus.map((status) => (status === null ? false : status)));
},
}
);
} else {
try {
await uploadFiles({
files: finalFiles,
projectId,
folderId,
memberId,
onProgress: (progressValue: number) => {
setProgress(progressValue);
},
});
setUploadStatus(finalFiles.map(() => true)); setUploadStatus((prevStatus) => prevStatus.map(() => 'success'));
setIsUploaded(true); setIsUploaded(true);
handleRefetch(); handleRefetch();
} catch (error) { } catch (error) {
setIsFailed(true); setUploadStatus((prevStatus) => prevStatus.map((status) => (status === 'uploading' ? 'failed' : status)));
setUploadStatus(finalFiles.map(() => false)); setIsFailed(true);
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,46 +209,44 @@ 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} itemSize={40}
itemSize={40} width="100%"
width="100%" >
> {({ index, style }) => (
{({ index, style }) => ( <div
<li key={index}
key={index} className="flex items-center justify-between border-b border-gray-200 p-2"
className="flex items-center justify-between p-1" style={style}
style={style} >
> <span className="truncate">{files[index].path}</span>
<span className="truncate">{files[index].path}</span> <div className="flex items-center">
{isUploading ? ( {uploadStatus[index] === 'success' ? (
<div className="p-2"> <CircleCheckBig
{uploadStatus[index] === true ? ( className="stroke-green-500"
<CircleCheckBig size={16}
className="stroke-green-500" strokeWidth="2"
size={16} />
strokeWidth="2" ) : uploadStatus[index] === 'failed' ? (
/> <CircleX
) : uploadStatus[index] === false ? ( className="stroke-red-500"
<CircleX size={16}
className="stroke-red-500" strokeWidth="2"
size={16} />
strokeWidth="2" ) : uploadStatus[index] === 'uploading' ? (
/> <CircleDashed
) : ( className="animate-spin stroke-gray-500"
<CircleDashed size={16}
className="stroke-gray-500" strokeWidth="2"
size={16} />
strokeWidth="2" ) : null}
/> {!isUploading && (
)}
</div>
) : (
<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> )}
</ul> </FixedSizeList>
)} )}
{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

View File

@ -11,55 +11,74 @@ 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 };
const foldersToCreate = Array.from(new Set(files.map(({ path }) => path.split('/').slice(0, -1).join('/')))); if (!useSingleUpload) {
foldersToCreate.sort(); const foldersToCreate = Array.from(new Set(files.map(({ path }) => path.split('/').slice(0, -1).join('/'))));
foldersToCreate.sort();
for (const folderPath of foldersToCreate) { for (const folderPath of foldersToCreate) {
if (folderPath) { if (folderPath) {
const pathSegments = folderPath.split('/'); const pathSegments = folderPath.split('/');
const parentPath = pathSegments.slice(0, -1).join('/'); const parentPath = pathSegments.slice(0, -1).join('/');
const folderName = pathSegments[pathSegments.length - 1]; const folderName = pathSegments[pathSegments.length - 1];
const parentId = folderIdMap[parentPath] || folderId; const parentId = folderIdMap[parentPath] || folderId;
const newFolder = await createFolderMutation.mutateAsync({ const newFolder = await createFolderMutation.mutateAsync({
projectId, projectId,
folderData: { folderData: {
title: folderName, title: folderName,
parentId: parentId, parentId: parentId,
}, },
}); });
folderIdMap[folderPath] = newFolder.id; folderIdMap[folderPath] = newFolder.id;
}
} }
} }
let progress = 0; let completedFiles = 0;
const totalFiles = files.length; const totalFiles = files.length;
for (const { path, file } of files) { if (useSingleUpload) {
const folderPath = path.split('/').slice(0, -1).join('/');
const targetFolderId = folderIdMap[folderPath] || folderId;
await uploadImageMutation.mutateAsync({ await uploadImageMutation.mutateAsync({
memberId, memberId,
projectId, projectId,
folderId: targetFolderId, folderId,
files: [file], files: files.map(({ file }) => file),
progressCallback: (value) => { progressCallback: (progressValue: number) => {
progress += value / totalFiles; const progress = (progressValue / totalFiles) * 100;
onProgress(Math.round(progress)); onProgress(Math.round(progress));
}, },
}); });
} else {
for (const { path, file } of files) {
const folderPath = path.split('/').slice(0, -1).join('/');
const targetFolderId = folderIdMap[folderPath] || folderId;
await uploadImageMutation.mutateAsync({
memberId,
projectId,
folderId: targetFolderId,
files: [file],
progressCallback: (progressValue: number) => {
const progress = ((completedFiles + progressValue / 100) / totalFiles) * 100;
onProgress(Math.round(progress));
},
});
completedFiles += 1;
}
} }
}; };

View File

@ -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>
{!accessToken ? ( <div className="mt-8">
<a {!accessToken ? (
href={`${BASE_URL}/login/oauth2/authorization/google`} <a
className="mb-4 transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-gray-300 active:opacity-80" href={`${BASE_URL}/login/oauth2/authorization/google`}
> className="flex items-center justify-center transition hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-gray-300 active:opacity-80"
<img >
src={GoogleLogo} <img
alt="Sign in with Google" src={GoogleLogo}
className="h-auto w-full" alt="Sign in with Google"
/> 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>
); );
} }