Files
frontend/apps/as/src/shared/lib/use-tus.ts
T

146 lines
4.2 KiB
TypeScript

"use client";
import { useCallback, useState } from "react";
import { type Upload } from "tus-js-client";
import { handleTusUpload, type TusErrorResponse } from "@/entities/tus";
import { useAlertStore } from "./use-alert-store";
interface UploadFileOptions {
fileKey?: string;
metadata?: Record<string, string>;
}
interface UseTusOptions {
onSuccess?: (atflCd: string, atflId: string, file: File, fileKey: string) => void;
onError?: (
error: Error,
file: File,
fileKey: string,
response: TusErrorResponse | null,
) => void;
accept?: string;
}
interface UseTusReturn {
uploadFile: (file: File, options?: UploadFileOptions) => void;
uploadFileAsync: (file: File, options?: UploadFileOptions) => Promise<string>;
progress: Record<string, number>;
isUploading: Record<string, boolean>;
uploads: Record<string, Upload>;
}
const ALLOWED_EXTENSIONS: readonly string[] = [
// 문서
".pdf",
".txt",
".hwp",
".hwpx",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
// 이미지
".jpg",
".jpeg",
".png",
".gif",
".bmp",
// 압축
".zip",
];
function getDefaultFileKey(file: File) {
return `${file.name}::${file.lastModified}::${file.size}`;
}
function useTus(options: UseTusOptions = {}): UseTusReturn {
const alert = useAlertStore((state) => state.setMessage);
const [progress, setProgress] = useState<Record<string, number>>({});
const [isUploading, setIsUploading] = useState<Record<string, boolean>>({});
const [uploads, setUploads] = useState<Record<string, Upload>>({});
const validateFileType = useCallback(
(file: File): boolean => {
const fileExtension = file.name.substring(file.name.lastIndexOf(".")).toLowerCase();
const acceptExtensions = options.accept
?.split(",")
.map((extension) => extension.trim().toLowerCase());
const isAllowed =
ALLOWED_EXTENSIONS.includes(fileExtension) &&
(!acceptExtensions || acceptExtensions.includes(fileExtension));
if (!isAllowed) {
alert(`허용되지 않은 파일 형식입니다. (${fileExtension})`);
return false;
}
return true;
},
[options.accept, alert],
);
const uploadFileAsync = useCallback(
(file: File, uploadOptions?: UploadFileOptions): Promise<string> => {
const fileKey = uploadOptions?.fileKey ?? getDefaultFileKey(file);
if (!validateFileType(file)) {
const error = new Error("Invalid file type");
options.onError?.(error, file, fileKey, null);
return Promise.reject(error);
}
setIsUploading((prev) => ({ ...prev, [fileKey]: true }));
setProgress((prev) => ({ ...prev, [fileKey]: 0 }));
return new Promise<string>((resolve, reject) => {
handleTusUpload(file, {
metadata: uploadOptions?.metadata,
handleUploadStart: (upload) => {
setUploads((prev) => ({ ...prev, [fileKey]: upload }));
},
handleProgress: (percentage) => {
setProgress((prev) => ({ ...prev, [fileKey]: percentage }));
},
handleSuccess: (_upload, atflCd, atflId) => {
setIsUploading((prev) => ({ ...prev, [fileKey]: false }));
setProgress((prev) => ({ ...prev, [fileKey]: 100 }));
options.onSuccess?.(atflCd, atflId, file, fileKey);
resolve(atflCd);
},
handleError: (error, response) => {
setIsUploading((prev) => ({ ...prev, [fileKey]: false }));
alert(response?.message ?? "파일 업로드 중 오류가 발생했습니다.");
options.onError?.(error, file, fileKey, response);
reject(error);
},
});
});
},
[validateFileType, options, alert],
);
const uploadFile = useCallback(
(file: File, uploadOptions?: UploadFileOptions) => {
void uploadFileAsync(file, uploadOptions);
},
[uploadFileAsync],
);
return {
uploadFile,
uploadFileAsync,
progress,
isUploading,
uploads,
};
}
export { getDefaultFileKey, useTus };
export type { UploadFileOptions, UseTusOptions, UseTusReturn };