Feat: 이미지 업로드 퍼센트 및 진행 상황 보여주기
This commit is contained in:
parent
048d73868d
commit
ee5489554c
@ -1,5 +1,6 @@
|
|||||||
import api from '@/api/axiosConfig';
|
import api from '@/api/axiosConfig';
|
||||||
import { ImageMoveRequest, ImageStatusChangeRequest } from '@/types';
|
import { ImageMoveRequest, ImageStatusChangeRequest, ImagePresignedUrlResponse } from '@/types';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export async function getImage(imageId: number, memberId: number) {
|
export async function getImage(imageId: number, memberId: number) {
|
||||||
return api.get(`/images/${imageId}`, {
|
return api.get(`/images/${imageId}`, {
|
||||||
@ -22,7 +23,7 @@ export async function deleteImage(imageId: number, memberId: number) {
|
|||||||
export async function changeImageStatus(
|
export async function changeImageStatus(
|
||||||
imageId: number,
|
imageId: number,
|
||||||
memberId: number,
|
memberId: number,
|
||||||
statusChangeRequest: ImageStatusChangeRequest
|
statusChangeRequest: ImageStatusChangeRequest,
|
||||||
) {
|
) {
|
||||||
return api
|
return api
|
||||||
.put(`/images/${imageId}/status`, statusChangeRequest, {
|
.put(`/images/${imageId}/status`, statusChangeRequest, {
|
||||||
@ -36,7 +37,7 @@ export async function uploadImageFile(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
files: File[],
|
files: File[],
|
||||||
processCallback: (progress: number) => void
|
processCallback: (progress: number) => void,
|
||||||
) {
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@ -61,7 +62,7 @@ export async function uploadImageFolderFile(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
files: File[],
|
files: File[],
|
||||||
processCallback: (progress: number) => void
|
processCallback: (progress: number) => void,
|
||||||
) {
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@ -86,7 +87,7 @@ export async function uploadImageFolder(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
files: File[],
|
files: File[],
|
||||||
processCallback: (progress: number) => void
|
processCallback: (progress: number) => void,
|
||||||
) {
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@ -111,7 +112,7 @@ export async function uploadImageZip(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
file: File,
|
file: File,
|
||||||
processCallback: (progress: number) => void
|
processCallback: (progress: number) => void,
|
||||||
) {
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('folderZip', file);
|
formData.append('folderZip', file);
|
||||||
@ -128,3 +129,63 @@ export async function uploadImageZip(
|
|||||||
})
|
})
|
||||||
.then(({ data }) => data);
|
.then(({ data }) => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadImagePresigned(
|
||||||
|
memberId: number,
|
||||||
|
projectId: number,
|
||||||
|
folderId: number,
|
||||||
|
files: File[],
|
||||||
|
processCallback: (index: number) => void,
|
||||||
|
) {
|
||||||
|
// 업로드 시작 시간 기록
|
||||||
|
const startTime = new Date().getTime();
|
||||||
|
|
||||||
|
// 파일 메타데이터 생성
|
||||||
|
const imageMetaList = files.map((file: File, index: number) => ({
|
||||||
|
id: index,
|
||||||
|
fileName: file.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 서버로부터 presigned URL 리스트 받아옴
|
||||||
|
const { data: presignedUrlList }: { data: ImagePresignedUrlResponse[] } = await api.post(
|
||||||
|
`/projects/${projectId}/folders/${folderId}/images/presigned`,
|
||||||
|
imageMetaList,
|
||||||
|
{
|
||||||
|
params: { memberId },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// 각 파일을 presigned URL에 맞춰서 업로드 (axios 직접 사용)
|
||||||
|
for (const presignedUrlInfo of presignedUrlList) {
|
||||||
|
const file = files[presignedUrlInfo.id];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// S3 presigned URL로 개별 파일 업로드
|
||||||
|
await axios.put(presignedUrlInfo.presignedUrl, file, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': file.type, // 파일의 타입 설정
|
||||||
|
},
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.total) {
|
||||||
|
processCallback(presignedUrlInfo.id); // 성공 시 진행 상황 업데이트
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 파일이 성공적으로 업로드되면 로그 출력
|
||||||
|
} catch (error) {
|
||||||
|
// 업로드 실패 시 로그 출력
|
||||||
|
console.error(`업로드 실패: ${file.name}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 업로드 완료 시간 기록
|
||||||
|
const endTime = new Date().getTime();
|
||||||
|
|
||||||
|
// 소요 시간 계산 (초 단위로 변환)
|
||||||
|
const durationInSeconds = (endTime - startTime) / 1000;
|
||||||
|
|
||||||
|
// 소요 시간 콘솔 출력
|
||||||
|
console.log(`모든 파일 업로드 완료. 총 소요 시간: ${durationInSeconds}초`);
|
||||||
|
}
|
||||||
|
@ -0,0 +1,196 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import useAuthStore from '@/stores/useAuthStore';
|
||||||
|
import { CircleCheckBig, CircleDashed, CircleX, X } from 'lucide-react';
|
||||||
|
import useUploadImagePresignedQuery from '@/queries/projects/useUploadImagePresignedQuery.ts';
|
||||||
|
|
||||||
|
export default function ImageUploadPresignedForm({
|
||||||
|
onClose,
|
||||||
|
onRefetch,
|
||||||
|
onFileCount,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onRefetch?: () => void;
|
||||||
|
onFileCount: (fileCount: number) => void;
|
||||||
|
projectId: number;
|
||||||
|
folderId: 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 [isUploaded, setIsUploaded] = useState<boolean>(false);
|
||||||
|
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||||
|
const [uploadStatus, setUploadStatus] = useState<(boolean | null)[]>([]); // 각 파일의 성공/실패 여부 관리
|
||||||
|
|
||||||
|
const uploadImageFile = useUploadImagePresignedQuery();
|
||||||
|
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefetch = () => {
|
||||||
|
if (onRefetch) {
|
||||||
|
onRefetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newFiles = event.target.files;
|
||||||
|
|
||||||
|
if (newFiles) {
|
||||||
|
const newImages = Array.from(newFiles).filter((file) => {
|
||||||
|
const fileExtension = file.name.split('.').pop()?.toLowerCase() ?? '';
|
||||||
|
return ['jpg', 'png', 'jpeg'].includes(fileExtension);
|
||||||
|
});
|
||||||
|
|
||||||
|
setFiles((prevFiles) => [...prevFiles, ...newImages]);
|
||||||
|
setUploadStatus((prevState) => [...prevState, ...newImages.map(()=> null)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
uploadImageFile.mutate(
|
||||||
|
{
|
||||||
|
memberId,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
files,
|
||||||
|
progressCallback: (index: number) => {
|
||||||
|
// 업로드 성공하면 상태 업데이트
|
||||||
|
setUploadStatus((prevStatus) => {
|
||||||
|
const newStatus = [...prevStatus];
|
||||||
|
newStatus[index] = true; // 업로드 성공 시 true
|
||||||
|
return newStatus;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleRefetch();
|
||||||
|
setIsUploaded(true);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setIsFailed(true);
|
||||||
|
setUploadStatus((prevStatus) =>
|
||||||
|
prevStatus.map((status) => (status === null ? false : status))
|
||||||
|
); // 실패 시 처리
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 전체 진행 상황 계산
|
||||||
|
const totalProgress = Math.round(
|
||||||
|
(uploadStatus.filter((status) => status !== null).length / files.length) * 100
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFileCount(files.length);
|
||||||
|
}, [files, onFileCount]);
|
||||||
|
|
||||||
|
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=".jpg,.jpeg,.png"
|
||||||
|
// 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 max-h-[260px] list-none overflow-y-auto p-0">
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<li key={index} className="flex items-center justify-between p-1">
|
||||||
|
<span className="truncate">{file.name}</span>
|
||||||
|
{isUploading ? (
|
||||||
|
<div className="p-2">
|
||||||
|
{uploadStatus[index] === true ? (
|
||||||
|
<CircleCheckBig className="stroke-green-500" size={16} strokeWidth="2" />
|
||||||
|
) : uploadStatus[index] === false ? (
|
||||||
|
<CircleX className="stroke-red-500" size={16} strokeWidth="2" />
|
||||||
|
) : (
|
||||||
|
<CircleDashed className="stroke-gray-500" size={16} strokeWidth="2" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className={'cursor-pointer p-2'} onClick={() => handleRemoveFile(index)}>
|
||||||
|
<X color="red" size={16} strokeWidth="2" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{isUploading ? (
|
||||||
|
<Button onClick={handleClose} variant={isFailed ? 'red' : 'blue'}>
|
||||||
|
{isFailed
|
||||||
|
? '업로드 실패 (닫기)'
|
||||||
|
: isUploaded
|
||||||
|
? '업로드 완료 (닫기)'
|
||||||
|
: totalProgress === 0
|
||||||
|
? '업로드 준비 중...'
|
||||||
|
: `업로드 중... ${totalProgress}%`}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleUpload}
|
||||||
|
variant="blue"
|
||||||
|
disabled={files.length === 0}
|
||||||
|
>
|
||||||
|
업로드
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
40
frontend/src/components/ImageUploadPresignedModal/index.tsx
Normal file
40
frontend/src/components/ImageUploadPresignedModal/index.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import ImageUploadPresingedForm from '@/components/ImageUploadPresignedModal/ImageUploadPresignedForm.tsx';
|
||||||
|
|
||||||
|
export default function ImageUploadPresignedModal({ projectId, folderId }: { projectId: number; folderId: number }) {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||||
|
|
||||||
|
const handleOpen = () => setIsOpen(true);
|
||||||
|
const handleClose = () => setIsOpen(false);
|
||||||
|
const handleFileCount = (fileCount: number) => {
|
||||||
|
setFileCount(fileCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
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={fileCount > 0 ? `파일 업로드 (${fileCount}) PreSigned` : '파일 업로드 PreSigned'} />
|
||||||
|
<ImageUploadPresingedForm
|
||||||
|
onClose={handleClose}
|
||||||
|
onFileCount={handleFileCount}
|
||||||
|
projectId={projectId}
|
||||||
|
folderId={folderId}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@ -7,11 +7,12 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '../ui/dropdown-menu';
|
} from '../ui/dropdown-menu';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from '../ui/dialogCustom';
|
||||||
import ImageUploadFileForm from '../ImageUploadFileModal/ImageUploadFileForm';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ImageUploadFileForm from '../ImageUploadFileModal/ImageUploadFileForm';
|
||||||
import ImageUploadFolderFileForm from '../ImageUploadFolderFileModal/ImageUploadFolderFileForm';
|
import ImageUploadFolderFileForm from '../ImageUploadFolderFileModal/ImageUploadFolderFileForm';
|
||||||
import ImageUploadFolderForm from '../ImageUploadFolderModal/ImageUploadFolderForm';
|
import ImageUploadFolderForm from '../ImageUploadFolderModal/ImageUploadFolderForm';
|
||||||
import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
|
import ImageUploadZipForm from '../ImageUploadZipModal/ImageUploadZipForm';
|
||||||
|
import ImageUploadPresignedForm from '../ImageUploadPresignedModal/ImageUploadPresignedForm.tsx'
|
||||||
|
|
||||||
export default function WorkspaceDropdownMenu({
|
export default function WorkspaceDropdownMenu({
|
||||||
projectId,
|
projectId,
|
||||||
@ -26,6 +27,7 @@ export default function WorkspaceDropdownMenu({
|
|||||||
const [fileCount, setFileCount] = React.useState<number>(0);
|
const [fileCount, setFileCount] = React.useState<number>(0);
|
||||||
const [isOpenUploadFolderFile, setIsOpenUploadFolderFile] = React.useState<boolean>(false);
|
const [isOpenUploadFolderFile, setIsOpenUploadFolderFile] = React.useState<boolean>(false);
|
||||||
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
const [isOpenUploadFolder, setIsOpenUploadFolder] = React.useState<boolean>(false);
|
||||||
|
const [isOpenUploadPresigned, setIsOpenUploadPresigned] = React.useState<boolean>(false);
|
||||||
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
const [isOpenUploadZip, setIsOpenUploadZip] = React.useState<boolean>(false);
|
||||||
|
|
||||||
const handleOpenUploadFile = () => setIsOpenUploadFile(true);
|
const handleOpenUploadFile = () => setIsOpenUploadFile(true);
|
||||||
@ -46,6 +48,12 @@ export default function WorkspaceDropdownMenu({
|
|||||||
setIsOpenUploadFolder(false);
|
setIsOpenUploadFolder(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenUploadPresigned = () => setIsOpenUploadPresigned(true);
|
||||||
|
|
||||||
|
const handleCloseUploadPresigned = () => {
|
||||||
|
setIsOpenUploadPresigned(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenUploadZip = () => setIsOpenUploadZip(true);
|
const handleOpenUploadZip = () => setIsOpenUploadZip(true);
|
||||||
|
|
||||||
const handleCloseUploadZip = () => {
|
const handleCloseUploadZip = () => {
|
||||||
@ -77,6 +85,7 @@ export default function WorkspaceDropdownMenu({
|
|||||||
<DropdownMenuItem onClick={handleOpenUploadFile}>파일 업로드</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadFile}>파일 업로드</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleOpenUploadFolderFile}>폴더 업로드 (파일 업로드 API 이용)</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadFolderFile}>폴더 업로드 (파일 업로드 API 이용)</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleOpenUploadFolder}>폴더 업로드 (백엔드 구현 필요)</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadFolder}>폴더 업로드 (백엔드 구현 필요)</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleOpenUploadPresigned}>폴더 업로드 (PresignedUrl 이용)</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={handleOpenUploadZip}>폴더 압축파일 업로드</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleOpenUploadZip}>폴더 압축파일 업로드</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@ -130,6 +139,23 @@ export default function WorkspaceDropdownMenu({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isOpenUploadPresigned}
|
||||||
|
onOpenChange={setIsOpenUploadPresigned}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild></DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader title='파일 업로드' />
|
||||||
|
<ImageUploadPresignedForm
|
||||||
|
onClose={handleCloseUploadPresigned}
|
||||||
|
onRefetch={onRefetch}
|
||||||
|
onFileCount={handleFileCount}
|
||||||
|
projectId={projectId}
|
||||||
|
folderId={folderId}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpenUploadZip}
|
open={isOpenUploadZip}
|
||||||
onOpenChange={setIsOpenUploadZip}
|
onOpenChange={setIsOpenUploadZip}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { uploadImagePresigned } from '@/api/imageApi';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export default function useUploadImagePresignedQuery() {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
memberId,
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
files,
|
||||||
|
progressCallback,
|
||||||
|
}: {
|
||||||
|
memberId: number;
|
||||||
|
projectId: number;
|
||||||
|
folderId: number;
|
||||||
|
files: File[];
|
||||||
|
progressCallback: (index: number) => void;
|
||||||
|
}) => uploadImagePresigned(memberId, projectId, folderId, files, progressCallback),
|
||||||
|
});
|
||||||
|
}
|
@ -31,3 +31,8 @@ export interface ImageFolderRequest {
|
|||||||
parentId: number;
|
parentId: number;
|
||||||
files: File[];
|
files: File[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImagePresignedUrlResponse{
|
||||||
|
id: number;
|
||||||
|
presignedUrl: string;
|
||||||
|
}
|
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "S11P21S002",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user