242 lines
8.0 KiB
TypeScript
242 lines
8.0 KiB
TypeScript
/**
|
|
* 📄 File Upload Form Component
|
|
* 파일 업로드 폼 컴포넌트
|
|
*
|
|
* - 파일 선택 및 업로드 기능
|
|
* - 년도 및 분기 선택 옵션 제공
|
|
* - 파일 등록 및 콜백 처리
|
|
* - 업로드 진행률 및 성공/실패 핸들링
|
|
* - 파일 형식 검증 및 미리보기 지원
|
|
* - TUS 프로토콜을 통한 대용량 파일 업로드
|
|
*/
|
|
"use client";
|
|
import { FileUploader } from "@common/components";
|
|
import { Button } from "@common/components/ui";
|
|
import { handleUpload } from "@/components/lib/utils";
|
|
import { useId, useState } from "react";
|
|
import type { HttpResponse, Upload } from "tus-js-client";
|
|
import { useAlertStore } from "@/shared/lib";
|
|
|
|
interface FileUploadFormProps {
|
|
onSubmit?: (
|
|
files: string,
|
|
year: string,
|
|
quarter: string,
|
|
code: string,
|
|
id: string,
|
|
) => void;
|
|
onFileSelect?: (
|
|
file: File | null,
|
|
atflCd: string | null,
|
|
atflId: string | null,
|
|
) => void; // 파일 선택 시 호출될 콜백 추가
|
|
showButton?: boolean; // 버튼 표시 여부 (기본값 true)
|
|
showPreview?: boolean; // 파일명 프리뷰 표시 여부
|
|
accept?: string; // 파일 형식 지정
|
|
fileIdx?: number; // 부모가 어떤 FileUploadForm인지 구분하기 위한 인덱스
|
|
}
|
|
|
|
const FileUploadForm = ({
|
|
onSubmit,
|
|
onFileSelect,
|
|
showButton = true,
|
|
showPreview = false,
|
|
accept = ".jpeg, .jpg, .png",
|
|
fileIdx, // props에서 fileIdx 받기
|
|
}: FileUploadFormProps) => {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const [internalAtflCd, setInternalAtflCd] = useState<string | null>(null);
|
|
const [internalAtflId, setInternalAtflId] = useState<string | null>(null);
|
|
const [currentFile, setCurrentFile] = useState<File | null>(null); // 현재 선택된 파일을 저장할 상태
|
|
const [fileName, setFileName] = useState<string>("");
|
|
const [files, setFiles] = useState<FileList | null>(null);
|
|
const [upload, setUpload] = useState<Upload | null>(null);
|
|
const uniqueId = useId(); // 각 FileUploadForm 인스턴스에 대한 고유 ID 생성
|
|
// FileUploader 컴포넌트를 강제로 재마운트하기 위한 key 상태 추가
|
|
const [uploaderKey, setUploaderKey] = useState(0);
|
|
|
|
const quater = [
|
|
{ text: "1분기", val: "1" },
|
|
{ text: "2분기", val: "2" },
|
|
{ text: "3분기", val: "3" },
|
|
{ text: "4분기", val: "4" },
|
|
];
|
|
// 현재 년도부터 10년 전까지의 년도 옵션 생성
|
|
const yearOptions = Array.from({ length: 10 }, (_, i) => {
|
|
const year = new Date().getFullYear() - i;
|
|
return { text: `${year}년`, val: year.toString() };
|
|
});
|
|
const [selectedYear, setSelectedYear] = useState<string>(
|
|
yearOptions[0]?.val || "",
|
|
);
|
|
const [selectedQuarter, setSelectedQuarter] = useState<string>(
|
|
quater[0]?.val || "",
|
|
);
|
|
|
|
function registFile() {
|
|
if (!currentFile) {
|
|
alert("파일을 선택해주세요.");
|
|
return;
|
|
}
|
|
if (!selectedYear) {
|
|
alert("등록년도를 선택해주세요.");
|
|
return;
|
|
}
|
|
if (!selectedQuarter) {
|
|
alert("분기를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
const fileNm = currentFile.name;
|
|
if (onSubmit && files && currentFile) {
|
|
// Promise로 handleUpload 완료를 기다림
|
|
handleUploadWithResponse(files, currentFile)
|
|
.then(({ atflCd, atflId }) => {
|
|
// 업로드 완료 후 onSubmit 실행
|
|
console.log("fileNm", fileNm);
|
|
console.log("selectedYear", selectedYear);
|
|
console.log("selectedQuarter", selectedQuarter);
|
|
console.log("atflCd", atflCd);
|
|
console.log("atflId", atflId);
|
|
onSubmit(
|
|
fileNm,
|
|
selectedYear,
|
|
selectedQuarter,
|
|
atflCd || "",
|
|
atflId || "",
|
|
);
|
|
|
|
// UI 초기화
|
|
setFiles(null);
|
|
setFileName("");
|
|
setUploaderKey((prev) => prev + 1);
|
|
})
|
|
.catch((error) => {
|
|
console.error("파일 업로드 실패:", error);
|
|
alert("파일 업로드 중 오류가 발생했습니다.");
|
|
});
|
|
}
|
|
}
|
|
|
|
// handleUpload를 Promise로 감싸는 헬퍼 함수
|
|
function handleUploadWithResponse(
|
|
files: FileList,
|
|
file: File,
|
|
): Promise<{ atflCd: string | null; atflId: string | null }> {
|
|
return new Promise((resolve, reject) => {
|
|
const uploadResponseHandler = async (res: HttpResponse) => {
|
|
try {
|
|
const fileInfo = res.getHeader("X-File-Info");
|
|
if (!fileInfo) {
|
|
resolve({ atflCd: null, atflId: null });
|
|
return;
|
|
}
|
|
|
|
const { atflCd: newAtflCd, atflId: newAtflId } =
|
|
JSON.parse(fileInfo) ?? {};
|
|
|
|
// 내부 상태도 업데이트 (다른 용도로 사용할 수 있음)
|
|
setInternalAtflCd(newAtflCd);
|
|
setInternalAtflId(newAtflId);
|
|
setFileName(file.name);
|
|
|
|
if (onFileSelect) {
|
|
onFileSelect(file, newAtflCd, newAtflId);
|
|
}
|
|
|
|
// Promise 완료
|
|
resolve({ atflCd: newAtflCd, atflId: newAtflId });
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
};
|
|
|
|
const uploadErrorHandler = (error: Error) => {
|
|
console.error("Upload failed:", error);
|
|
reject(error);
|
|
};
|
|
|
|
console.log("Starting upload for files:", files);
|
|
|
|
// 기존 handleUpload 유틸리티 사용 (수정하지 않음)
|
|
handleUpload(files, {
|
|
handleUploadStart: setUpload,
|
|
handleSuccess: setUpload,
|
|
handleResponse: uploadResponseHandler,
|
|
});
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{" "}
|
|
{/* 컴포넌트 루트 div */}
|
|
<div className="card p-6">
|
|
{" "}
|
|
{/* 카드 스타일링 */}
|
|
<div className="flex items-center gap-2">
|
|
{" "}
|
|
{/* FileUploader와 Button을 정렬하는 메인 flex 컨테이너 */}
|
|
<div className="mr-4 w-[300px]">
|
|
{" "}
|
|
{/* FileUploader의 너비와 오른쪽 여백을 제어하는 컨테이너 */}
|
|
<FileUploader
|
|
key={uploaderKey} // key 추가: 변경 시 컴포넌트 재마운트됨
|
|
id={uniqueId}
|
|
className="!h-auto !max-w-none flex-row items-center gap-2 [&_.file-uploader-label]:h-[3.2rem]"
|
|
accept={accept}
|
|
onUpload={(e) => {
|
|
const selectedFiles = e.target.files;
|
|
if (!selectedFiles || selectedFiles.length === 0) {
|
|
setFiles(null);
|
|
setCurrentFile(null);
|
|
setInternalAtflCd(null);
|
|
setInternalAtflId(null);
|
|
if (onFileSelect) {
|
|
onFileSelect(null, null, null); // 파일 선택 취소 시 null 전달
|
|
}
|
|
return;
|
|
}
|
|
|
|
const firstFile = selectedFiles[0];
|
|
if (!firstFile) return; // 타입스크립트 undefined 가능성 제거
|
|
|
|
setFiles(selectedFiles);
|
|
setFileName(firstFile.name);
|
|
setCurrentFile(firstFile);
|
|
|
|
console.log("selectedFiles", selectedFiles);
|
|
console.log("firstFile", firstFile);
|
|
console.log("firstFile name", firstFile.name);
|
|
|
|
if (onFileSelect) {
|
|
onFileSelect(firstFile, internalAtflCd, internalAtflId);
|
|
}
|
|
}}
|
|
customPreview={
|
|
showPreview ? (
|
|
<div className="file-uploader-preview text-center">
|
|
{files && files.length > 0
|
|
? fileName
|
|
: "파일을 선택하세요."}
|
|
</div>
|
|
) : null
|
|
}
|
|
/>
|
|
</div>
|
|
{showButton ? ( // showButton이 true일 때만 버튼 렌더링
|
|
<Button
|
|
className="hover:bg-primary/90 rounded-sm bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
|
|
onClick={() => registFile()}
|
|
>
|
|
등록
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FileUploadForm;
|