Files
frontend/apps/sn/app/components/ui/fileUploadForm.tsx
T

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;