Merge branch 'fe/refactor/image-upload' into 'fe/develop'

Refactor: 이미지 업로드 컴포넌트 리팩토링 - S11P21S002-243

See merge request s11-s-project/S11P21S002!239
This commit is contained in:
정현조 2024-09-30 12:47:49 +09:00
commit c0e25cc2f1
12 changed files with 169 additions and 43 deletions

View File

@ -31,7 +31,13 @@ export async function changeImageStatus(
.then(({ data }) => data); .then(({ data }) => data);
} }
export async function uploadImageFile(memberId: number, projectId: number, folderId: number, files: File[]) { export async function uploadImageFile(
memberId: number,
projectId: number,
folderId: number,
files: File[],
processCallback: (progress: number) => void
) {
const formData = new FormData(); const formData = new FormData();
files.forEach((file) => { files.forEach((file) => {
formData.append('imageList', file); formData.append('imageList', file);
@ -40,30 +46,60 @@ export async function uploadImageFile(memberId: number, projectId: number, folde
return api return api
.post(`/projects/${projectId}/folders/${folderId}/images/file`, formData, { .post(`/projects/${projectId}/folders/${folderId}/images/file`, formData, {
params: { memberId }, params: { memberId },
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
processCallback(progress);
}
},
}) })
.then(({ data }) => data); .then(({ data }) => data);
} }
export async function uploadImageFolder(memberId: number, projectId: number, files: File[]) { export async function uploadImageFolder(
memberId: number,
projectId: number,
folderId: number,
files: File[],
processCallback: (progress: number) => void
) {
const formData = new FormData(); const formData = new FormData();
files.forEach((file) => { files.forEach((file) => {
formData.append('imageList', file); formData.append('imageList', file);
}); });
return api return api
.post(`/projects/${projectId}/folders/${0}/images/file`, formData, { .post(`/projects/${projectId}/folders/${folderId}/images/file`, formData, {
params: { memberId }, params: { memberId },
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
processCallback(progress);
}
},
}) })
.then(({ data }) => data); .then(({ data }) => data);
} }
export async function uploadImageZip(memberId: number, projectId: number, file: File) { export async function uploadImageZip(
memberId: number,
projectId: number,
folderId: number,
file: File,
processCallback: (progress: number) => void
) {
const formData = new FormData(); const formData = new FormData();
formData.append('folderZip', file); formData.append('folderZip', file);
return api return api
.post(`/projects/${projectId}/folders/${0}/images/zip`, formData, { .post(`/projects/${projectId}/folders/${folderId}/images/zip`, formData, {
params: { memberId }, params: { memberId },
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
processCallback(progress);
}
},
}) })
.then(({ data }) => data); .then(({ data }) => data);
} }

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
@ -7,10 +7,14 @@ import useUploadImageFileQuery from '@/queries/projects/useUploadImageFileQuery'
export default function ImageUploadFileForm({ export default function ImageUploadFileForm({
onClose, onClose,
onRefetch,
onFileCount,
projectId, projectId,
folderId, folderId,
}: { }: {
onClose: () => void; onClose: () => void;
onRefetch: () => void;
onFileCount: (fileCount: number) => void;
projectId: number; projectId: number;
folderId: number; folderId: number;
}) { }) {
@ -22,6 +26,7 @@ export default function ImageUploadFileForm({
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 uploadImageFile = useUploadImageFileQuery(); const uploadImageFile = useUploadImageFileQuery();
@ -71,9 +76,13 @@ export default function ImageUploadFileForm({
projectId, projectId,
folderId, folderId,
files, files,
progressCallback: (progress: number) => {
setProgress(progress);
},
}, },
{ {
onSuccess: () => { onSuccess: () => {
onRefetch();
setIsUploaded(true); setIsUploaded(true);
}, },
onError: () => { onError: () => {
@ -83,6 +92,10 @@ export default function ImageUploadFileForm({
); );
}; };
useEffect(() => {
onFileCount(files.length);
}, [files, onFileCount]);
return ( return (
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
{!isUploading && ( {!isUploading && (
@ -115,11 +128,11 @@ export default function ImageUploadFileForm({
</div> </div>
)} )}
{files.length > 0 && ( {files.length > 0 && (
<ul className="m-0 max-h-[200px] list-none overflow-y-auto p-0"> <ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
{files.map((file, index) => ( {files.map((file, index) => (
<li <li
key={index} key={index}
className={cn('flex items-center justify-between p-1')} className="flex items-center justify-between p-1"
> >
<span className="truncate">{file.webkitRelativePath || file.name}</span> <span className="truncate">{file.webkitRelativePath || file.name}</span>
{isUploading ? ( {isUploading ? (
@ -127,19 +140,19 @@ export default function ImageUploadFileForm({
{isUploaded ? ( {isUploaded ? (
<CircleCheckBig <CircleCheckBig
className="stroke-green-500" className="stroke-green-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
) : isFailed ? ( ) : isFailed ? (
<CircleX <CircleX
className="stroke-red-500" className="stroke-red-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
) : ( ) : (
<CircleDashed <CircleDashed
className="stroke-gray-500" className="stroke-gray-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
)} )}
@ -171,7 +184,7 @@ export default function ImageUploadFileForm({
} }
disabled={!isUploaded && !isFailed} disabled={!isUploaded && !isFailed}
> >
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : '업로드 중...'} {isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
</Button> </Button>
) : ( ) : (
<Button <Button

View File

@ -4,10 +4,14 @@ import { Plus } from 'lucide-react';
import ImageUploadFileForm from './ImageUploadFileForm'; import ImageUploadFileForm from './ImageUploadFileForm';
export default function ImageUploadFileModal({ projectId, folderId }: { projectId: number; folderId: number }) { export default function ImageUploadFileModal({ projectId, folderId }: { projectId: number; folderId: number }) {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [fileCount, setFileCount] = React.useState<number>(0);
const handleOpen = () => setIsOpen(true); const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false); const handleClose = () => setIsOpen(false);
const handleFileCount = (fileCount: number) => {
setFileCount(fileCount);
};
return ( return (
<Dialog <Dialog
@ -23,9 +27,10 @@ export default function ImageUploadFileModal({ projectId, folderId }: { projectI
</button> </button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader title="파일 업로드" /> <DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
<ImageUploadFileForm <ImageUploadFileForm
onClose={handleClose} onClose={handleClose}
onFileCount={handleFileCount}
projectId={projectId} projectId={projectId}
folderId={folderId} folderId={folderId}
/> />

View File

@ -5,7 +5,17 @@ import useAuthStore from '@/stores/useAuthStore';
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react'; import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
import useUploadImageFolderQuery from '@/queries/projects/useUploadImageFolderQuery'; import useUploadImageFolderQuery from '@/queries/projects/useUploadImageFolderQuery';
export default function ImageUploadFolderForm({ onClose, projectId }: { onClose: () => void; projectId: number }) { export default function ImageUploadFolderForm({
onClose,
onRefetch,
projectId,
folderId,
}: {
onClose: () => void;
onRefetch: () => void;
projectId: number;
folderId: number;
}) {
const profile = useAuthStore((state) => state.profile); const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0; const memberId = profile?.id || 0;
@ -14,6 +24,7 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
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 uploadImageFolder = useUploadImageFolderQuery(); const uploadImageFolder = useUploadImageFolderQuery();
@ -56,10 +67,15 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
{ {
memberId, memberId,
projectId, projectId,
folderId,
files, files,
progressCallback: (progress: number) => {
setProgress(progress);
},
}, },
{ {
onSuccess: () => { onSuccess: () => {
onRefetch;
setIsUploaded(true); setIsUploaded(true);
}, },
onError: () => { onError: () => {
@ -100,11 +116,11 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
</div> </div>
)} )}
{files.length > 0 && ( {files.length > 0 && (
<ul className="m-0 max-h-[200px] list-none overflow-y-auto p-0"> <ul className="m-0 max-h-[260px] list-none overflow-y-auto p-0">
{files.map((file, index) => ( {files.map((file, index) => (
<li <li
key={index} key={index}
className={cn('flex items-center justify-between p-1')} className="flex items-center justify-between p-1"
> >
<span className="truncate">{file.webkitRelativePath || file.name}</span> <span className="truncate">{file.webkitRelativePath || file.name}</span>
{isUploading ? ( {isUploading ? (
@ -112,19 +128,19 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
{isUploaded ? ( {isUploaded ? (
<CircleCheckBig <CircleCheckBig
className="stroke-green-500" className="stroke-green-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
) : isFailed ? ( ) : isFailed ? (
<CircleX <CircleX
className="stroke-red-500" className="stroke-red-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
) : ( ) : (
<CircleDashed <CircleDashed
className="stroke-gray-500" className="stroke-gray-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
)} )}
@ -156,7 +172,7 @@ export default function ImageUploadFolderForm({ onClose, projectId }: { onClose:
} }
disabled={!isUploaded && !isFailed} disabled={!isUploaded && !isFailed}
> >
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : '업로드 중...'} {isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
</Button> </Button>
) : ( ) : (
<Button <Button

View File

@ -3,7 +3,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialog
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import ImageUploadFolderForm from './ImageUploadFolderForm'; import ImageUploadFolderForm from './ImageUploadFolderForm';
export default function ImageUploadFolderModal({ projectId }: { projectId: number }) { export default function ImageUploadFolderModal({ projectId, folderId }: { projectId: number; folderId: number }) {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true); const handleOpen = () => setIsOpen(true);
@ -27,6 +27,7 @@ export default function ImageUploadFolderModal({ projectId }: { projectId: numbe
<ImageUploadFolderForm <ImageUploadFolderForm
onClose={handleClose} onClose={handleClose}
projectId={projectId} projectId={projectId}
folderId={folderId}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -5,7 +5,17 @@ import useAuthStore from '@/stores/useAuthStore';
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react'; import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
import useUploadImageZipQuery from '@/queries/projects/useUploadImageZipQuery'; import useUploadImageZipQuery from '@/queries/projects/useUploadImageZipQuery';
export default function ImageUploadZipForm({ onClose, projectId }: { onClose: () => void; projectId: number }) { export default function ImageUploadZipForm({
onClose,
onRefetch,
projectId,
folderId,
}: {
onClose: () => void;
onRefetch: () => void;
projectId: number;
folderId: number;
}) {
const profile = useAuthStore((state) => state.profile); const profile = useAuthStore((state) => state.profile);
const memberId = profile?.id || 0; const memberId = profile?.id || 0;
@ -14,6 +24,7 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
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 uploadImageZip = useUploadImageZipQuery(); const uploadImageZip = useUploadImageZipQuery();
@ -57,10 +68,15 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
{ {
memberId, memberId,
projectId, projectId,
folderId,
file, file,
progressCallback: (progress: number) => {
setProgress(progress);
},
}, },
{ {
onSuccess: () => { onSuccess: () => {
onRefetch();
setIsUploaded(true); setIsUploaded(true);
}, },
onError: () => { onError: () => {
@ -110,19 +126,19 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
{isUploaded ? ( {isUploaded ? (
<CircleCheckBig <CircleCheckBig
className="stroke-green-500" className="stroke-green-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
) : isFailed ? ( ) : isFailed ? (
<CircleX <CircleX
className="stroke-red-500" className="stroke-red-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
) : ( ) : (
<CircleDashed <CircleDashed
className="stroke-gray-500" className="stroke-gray-500"
size={20} size={16}
strokeWidth="2" strokeWidth="2"
/> />
)} )}
@ -152,7 +168,7 @@ export default function ImageUploadZipForm({ onClose, projectId }: { onClose: ()
} }
disabled={!isUploaded && !isFailed} disabled={!isUploaded && !isFailed}
> >
{isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : '업로드 중...'} {isFailed ? '업로드 실패 (닫기)' : isUploaded ? '업로드 완료 (닫기)' : `업로드 중... ${progress}%`}
</Button> </Button>
) : ( ) : (
<Button <Button

View File

@ -3,7 +3,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialog
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import ImageUploadZipForm from './ImageUploadZipForm'; import ImageUploadZipForm from './ImageUploadZipForm';
export default function ImageUploadZipModal({ projectId }: { projectId: number }) { export default function ImageUploadZipModal({ projectId, folderId }: { projectId: number; folderId: number }) {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const handleOpen = () => setIsOpen(true); const handleOpen = () => setIsOpen(true);
@ -27,6 +27,7 @@ export default function ImageUploadZipModal({ projectId }: { projectId: number }
<ImageUploadZipForm <ImageUploadZipForm
onClose={handleClose} onClose={handleClose}
projectId={projectId} projectId={projectId}
folderId={folderId}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -15,32 +15,40 @@ import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
export default function WorkspaceDropdownMenu({ export default function WorkspaceDropdownMenu({
projectId, projectId,
folderId, folderId,
refetch, onRefetch,
}: { }: {
projectId: number; projectId: number;
folderId: number; folderId: number;
refetch: () => void; onRefetch: () => void;
}) { }) {
const [isOpenUploadFile, setIsOpenUploadFile] = React.useState(false); const [isOpenUploadFile, setIsOpenUploadFile] = React.useState<boolean>(false);
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState(false); const [fileCount, setFileCount] = React.useState<number>(0);
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState(false); const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
const handleOpenUploadFile = () => setIsOpenUploadFile(true); const handleOpenUploadFile = () => setIsOpenUploadFile(true);
const handleCloseUploadFile = () => { const handleCloseUploadFile = () => {
refetch();
setIsOpenUploadFile(false); setIsOpenUploadFile(false);
}; };
const handleOpenUploadFolder = () => setIsOpenUploadFolder(true); const handleOpenUploadFolder = () => setIsOpenUploadFolder(true);
const handleCloseUploadFolder = () => { const handleCloseUploadFolder = () => {
refetch();
setIsOpenUploadFolder(false); setIsOpenUploadFolder(false);
}; };
const handleOpenUploadZip = () => setIsOpenUploadZip(true); const handleOpenUploadZip = () => setIsOpenUploadZip(true);
const handleCloseUploadZip = () => { const handleCloseUploadZip = () => {
refetch();
setIsOpenUploadZip(false); setIsOpenUploadZip(false);
}; };
const handleFileCount = (fileCount: number) => {
console.log(fileCount);
setFileCount(fileCount);
};
return ( return (
<> <>
<DropdownMenu> <DropdownMenu>
@ -71,9 +79,11 @@ export default function WorkspaceDropdownMenu({
> >
<DialogTrigger asChild></DialogTrigger> <DialogTrigger asChild></DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader title="파일 업로드" /> <DialogHeader title={fileCount > 0 ? `파일 업로드 (${fileCount})` : '파일 업로드'} />
<ImageUploadFileForm <ImageUploadFileForm
onClose={handleCloseUploadFile} onClose={handleCloseUploadFile}
onRefetch={onRefetch}
onFileCount={handleFileCount}
projectId={projectId} projectId={projectId}
folderId={folderId} folderId={folderId}
/> />
@ -89,7 +99,9 @@ export default function WorkspaceDropdownMenu({
<DialogHeader title="폴더 업로드 (임시)" /> <DialogHeader title="폴더 업로드 (임시)" />
<ImageUploadFolderForm <ImageUploadFolderForm
onClose={handleCloseUploadFolder} onClose={handleCloseUploadFolder}
onRefetch={onRefetch}
projectId={projectId} projectId={projectId}
folderId={folderId}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@ -103,7 +115,9 @@ export default function WorkspaceDropdownMenu({
<DialogHeader title="폴더 압축파일 업로드" /> <DialogHeader title="폴더 압축파일 업로드" />
<ImageUploadZipForm <ImageUploadZipForm
onClose={handleCloseUploadZip} onClose={handleCloseUploadZip}
onRefetch={onRefetch}
projectId={projectId} projectId={projectId}
folderId={folderId}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -40,7 +40,7 @@ export default function ProjectStructure({ project }: { project: Project }) {
<WorkspaceDropdownMenu <WorkspaceDropdownMenu
projectId={project.id} projectId={project.id}
folderId={0} folderId={0}
refetch={refetch} onRefetch={refetch}
/> />
</header> </header>
{folderData.children.length === 0 && folderData.images.length === 0 ? ( {folderData.children.length === 0 && folderData.images.length === 0 ? (

View File

@ -8,11 +8,13 @@ export default function useUploadImageFileQuery() {
projectId, projectId,
folderId, folderId,
files, files,
progressCallback,
}: { }: {
memberId: number; memberId: number;
projectId: number; projectId: number;
folderId: number; folderId: number;
files: File[]; files: File[];
}) => uploadImageFile(memberId, projectId, folderId, files), progressCallback: (progress: number) => void;
}) => uploadImageFile(memberId, projectId, folderId, files, progressCallback),
}); });
} }

View File

@ -3,7 +3,18 @@ import { useMutation } from '@tanstack/react-query';
export default function useUploadImageFolderQuery() { export default function useUploadImageFolderQuery() {
return useMutation({ return useMutation({
mutationFn: ({ memberId, projectId, files }: { memberId: number; projectId: number; files: File[] }) => mutationFn: ({
uploadImageFolder(memberId, projectId, files), memberId,
projectId,
folderId,
files,
progressCallback,
}: {
memberId: number;
projectId: number;
folderId: number;
files: File[];
progressCallback: (progress: number) => void;
}) => uploadImageFolder(memberId, projectId, folderId, files, progressCallback),
}); });
} }

View File

@ -3,7 +3,18 @@ import { useMutation } from '@tanstack/react-query';
export default function useUploadImageZipQuery() { export default function useUploadImageZipQuery() {
return useMutation({ return useMutation({
mutationFn: ({ memberId, projectId, file }: { memberId: number; projectId: number; file: File }) => mutationFn: ({
uploadImageZip(memberId, projectId, file), memberId,
projectId,
folderId,
file,
progressCallback,
}: {
memberId: number;
projectId: number;
folderId: number;
file: File;
progressCallback: (progress: number) => void;
}) => uploadImageZip(memberId, projectId, folderId, file, progressCallback),
}); });
} }