summaryrefslogtreecommitdiff
path: root/lib/tech-vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tech-vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx')
-rw-r--r--lib/tech-vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx354
1 files changed, 354 insertions, 0 deletions
diff --git a/lib/tech-vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx b/lib/tech-vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx
new file mode 100644
index 00000000..6c622fd1
--- /dev/null
+++ b/lib/tech-vendor-rfq-response/vendor-tbe-table/tbeFileHandler.tsx
@@ -0,0 +1,354 @@
+"use client";
+
+import { useCallback, useState, useEffect } from "react";
+import { toast } from "sonner";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import {
+ fetchTbeTemplateFiles,
+ uploadTbeResponseFile,
+ getTbeSubmittedFiles,
+ getFileFromRfqAttachmentsbyid,
+} from "../../rfqs-tech/service";
+import {
+ Dropzone,
+ DropzoneDescription,
+ DropzoneInput,
+ DropzoneTitle,
+ DropzoneUploadIcon,
+ DropzoneZone,
+} from "@/components/ui/dropzone";
+import {
+ FileList,
+ FileListAction,
+ FileListDescription,
+ FileListIcon,
+ FileListInfo,
+ FileListItem,
+ FileListName,
+ FileListSize,
+} from "@/components/ui/file-list";
+import { Download, X } from "lucide-react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { formatDateTime } from "@/lib/utils";
+
+export function useTbeFileHandlers() {
+ // 모달 열림 여부, 현재 선택된 IDs
+ const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
+ const [currentTbeId, setCurrentTbeId] = useState<number | null>(null);
+ const [currentVendorId, setCurrentVendorId] = useState<number | null>(null);
+ const [currentRfqId, setCurrentRfqId] = useState<number | null>(null);
+ const [currentvendorResponseId, setCurrentvendorResponseId] = useState<number | null>(null);
+
+
+
+ // 로딩 상태들
+ const [isLoading, setIsLoading] = useState(false);
+ const [isFetchingFiles, setIsFetchingFiles] = useState(false);
+
+ // 업로드할 파일, 제출된 파일 목록
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
+ const [submittedFiles, setSubmittedFiles] = useState<
+ Array<{ id: number; fileName: string; filePath: string; uploadedAt: Date }>
+ >([]);
+
+ // ===================================
+ // 1) 제출된 파일 목록 가져오기
+ // ===================================
+ const fetchSubmittedFiles = useCallback(async (vendorResponseId: number) => {
+ if (!vendorResponseId ) return;
+
+ setIsFetchingFiles(true);
+ try {
+ const { files, error } = await getTbeSubmittedFiles(vendorResponseId);
+ if (error) {
+ console.error(error);
+ return;
+ }
+ setSubmittedFiles(files);
+ } catch (error) {
+ console.error("Failed to fetch submitted files:", error);
+ } finally {
+ setIsFetchingFiles(false);
+ }
+ }, []);
+
+ // ===================================
+ // 2) TBE 템플릿 다운로드
+ // ===================================
+ const handleDownloadTbeTemplate = useCallback(
+ async (tbeId: number, vendorId: number, rfqId: number) => {
+ setCurrentTbeId(tbeId);
+ setCurrentVendorId(vendorId);
+ setCurrentRfqId(rfqId);
+ setIsLoading(true);
+
+ try {
+ const { files, error } = await fetchTbeTemplateFiles(tbeId);
+ if (error) {
+ toast.error(error);
+ return;
+ }
+ if (files.length === 0) {
+ toast.warning("다운로드할 템플릿 파일이 없습니다");
+ return;
+ }
+ // 순차적으로 파일 다운로드
+ for (const file of files) {
+ await downloadFile(file.id);
+ }
+ toast.success("모든 템플릿 파일이 다운로드되었습니다");
+ } catch (error) {
+ toast.error("템플릿 파일을 다운로드하는 데 실패했습니다");
+ console.error(error);
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ []
+ );
+
+ // 실제 다운로드 로직
+ const downloadFile = useCallback(async (fileId: number) => {
+ try {
+ const { file, error } = await getFileFromRfqAttachmentsbyid(fileId);
+ if (error || !file) {
+ throw new Error(error || "파일 정보를 가져오는 데 실패했습니다");
+ }
+
+ const link = document.createElement("a");
+ link.href = `/api/rfq-download?path=${encodeURIComponent(file.filePath)}`;
+ link.download = file.fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ return true;
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ }, []);
+
+ // ===================================
+ // 3) 제출된 파일 다운로드
+ // ===================================
+ const downloadSubmittedFile = useCallback((file: { id: number; fileName: string; filePath: string }) => {
+ try {
+ const link = document.createElement("a");
+ link.href = `/api/tbe-download?path=${encodeURIComponent(file.filePath)}`;
+ link.download = file.fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ toast.success(`${file.fileName} 다운로드 시작`);
+ } catch (error) {
+ console.error("Failed to download file:", error);
+ toast.error("파일 다운로드에 실패했습니다");
+ }
+ }, []);
+
+ // ===================================
+ // 4) TBE 응답 업로드 모달 열기
+ // (이 시점에서는 데이터 fetch하지 않음)
+ // ===================================
+ const handleUploadTbeResponse = useCallback((tbeId: number, vendorId: number, rfqId: number, vendorResponseId:number) => {
+ setCurrentTbeId(tbeId);
+ setCurrentVendorId(vendorId);
+ setCurrentRfqId(rfqId);
+ setCurrentvendorResponseId(vendorResponseId);
+ setIsUploadDialogOpen(true);
+ }, []);
+
+ // ===================================
+ // 5) Dialog 열고 닫힐 때 상태 초기화
+ // 열렸을 때 -> useEffect로 파일 목록 가져오기
+ // ===================================
+ useEffect(() => {
+ if (!isUploadDialogOpen) {
+ // 닫힐 때는 파일 상태들 초기화
+ setSelectedFile(null);
+ setSubmittedFiles([]);
+ }
+ }, [isUploadDialogOpen]);
+
+ useEffect(() => {
+ // Dialog가 열렸고, ID들이 유효하면
+ if (isUploadDialogOpen &&currentvendorResponseId) {
+ fetchSubmittedFiles(currentvendorResponseId);
+ }
+ }, [isUploadDialogOpen, currentvendorResponseId, fetchSubmittedFiles]);
+
+ // ===================================
+ // 6) 드롭존 파일 선택 & 제거
+ // ===================================
+ const handleFileDrop = useCallback((files: File[]) => {
+ if (files && files.length > 0) {
+ setSelectedFile(files[0]);
+ }
+ }, []);
+
+ const handleRemoveFile = useCallback(() => {
+ setSelectedFile(null);
+ }, []);
+
+ // ===================================
+ // 7) 응답 파일 업로드
+ // ===================================
+ const handleSubmitResponse = useCallback(async () => {
+ if (!selectedFile || !currentTbeId || !currentVendorId || !currentRfqId ||!currentvendorResponseId) {
+ toast.error("업로드할 파일을 선택해주세요");
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ // FormData 생성
+ const formData = new FormData();
+ formData.append("file", selectedFile);
+ formData.append("rfqId", currentRfqId.toString());
+ formData.append("vendorId", currentVendorId.toString());
+ formData.append("evaluationId", currentTbeId.toString());
+ formData.append("vendorResponseId", currentvendorResponseId.toString());
+
+ const result = await uploadTbeResponseFile(formData);
+ if (!result.success) {
+ throw new Error(result.error || "파일 업로드에 실패했습니다");
+ }
+
+ toast.success(result.message || "응답이 성공적으로 업로드되었습니다");
+
+ // 업로드 후 다시 제출된 파일 목록 가져오기
+ await fetchSubmittedFiles(currentvendorResponseId);
+
+ // 업로드 성공 시 선택 파일 초기화
+ setSelectedFile(null);
+
+ // 페이지 새로고침으로 테이블 데이터 업데이트
+ window.location.reload();
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : "응답 업로드에 실패했습니다");
+ console.error(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [selectedFile, currentTbeId, currentVendorId, currentRfqId, currentvendorResponseId,fetchSubmittedFiles]);
+
+ // ===================================
+ // 8) 실제 Dialog 컴포넌트
+ // ===================================
+ const UploadDialog = () => (
+ <Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}>
+ <DialogContent className="sm:max-w-lg">
+ <DialogHeader>
+ <DialogTitle>TBE 응답 파일</DialogTitle>
+ <DialogDescription>제출된 파일을 확인하거나 새 파일을 업로드하세요.</DialogDescription>
+ </DialogHeader>
+
+ <Tabs defaultValue="upload" className="w-full">
+ <TabsList className="grid w-full grid-cols-2">
+ <TabsTrigger value="upload">새 파일 업로드</TabsTrigger>
+ <TabsTrigger
+ value="submitted"
+ disabled={submittedFiles.length === 0}
+ className={submittedFiles.length > 0 ? "relative" : ""}
+ >
+ 제출된 파일{" "}
+ {submittedFiles.length > 0 && (
+ <span className="ml-2 inline-flex items-center justify-center rounded-full bg-primary w-4 h-4 text-[10px] text-primary-foreground">
+ {submittedFiles.length}
+ </span>
+ )}
+ </TabsTrigger>
+ </TabsList>
+
+ {/* 업로드 탭 */}
+ <TabsContent value="upload" className="pt-4">
+ <div className="grid gap-4">
+ {selectedFile ? (
+ <FileList>
+ <FileListItem>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>{selectedFile.name}</FileListName>
+ <FileListSize>{selectedFile.size}</FileListSize>
+ </FileListInfo>
+ <FileListAction onClick={handleRemoveFile}>
+ <X className="h-4 w-4" />
+ <span className="sr-only">파일 제거</span>
+ </FileListAction>
+ </FileListItem>
+ </FileList>
+ ) : (
+ <Dropzone onDrop={handleFileDrop}>
+ <DropzoneInput className="sr-only" />
+ <DropzoneZone className="flex flex-col items-center justify-center gap-2 p-6">
+ <DropzoneUploadIcon className="h-10 w-10 text-muted-foreground" />
+ <DropzoneTitle>파일을 드래그하거나 클릭하여 업로드</DropzoneTitle>
+ <DropzoneDescription>TBE 응답 파일 (XLSX, XLS, DOCX, PDF 등)</DropzoneDescription>
+ </DropzoneZone>
+ </Dropzone>
+ )}
+
+ <DialogFooter className="mt-4">
+ <Button type="submit" onClick={handleSubmitResponse} disabled={!selectedFile || isLoading}>
+ {isLoading ? "업로드 중..." : "응답 업로드"}
+ </Button>
+ </DialogFooter>
+ </div>
+ </TabsContent>
+
+ {/* 제출된 파일 탭 */}
+ <TabsContent value="submitted" className="pt-4">
+ {isFetchingFiles ? (
+ <div className="flex justify-center items-center py-8">
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
+ </div>
+ ) : submittedFiles.length > 0 ? (
+ <div className="grid gap-2">
+ <FileList>
+ {submittedFiles.map((file) => (
+ <FileListItem key={file.id} className="flex items-center justify-between gap-3">
+ <div className="flex items-center gap-3 flex-1">
+ <FileListIcon className="flex-shrink-0" />
+ <FileListInfo className="flex-1 min-w-0">
+ <FileListName className="text-sm font-medium truncate">{file.fileName}</FileListName>
+ <FileListDescription className="text-xs text-muted-foreground">
+ {file.uploadedAt ? formatDateTime(file.uploadedAt) : ""}
+ </FileListDescription>
+ </FileListInfo>
+ </div>
+ <FileListAction className="flex-shrink-0 ml-2" onClick={() => downloadSubmittedFile(file)}>
+ <Download className="h-4 w-4" />
+ <span className="sr-only">파일 다운로드</span>
+ </FileListAction>
+ </FileListItem>
+ ))}
+ </FileList>
+ </div>
+ ) : (
+ <div className="text-center py-8 text-muted-foreground">제출된 파일이 없습니다.</div>
+ )}
+ </TabsContent>
+ </Tabs>
+ </DialogContent>
+ </Dialog>
+ );
+
+ // ===================================
+ // 9) Hooks 내보내기
+ // ===================================
+ return {
+ handleDownloadTbeTemplate,
+ handleUploadTbeResponse,
+ UploadDialog,
+ };
+} \ No newline at end of file