From 92ddb4f13d48cbf344dc2bf63df4457b3c713608 Mon Sep 17 00:00:00 2001
From: rlaks5757 <rlaks5757@gmail.com>
Date: Wed, 26 Mar 2025 16:51:54 +0900
Subject: feat: report batch download 기능 완료
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../form-data/form-data-report-batch-dialog.tsx    | 348 +++++++++++++++++++++
 1 file changed, 348 insertions(+)
 create mode 100644 components/form-data/form-data-report-batch-dialog.tsx

(limited to 'components/form-data/form-data-report-batch-dialog.tsx')

diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx
new file mode 100644
index 00000000..614f890e
--- /dev/null
+++ b/components/form-data/form-data-report-batch-dialog.tsx
@@ -0,0 +1,348 @@
+"use client";
+
+import React, {
+  FC,
+  Dispatch,
+  SetStateAction,
+  useState,
+  useEffect,
+} from "react";
+import { useToast } from "@/hooks/use-toast";
+import prettyBytes from "pretty-bytes";
+import { X, Loader2 } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+  DialogDescription,
+  DialogFooter,
+} from "@/components/ui/dialog";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Label } from "@/components/ui/label";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  Dropzone,
+  DropzoneDescription,
+  DropzoneInput,
+  DropzoneTitle,
+  DropzoneUploadIcon,
+  DropzoneZone,
+} from "@/components/ui/dropzone";
+import {
+  FileList,
+  FileListAction,
+  FileListDescription,
+  FileListHeader,
+  FileListIcon,
+  FileListInfo,
+  FileListItem,
+  FileListName,
+} from "@/components/ui/file-list";
+import { Button } from "@/components/ui/button";
+import { getReportTempList, getOrigin } from "@/lib/forms/services";
+import { DataTableColumnJSON } from "./form-data-table-columns";
+
+const MAX_FILE_SIZE = 3000000;
+
+type ReportData = {
+  [key: string]: any;
+};
+
+interface tempFile {
+  fileName: string;
+  filePath: string;
+}
+
+interface FormDataReportBatchDialogProps {
+  open: boolean;
+  setOpen: Dispatch<SetStateAction<boolean>>;
+  columnsJSON: DataTableColumnJSON[];
+  reportData: ReportData[];
+  packageId: number;
+  formId: number;
+  formCode: string;
+}
+
+export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
+  open,
+  setOpen,
+  columnsJSON,
+  reportData,
+  packageId,
+  formId,
+  formCode,
+}) => {
+  const { toast } = useToast();
+  const [tempList, setTempList] = useState<tempFile[]>([]);
+  const [selectTemp, setSelectTemp] = useState<string>("");
+  const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
+  const [isUploading, setIsUploading] = useState(false);
+
+  useEffect(() => {
+    updateReportTempList(packageId, formId, setTempList);
+  }, [packageId, formId]);
+
+  const onClose = () => {
+    if (isUploading) {
+      return;
+    }
+    setOpen(false);
+  };
+
+  // 드롭존 - 파일 드랍 처리
+  const handleDropAccepted = (acceptedFiles: File[]) => {
+    const newFiles = [...acceptedFiles];
+    setSelectedFiles(newFiles);
+  };
+
+  // 드롭존 - 파일 거부(에러) 처리
+  const handleDropRejected = (fileRejections: any[]) => {
+    fileRejections.forEach((rejection) => {
+      toast({
+        variant: "destructive",
+        title: "File Error",
+        description: `${rejection.file.name}: ${
+          rejection.errors[0]?.message || "Upload failed"
+        }`,
+      });
+    });
+  };
+
+  // 파일 제거 핸들러
+  const removeFile = (index: number) => {
+    const updatedFiles = [...selectedFiles];
+    updatedFiles.splice(index, 1);
+    setSelectedFiles(updatedFiles);
+  };
+
+  const submitData = async () => {
+    setIsUploading(true);
+
+    try {
+      const origin = await getOrigin()
+
+      const targetFiles = selectedFiles[0];
+
+      const reportDatas = reportData.map((c) => {
+        const reportValue = stringifyAllValues(c);
+
+        const reportValueMapping: { [key: string]: any } = {};
+
+        columnsJSON.forEach((c2) => {
+          const { key, label } = c2;
+
+          const objKey = label.split(" ").join("_");
+
+          reportValueMapping[objKey] = reportValue?.[key] ?? "";
+        });
+
+        return reportValueMapping;
+      });
+      const formData = new FormData();
+      formData.append("file", targetFiles);
+      formData.append("customFileName", `${formCode}.pdf`);
+      formData.append("reportDatas", JSON.stringify(reportDatas));
+      formData.append("reportTempPath", selectTemp);
+
+      const reqeustCreateReport = await fetch(
+        `${origin}/api/pdftron/createVendorDataReports`,
+        { method: "POST", body: formData }
+      );
+
+      if (reqeustCreateReport.ok) {
+        const blob = await reqeustCreateReport.blob();
+        const url = window.URL.createObjectURL(blob);
+        const a = document.createElement("a");
+        a.href = url;
+        a.download = `${formCode}.pdf`;
+        a.click();
+        window.URL.revokeObjectURL(url);
+      } else {
+        const err = await reqeustCreateReport.json();
+        console.error("에러:", err);
+      }
+    } catch (err) {
+      console.error(err);
+      toast({
+        title: "Error",
+        description: "Report 생성 중 오류가 발생했습니다.",
+        variant: "destructive",
+      });
+    } finally {
+      setIsUploading(false);
+      setSelectedFiles([])
+      setOpen(false)
+    }
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onClose}>
+      <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
+        <DialogHeader>
+          <DialogTitle>Batch Report Download</DialogTitle>
+          <DialogDescription>
+            Report Template을 선택하신 후 갑지를 업로드하여 주시기 바랍니다.
+          </DialogDescription>
+        </DialogHeader>
+        <div className="h-[60px]">
+          <Label>Report Template Select</Label>
+          <Select value={selectTemp} onValueChange={setSelectTemp}>
+            <SelectTrigger className="w-[100%]">
+              <SelectValue placeholder="사용하시고자하는 Report Template를 선택하여 주시기 바랍니다." />
+            </SelectTrigger>
+            <SelectContent>
+              {tempList.map((c) => {
+                const { fileName, filePath } = c;
+
+                return (
+                  <SelectItem key={filePath} value={filePath}>
+                    {fileName}
+                  </SelectItem>
+                );
+              })}
+            </SelectContent>
+          </Select>
+        </div>
+        <div>
+          <Label>Report Cover Page Upload</Label>
+          <Dropzone
+            maxSize={MAX_FILE_SIZE}
+            multiple={false}
+            accept={{ accept: [".docx"] }}
+            onDropAccepted={handleDropAccepted}
+            onDropRejected={handleDropRejected}
+            disabled={isUploading}
+          >
+            {({ maxSize }) => (
+              <>
+                <DropzoneZone className="flex justify-center">
+                  <DropzoneInput />
+                  <div className="flex items-center gap-6">
+                    <DropzoneUploadIcon />
+                    <div className="grid gap-0.5">
+                      <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
+                      <DropzoneDescription>
+                        또는 클릭하여 파일을 선택하세요. 최대 크기:{" "}
+                        {maxSize ? prettyBytes(maxSize) : "무제한"}
+                      </DropzoneDescription>
+                    </div>
+                  </div>
+                </DropzoneZone>
+                <Label className="text-xs text-muted-foreground">
+                  여러 파일을 선택할 수 있습니다.
+                </Label>
+              </>
+            )}
+          </Dropzone>
+        </div>
+
+        {selectedFiles.length > 0 && (
+          <div className="grid gap-2">
+            <div className="flex items-center justify-between">
+              <h6 className="text-sm font-semibold">
+                선택된 파일 ({selectedFiles.length})
+              </h6>
+              <Badge variant="secondary">{selectedFiles.length}개 파일</Badge>
+            </div>
+            <ScrollArea>
+              <UploadFileItem
+                selectedFiles={selectedFiles}
+                removeFile={removeFile}
+                isUploading={isUploading}
+              />
+            </ScrollArea>
+          </div>
+        )}
+
+        <DialogFooter>
+          <Button
+            disabled={selectedFiles.length === 0 || selectTemp.length === 0 || isUploading}
+            onClick={submitData}
+          >
+            {isUploading && <Loader2 />}다운로드
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+};
+
+interface UploadFileItemProps {
+  selectedFiles: File[];
+  removeFile: (index: number) => void;
+  isUploading: boolean;
+}
+
+const UploadFileItem: FC<UploadFileItemProps> = ({
+  selectedFiles,
+  removeFile,
+  isUploading,
+}) => {
+  return (
+    <FileList className="max-h-[200px] gap-3">
+      {selectedFiles.map((file, index) => (
+        <FileListItem key={index} className="p-3">
+          <FileListHeader>
+            <FileListIcon />
+            <FileListInfo>
+              <FileListName>{file.name}</FileListName>
+              <FileListDescription>
+                {prettyBytes(file.size)}
+              </FileListDescription>
+            </FileListInfo>
+            <FileListAction
+              onClick={() => removeFile(index)}
+              disabled={isUploading}
+            >
+              <X className="h-4 w-4" />
+              <span className="sr-only">Remove</span>
+            </FileListAction>
+          </FileListHeader>
+        </FileListItem>
+      ))}
+    </FileList>
+  );
+};
+
+type UpdateReportTempList = (
+  packageId: number,
+  formId: number,
+  setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>>
+) => void;
+
+const updateReportTempList: UpdateReportTempList = async (
+  packageId,
+  formId,
+  setTempList
+) => {
+  const tempList = await getReportTempList(packageId, formId);
+
+  setTempList(
+    tempList.map((c) => {
+      const { fileName, filePath } = c;
+      return { fileName, filePath };
+    })
+  );
+};
+
+const stringifyAllValues = (obj: any): any => {
+  if (Array.isArray(obj)) {
+    return obj.map((item) => stringifyAllValues(item));
+  } else if (typeof obj === "object" && obj !== null) {
+    const result: any = {};
+    for (const key in obj) {
+      result[key] = stringifyAllValues(obj[key]);
+    }
+    return result;
+  } else {
+    return obj !== null && obj !== undefined ? String(obj) : "";
+  }
+};
-- 
cgit v1.2.3


From c4c3f12b4a6d1a3c09b797e1a160747cac8761e9 Mon Sep 17 00:00:00 2001
From: rlaks5757 <rlaks5757@gmail.com>
Date: Fri, 28 Mar 2025 11:27:25 +0900
Subject: template file download 개발 완료
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../form-data/form-data-report-batch-dialog.tsx    |  13 +-
 components/form-data/form-data-report-dialog.tsx   | 139 ++-------
 .../form-data-report-temp-upload-dialog.tsx        | 340 ++++++++++++++++++---
 components/form-data/form-data-table.tsx           |  47 +--
 db/schema/vendorData.ts                            |  19 --
 lib/forms/services.ts                              |  53 +++-
 .../sample_template_file.docx                      | Bin 0 -> 24773 bytes
 7 files changed, 399 insertions(+), 212 deletions(-)
 create mode 100644 public/vendorFormReportSample/sample_template_file.docx

(limited to 'components/form-data/form-data-report-batch-dialog.tsx')

diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx
index 614f890e..6c690363 100644
--- a/components/form-data/form-data-report-batch-dialog.tsx
+++ b/components/form-data/form-data-report-batch-dialog.tsx
@@ -8,8 +8,10 @@ import React, {
   useEffect,
 } from "react";
 import { useToast } from "@/hooks/use-toast";
+import { toast as toastMessage} from "sonner";
 import prettyBytes from "pretty-bytes";
 import { X, Loader2 } from "lucide-react";
+import { saveAs } from 'file-saver';
 import { Badge } from "@/components/ui/badge";
 import {
   Dialog,
@@ -159,15 +161,14 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
 
       if (reqeustCreateReport.ok) {
         const blob = await reqeustCreateReport.blob();
-        const url = window.URL.createObjectURL(blob);
-        const a = document.createElement("a");
-        a.href = url;
-        a.download = `${formCode}.pdf`;
-        a.click();
-        window.URL.revokeObjectURL(url);
+
+        saveAs(blob, `${formCode}.pdf`);        
+
+        toastMessage.success("Report 다운로드 완료!")
       } else {
         const err = await reqeustCreateReport.json();
         console.error("에러:", err);
+        throw new Error(err.message)
       }
     } catch (err) {
       console.error(err);
diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx
index deb0873b..e28b4345 100644
--- a/components/form-data/form-data-report-dialog.tsx
+++ b/components/form-data/form-data-report-dialog.tsx
@@ -8,11 +8,10 @@ import React, {
   useEffect,
   useRef,
 } from "react";
-import { WebViewerInstance, Core } from "@pdftron/webviewer";
-import { useToast } from "@/hooks/use-toast";
-import prettyBytes from "pretty-bytes";
-import { X, Loader2 } from "lucide-react";
-import { Badge } from "@/components/ui/badge";
+import { WebViewerInstance } from "@pdftron/webviewer";
+import { Loader2 } from "lucide-react";
+import { saveAs } from "file-saver";
+import { toast } from "sonner";
 import {
   Dialog,
   DialogContent,
@@ -25,11 +24,11 @@ import { Label } from "@/components/ui/label";
 import {
   Select,
   SelectContent,
-  SelectGroup,
   SelectItem,
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
+
 import { Button } from "@/components/ui/button";
 import { getReportTempList } from "@/lib/forms/services";
 import { DataTableColumnJSON } from "./form-data-table-columns";
@@ -58,7 +57,9 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
   setReportData,
   packageId,
   formId,
+  formCode,
 }) => {
+  
   const [tempList, setTempList] = useState<tempFile[]>([]);
   const [selectTemp, setSelectTemp] = useState<string>("");
   const [instance, setInstance] = useState<null | WebViewerInstance>(null);
@@ -92,46 +93,9 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
         // },
       });
 
-      const blob = new Blob([fileData], {
-        type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-      });
+      saveAs(new Blob([fileData]), fileName);      
 
-      const link = document.createElement("a");
-      link.href = URL.createObjectURL(blob);
-      link.download = fileName;
-      document.body.appendChild(link);
-      link.click();
-      document.body.removeChild(link);
-
-      // const allTabs = UI.TabManager.getAllTabs() as {
-      //   id: number;
-      //   src: Core.Document;
-      // }[];
-
-      // for (const tab of allTabs) {
-      //   // await UI.TabManager.setActiveTab(tab.id);
-      //   await activateTabAndWaitForLoad(instance, tab.id);
-      //   const tabDoc = tab.src;
-      //   const fileName = tabDoc.getFilename();
-
-      //   const fileData = await tabDoc.getFileData({
-      //     includeAnnotations: true,
-      //   });
-
-      //   console.log({ fileData });
-
-      //   const blob = new Blob([fileData], {
-      //     type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-      //   });
-
-      //   // 다운로드
-      //   // const link = document.createElement("a");
-      //   // link.href = URL.createObjectURL(blob);
-      //   // link.download = fileName;
-      //   // document.body.appendChild(link);
-      //   // link.click();
-      //   // document.body.removeChild(link);
-      // }
+      toast.success("Report 다운로드 완료!");      
     }
   };
 
@@ -175,6 +139,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
             instance={instance}
             setInstance={setInstance}
             setFileLoading={setFileLoading}
+            formCode={formCode}
           />
         </div>
 
@@ -195,6 +160,7 @@ interface ReportWebViewerProps {
   instance: null | WebViewerInstance;
   setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>;
   setFileLoading: Dispatch<SetStateAction<boolean>>;
+  formCode: string;
 }
 
 const ReportWebViewer: FC<ReportWebViewerProps> = ({
@@ -204,6 +170,7 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({
   instance,
   setInstance,
   setFileLoading,
+  formCode,
 }) => {
   const [viwerLoading, setViewerLoading] = useState<boolean>(true);
   const viewer = useRef<HTMLDivElement>(null);
@@ -234,12 +201,6 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({
               viewer.current as HTMLDivElement
             ).then(async (instance: WebViewerInstance) => {
               setInstance(instance);
-              // //Tab 메뉴 사용 필요시 활성화
-              // instance.UI.enableFeatures([instance.UI.Feature.MultiTab]);
-              // instance.UI.disableElements([
-              //   "addTabButton",
-              //   "multiTabsEmptyPage",
-              // ]);
               setViewerLoading(false);
             });
           });
@@ -262,9 +223,10 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({
       instance,
       reportDatas,
       reportTempPath,
-      setFileLoading
+      setFileLoading,
+      formCode
     );
-  }, [reportTempPath, reportDatas, instance, columnsJSON]);
+  }, [reportTempPath, reportDatas, instance, columnsJSON, formCode]);
 
   return (
     <div ref={viewer} className="h-[100%]">
@@ -319,7 +281,8 @@ type ImportReportData = (
   instance: null | WebViewerInstance,
   reportDatas: ReportData[],
   reportTempPath: string,
-  setFileLoading: Dispatch<SetStateAction<boolean>>
+  setFileLoading: Dispatch<SetStateAction<boolean>>,
+  formCode: string
 ) => void;
 
 const importReportData: ImportReportData = async (
@@ -327,7 +290,8 @@ const importReportData: ImportReportData = async (
   instance,
   reportDatas,
   reportTempPath,
-  setFileLoading
+  setFileLoading,
+  formCode
 ) => {
   setFileLoading(true);
   try {
@@ -352,12 +316,13 @@ const importReportData: ImportReportData = async (
       });
 
       const doc = await createDocument(reportFileBlob, {
+        filename: `${formCode}_report.docx`,
         extension: "docx",
       });
 
       await doc.applyTemplateValues(reportValueMapping);
 
-      documentViewer.loadDocument(doc, {
+      documentViewer.loadDocument(doc, {        
         extension: "docx",
         enableOfficeEditing: true,
         officeOptions: {
@@ -373,68 +338,6 @@ const importReportData: ImportReportData = async (
   }
 };
 
-const importReportDataTab: ImportReportData = async (
-  columnJSON,
-  instance,
-  reportDatas,
-  reportTempPath,
-  setFileLoading
-) => {
-  setFileLoading(true);
-  try {
-    if (instance && reportDatas.length > 0 && reportTempPath.length > 0) {
-      const { UI, Core } = instance;
-      const { createDocument } = Core;
-
-      const getFileData = await fetch(reportTempPath);
-      const reportFileBlob = await getFileData.blob();
-
-      const prevTab = UI.TabManager.getAllTabs();
-
-      (prevTab as object[] as { id: number }[]).forEach((c) => {
-        const { id } = c;
-        UI.TabManager.deleteTab(id);
-      });
-
-      const fileOptions = reportDatas.map((c) => {
-        const { tagNumber } = c;
-
-        const options = {
-          filename: `${tagNumber}_report.docx`,
-        };
-
-        return { options, reportData: c };
-      });
-
-      const tabIds = [];
-
-      for (const fileOption of fileOptions) {
-        let doc = await createDocument(reportFileBlob, {
-          ...fileOption.options,
-          extension: "docx",
-        });
-
-        await doc.applyTemplateValues(
-          stringifyAllValues(fileOption.reportData)
-        );
-
-        const tab = await UI.TabManager.addTab(doc, {
-          ...fileOption.options,
-        });
-
-        tabIds.push(tab); // 탭 ID 저장
-      }
-
-      if (tabIds.length > 0) {
-        await UI.TabManager.setActiveTab(tabIds[0]);
-      }
-    }
-  } catch (err) {
-  } finally {
-    setFileLoading(false);
-  }
-};
-
 type UpdateReportTempList = (
   packageId: number,
   formId: number,
diff --git a/components/form-data/form-data-report-temp-upload-dialog.tsx b/components/form-data/form-data-report-temp-upload-dialog.tsx
index 413c1e51..69df704e 100644
--- a/components/form-data/form-data-report-temp-upload-dialog.tsx
+++ b/components/form-data/form-data-report-temp-upload-dialog.tsx
@@ -8,8 +8,11 @@ import React, {
   useEffect,
 } from "react";
 import { useToast } from "@/hooks/use-toast";
+import { toast as toastMessage } from "sonner";
 import prettyBytes from "pretty-bytes";
-import { X, Loader2, Download } from "lucide-react";
+import { X, Loader2, Download, Delete, Trash2 } from "lucide-react";
+import ExcelJS from "exceljs";
+import { saveAs } from "file-saver";
 import { Badge } from "@/components/ui/badge";
 import {
   Dialog,
@@ -40,10 +43,28 @@ import {
   FileListItem,
   FileListName,
 } from "@/components/ui/file-list";
-import { getReportTempList, uploadReportTemp } from "@/lib/forms/services";
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+  AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import {
+  getReportTempList,
+  uploadReportTemp,
+  getReportTempFileData,
+  deleteReportTempFile,
+} from "@/lib/forms/services";
 import { VendorDataReportTemps } from "@/db/schema/vendorData";
+import { DataTableColumnJSON } from "./form-data-table-columns";
 
 interface FormDataReportTempUploadDialogProps {
+  columnsJSON: DataTableColumnJSON[];
   open: boolean;
   setOpen: Dispatch<SetStateAction<boolean>>;
   packageId: number;
@@ -57,7 +78,15 @@ const MAX_FILE_SIZE = 3000000;
 
 export const FormDataReportTempUploadDialog: FC<
   FormDataReportTempUploadDialogProps
-> = ({ open, setOpen, packageId, formId, uploaderType }) => {
+> = ({
+  columnsJSON,
+  open,
+  setOpen,
+  packageId,
+  formId,
+  formCode,
+  uploaderType,
+}) => {
   const { toast } = useToast();
   const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
   const [isUploading, setIsUploading] = useState(false);
@@ -131,29 +160,106 @@ export const FormDataReportTempUploadDialog: FC<
     }
   };
 
+  const downloadTempFile = async () => {
+    try {
+      const { fileName, fileType, base64 } = await getReportTempFileData();
+
+      saveAs(`data:${fileType};base64,${base64}`, fileName);
+
+      toastMessage.success("Report Sample File 다운로드 완료!");
+    } catch (err) {
+      console.log(err);
+      toast({
+        title: "Error",
+        description: "Sample File을 찾을 수가 없습니다.",
+        variant: "destructive",
+      });
+    }
+  };
+
+  const downloadReportVarList = async () => {
+    try {
+      // Create a new workbook
+      const workbook = new ExcelJS.Workbook();
+
+      // 데이터 시트 생성
+      const worksheet = workbook.addWorksheet("Data");
+
+      // 유효성 검사용 숨김 시트 생성
+      const validationSheet = workbook.addWorksheet("ValidationData");
+      validationSheet.state = "hidden"; // 시트 숨김 처리
+
+      // 1. 데이터 시트에 헤더 추가
+      const headers = ["Table Column Label", "Report Variable"];
+      worksheet.addRow(headers);
+
+      // 헤더 스타일 적용
+      const headerRow = worksheet.getRow(1);
+      headerRow.font = { bold: true };
+      headerRow.alignment = { horizontal: "center" };
+      headerRow.eachCell((cell) => {
+        cell.fill = {
+          type: "pattern",
+          pattern: "solid",
+          fgColor: { argb: "FFCCCCCC" },
+        };
+      });
+
+      // 2. 데이터 행 추가
+      columnsJSON.forEach((row) => {
+        const { displayLabel, label } = row;
+
+        const labelConvert = label.replaceAll(" ", "_");
+
+        worksheet.addRow([displayLabel, labelConvert]);
+      });
+
+      // 3. 컬럼 너비 자동 조정
+      headers.forEach((col, idx) => {
+        const column = worksheet.getColumn(idx + 1);
+
+        // 최적 너비 계산
+        let maxLength = col.length;
+        columnsJSON.forEach((row) => {
+          const valueKey = idx === 0 ? "displayLabel" : "label";
+
+          const value = row[valueKey];
+          if (value !== undefined && value !== null) {
+            const valueLength = String(value).length;
+            if (valueLength > maxLength) {
+              maxLength = valueLength;
+            }
+          }
+        });
+
+        // 너비 설정 (최소 10, 최대 50)
+        column.width = Math.min(Math.max(maxLength + 2, 10), 50);
+      });
+
+      const buffer = await workbook.xlsx.writeBuffer();
+      saveAs(new Blob([buffer]), `${formCode}_report_varible_list.xlsx`);
+      toastMessage.success("Report Varible List File 다운로드 완료!");
+    } catch (err) {
+      console.log(err);
+      toast({
+        title: "Error",
+        description: "Variable List 파일을 찾을 수가 없습니다.",
+        variant: "destructive",
+      });
+    }
+  };
+
   return (
     <Dialog open={open} onOpenChange={setOpen}>
       <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
         <DialogHeader>
           <DialogTitle>Report Template Upload</DialogTitle>
           <DialogDescription>
-            사용하시고자 하는 Report Template(docx File)를 업로드 하여주시기
+            사용하시고자 하는 Report Template(.docx)를 업로드 하여주시기
             바랍니다.
           </DialogDescription>
         </DialogHeader>
-        {/* {prevReportTemp.length > 0 && (
-          <>
-            <Label>Prev Report Template</Label>
-            <ScrollArea className="max-h-[100px]">
-              {prevReportTemp.map((c, i) => {
-                return <div key={i}>{i}</div>;
-              })}
-            </ScrollArea>
-          </>
-        )} */}
-        <div
-        //  className="flex flex-col gap-2"
-        >
+        <div>
           <Label>Sample Template Download</Label>
 
           <FileList className="max-h-[200px] gap-3">
@@ -163,10 +269,19 @@ export const FormDataReportTempUploadDialog: FC<
                 <FileListInfo>
                   <FileListName>sample_template_file.docx</FileListName>
                 </FileListInfo>
-                <FileListAction
-                // onClick={() => removeFile(index)}
-                // disabled={isUploading}
-                >
+                <FileListAction onClick={downloadTempFile}>
+                  <Download className="h-4 w-4" />
+                  <span className="sr-only">Download</span>
+                </FileListAction>
+              </FileListHeader>
+            </FileListItem>
+            <FileListItem className="p-3">
+              <FileListHeader>
+                <FileListIcon />
+                <FileListInfo>
+                  <FileListName>report_variable_list.xlsx</FileListName>
+                </FileListInfo>
+                <FileListAction onClick={downloadReportVarList}>
                   <Download className="h-4 w-4" />
                   <span className="sr-only">Download</span>
                 </FileListAction>
@@ -174,36 +289,48 @@ export const FormDataReportTempUploadDialog: FC<
             </FileListItem>
           </FileList>
         </div>
+        <div>
+          <Label>Uploaded Template Files</Label>
+          <UploadedTempFiles
+            prevReportTemp={prevReportTemp}
+            updateReportTempList={() =>
+              updateReportTempList(packageId, formId, setPrevReportTemp)
+            }
+          />
+        </div>
 
-        <Dropzone
-          maxSize={MAX_FILE_SIZE}
-          multiple={true}
-          accept={{ accept: [".docx"] }}
-          onDropAccepted={handleDropAccepted}
-          onDropRejected={handleDropRejected}
-          disabled={isUploading}
-        >
-          {({ maxSize }) => (
-            <>
-              <DropzoneZone className="flex justify-center">
-                <DropzoneInput />
-                <div className="flex items-center gap-6">
-                  <DropzoneUploadIcon />
-                  <div className="grid gap-0.5">
-                    <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
-                    <DropzoneDescription>
-                      또는 클릭하여 파일을 선택하세요. 최대 크기:{" "}
-                      {maxSize ? prettyBytes(maxSize) : "무제한"}
-                    </DropzoneDescription>
+        <div>
+          <Label>Report Template File Upload(.docx)</Label>
+          <Dropzone
+            maxSize={MAX_FILE_SIZE}
+            multiple={true}
+            accept={{ accept: [".docx"] }}
+            onDropAccepted={handleDropAccepted}
+            onDropRejected={handleDropRejected}
+            disabled={isUploading}
+          >
+            {({ maxSize }) => (
+              <>
+                <DropzoneZone className="flex justify-center">
+                  <DropzoneInput />
+                  <div className="flex items-center gap-6">
+                    <DropzoneUploadIcon />
+                    <div className="grid gap-0.5">
+                      <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
+                      <DropzoneDescription>
+                        또는 클릭하여 파일을 선택하세요. 최대 크기:{" "}
+                        {maxSize ? prettyBytes(maxSize) : "무제한"}
+                      </DropzoneDescription>
+                    </div>
                   </div>
-                </div>
-              </DropzoneZone>
-              <Label className="text-xs text-muted-foreground">
-                여러 파일을 선택할 수 있습니다.
-              </Label>
-            </>
-          )}
-        </Dropzone>
+                </DropzoneZone>
+                <Label className="text-xs text-muted-foreground">
+                  여러 파일을 선택할 수 있습니다.
+                </Label>
+              </>
+            )}
+          </Dropzone>
+        </div>
 
         {selectedFiles.length > 0 && (
           <div className="grid gap-2">
@@ -247,7 +374,7 @@ const UploadFileItem: FC<UploadFileItemProps> = ({
   isUploading,
 }) => {
   return (
-    <FileList className="max-h-[200px] gap-3">
+    <FileList className="max-h-[100px] gap-3">
       {selectedFiles.map((file, index) => (
         <FileListItem key={index} className="p-3">
           <FileListHeader>
@@ -327,3 +454,116 @@ const updateReportTempList: UpdateReportTempList = async (
   const tempList = await getReportTempList(packageId, formId);
   setPrevReportTemp(tempList);
 };
+
+interface UploadedTempFiles {
+  prevReportTemp: VendorDataReportTemps[];
+  updateReportTempList: () => void;
+}
+
+const UploadedTempFiles: FC<UploadedTempFiles> = ({
+  prevReportTemp,
+  updateReportTempList,
+}) => {
+  const { toast } = useToast();
+
+  const downloadTempFile = async (fileName: string, filePath: string) => {
+    try {
+      const getTempFile = await fetch(filePath);
+
+      if (getTempFile.ok) {
+        const blob = await getTempFile.blob();
+
+        saveAs(blob, fileName);
+
+        toastMessage.success("Report 다운로드 완료!");
+      } else {
+        const err = await getTempFile.json();
+        console.error("에러:", err);
+        throw new Error(err.message);
+      }
+
+      toastMessage.success("Template File 다운로드 완료!");
+    } catch (err) {
+      console.error(err)
+      toast({
+        title: "Error",
+        description: "Template File 다운로드 중 오류가 발생했습니다.",
+        variant: "destructive",
+      });
+    }
+  };
+
+  const deleteTempFile = async (id: number) => {
+    try {
+      const { result, error } = await deleteReportTempFile(id);
+
+      if (result) {
+        updateReportTempList();
+        toastMessage.success("Template File 삭제 완료!");
+      } else {
+        throw new Error(error);
+      }
+    } catch (err) {
+      toast({
+        title: "Error",
+        description: "Template File 삭제 중 오류가 발생했습니다.",
+        variant: "destructive",
+      });
+    }
+  };
+
+  return (
+    <ScrollArea>
+      <FileList className="max-h-[100px] gap-3">
+        {prevReportTemp.map((c) => {
+          const { fileName, filePath, id } = c;
+
+          return (
+            <AlertDialog key={id}>
+              <FileListItem className="p-3">
+                <FileListHeader>
+                  <FileListIcon />
+                  <FileListInfo>
+                    <FileListName>{fileName}</FileListName>
+                  </FileListInfo>
+                  <FileListAction
+                    onClick={() => {
+                      downloadTempFile(fileName, filePath);
+                    }}
+                  >
+                    <Download className="h-4 w-4" />
+                    <span className="sr-only">Download</span>
+                  </FileListAction>
+                  <AlertDialogTrigger asChild>
+                    <FileListAction>
+                      <Trash2 className="h-4 w-4" />
+                      <span className="sr-only">Delete</span>
+                    </FileListAction>
+                  </AlertDialogTrigger>
+                  <AlertDialogContent>
+                    <AlertDialogHeader>
+                      <AlertDialogTitle>
+                        Report Templete File({fileName})을 삭제하시겠습니까?
+                      </AlertDialogTitle>
+                      <AlertDialogDescription />
+                    </AlertDialogHeader>
+                    <AlertDialogFooter>
+                      <AlertDialogCancel>취소</AlertDialogCancel>
+                      <AlertDialogAction
+                        onClick={() => {
+                          deleteTempFile(id);
+                        }}
+                      >
+                        삭제
+                      </AlertDialogAction>
+                    </AlertDialogFooter>
+                  </AlertDialogContent>
+                </FileListHeader>
+              </FileListItem>
+            </AlertDialog>
+          );
+        })}
+      </FileList>
+    </ScrollArea>
+  );
+};
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 50c4f267..823416c1 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -11,19 +11,22 @@ import {
   DataTableColumnJSON,
   ColumnType,
 } from "./form-data-table-columns";
-
 import type { DataTableAdvancedFilterField } from "@/types/table";
 import { Button } from "../ui/button";
 import { Download, Loader, Save, Upload } from "lucide-react";
 import { toast } from "sonner";
 import { syncMissingTags, updateFormDataInDB } from "@/lib/forms/services";
 import { UpdateTagSheet } from "./update-form-sheet";
-
 import ExcelJS from "exceljs";
 import { saveAs } from "file-saver";
 import { FormDataReportTempUploadDialog } from "./form-data-report-temp-upload-dialog";
 import { FormDataReportDialog } from "./form-data-report-dialog";
 import { FormDataReportBatchDialog } from "./form-data-report-batch-dialog";
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from "@/components/ui/popover";
 
 interface GenericData {
   [key: string]: any;
@@ -526,20 +529,29 @@ export default function DynamicTable({
         {/* 버튼 그룹 */}
         <div className="flex items-center gap-2">
           {/* 태그 불러오기 버튼 */}
-          <Button
-            variant="default"
-            size="sm"
-            onClick={() => setBatchDownDialog(true)}
-          >
-            Report Download
-          </Button>
-          <Button
-            variant="default"
-            size="sm"
-            onClick={() => setTempUpDialog(true)}
-          >
-            Template Upload
-          </Button>
+          <Popover>
+            <PopoverTrigger asChild>
+              <Button variant="default" size="sm">
+                Report
+              </Button>
+            </PopoverTrigger>
+            <PopoverContent className="flex flex-row gap-2">
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={() => setTempUpDialog(true)}
+              >
+                Template Upload
+              </Button>
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={() => setBatchDownDialog(true)}
+              >
+                Report Download
+              </Button>
+            </PopoverContent>
+          </Popover>
           <Button
             variant="default"
             size="sm"
@@ -611,6 +623,7 @@ export default function DynamicTable({
       />
       {tempUpDialog && (
         <FormDataReportTempUploadDialog
+          columnsJSON={columnsJSON}
           open={tempUpDialog}
           setOpen={setTempUpDialog}
           packageId={contractItemId}
@@ -645,5 +658,3 @@ export default function DynamicTable({
     </>
   );
 }
-
-
diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts
index 2739e8eb..01a10b7e 100644
--- a/db/schema/vendorData.ts
+++ b/db/schema/vendorData.ts
@@ -238,19 +238,11 @@ import {
   
     createdAt: timestamp("created_at", { withTimezone: true }),
     updatedAt: timestamp("updated_at", { withTimezone: true }),
-<<<<<<< HEAD
   });
   
   export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect;
   
   export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
-=======
-})
-
-export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect
-
-export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
->>>>>>> dev
     id: serial("id").primaryKey(),
     contractItemId: integer("contract_item_id")
       .notNull()
@@ -261,7 +253,6 @@ export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
     fileName: varchar("file_name", { length: 255 }).notNull(),
     filePath: varchar("file_path", { length: 1024 }).notNull(),
     createdAt: timestamp("created_at", { withTimezone: true })
-<<<<<<< HEAD
       .defaultNow()
       .notNull(),
     updatedAt: timestamp("updated_at", { withTimezone: true })
@@ -271,13 +262,3 @@ export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
   
   export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect;
   
-=======
-    .defaultNow()
-    .notNull(),
-  updatedAt: timestamp("updated_at", { withTimezone: true })
-    .defaultNow()
-    .notNull(),
-  });
-  
-  export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect;
->>>>>>> dev
diff --git a/lib/forms/services.ts b/lib/forms/services.ts
index a1bbf003..22f10466 100644
--- a/lib/forms/services.ts
+++ b/lib/forms/services.ts
@@ -807,7 +807,7 @@ export async function uploadReportTemp(
   }
 }
 
-export const getOrigin = async ():Promise<string> => {
+export const getOrigin = async (): Promise<string> => {
   const headersList = await headers();
   const host = headersList.get("host");
   const proto = headersList.get("x-forwarded-proto") || "http"; // 기본값은 http
@@ -815,3 +815,54 @@ export const getOrigin = async ():Promise<string> => {
 
   return origin;
 };
+
+export const getReportTempFileData = async (): Promise<{
+  fileName: string;
+  fileType: string;
+  base64: string;
+}> => {
+  const fileName = "sample_template_file.docx";
+
+  const tempFile = await fs.readFile(
+    `public/vendorFormReportSample/${fileName}`
+  );
+
+  return {
+    fileName,
+    fileType:
+      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+    base64: tempFile.toString("base64"),
+  };
+};
+
+type deleteReportTempFile = (id: number) => Promise<{
+  result: boolean;
+  error?: any;
+}>;
+
+export const deleteReportTempFile: deleteReportTempFile = async (id) => {
+  try {
+    return db.transaction(async (tx) => {
+      const [targetTempFile] = await tx
+        .select()
+        .from(vendorDataReportTemps)
+        .where(eq(vendorDataReportTemps.id, id));
+
+      if (!targetTempFile) {
+        throw new Error("해당 Template File을 찾을 수 없습니다.");
+      }
+
+      await tx
+        .delete(vendorDataReportTemps)
+        .where(eq(vendorDataReportTemps.id, id));
+
+      const { filePath } = targetTempFile;
+
+      await fs.unlink("public" + filePath);
+
+      return { result: true };
+    });
+  } catch (err) {
+    return { result: false, error: (err as Error).message };
+  }
+};
diff --git a/public/vendorFormReportSample/sample_template_file.docx b/public/vendorFormReportSample/sample_template_file.docx
new file mode 100644
index 00000000..0e338eb8
Binary files /dev/null and b/public/vendorFormReportSample/sample_template_file.docx differ
-- 
cgit v1.2.3


From 54a8a851442e675afb13669509cd44ab33386987 Mon Sep 17 00:00:00 2001
From: rlaks5757 <rlaks5757@gmail.com>
Date: Fri, 28 Mar 2025 14:04:38 +0900
Subject: features merge / upload 문구 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/form-data/form-data-report-batch-dialog.tsx | 2 +-
 components/form-data/form-data-table.tsx               | 1 -
 lib/docuSign/docuSignFns.ts                            | 2 +-
 3 files changed, 2 insertions(+), 3 deletions(-)

(limited to 'components/form-data/form-data-report-batch-dialog.tsx')

diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx
index 6c690363..f4efde64 100644
--- a/components/form-data/form-data-report-batch-dialog.tsx
+++ b/components/form-data/form-data-report-batch-dialog.tsx
@@ -213,7 +213,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
           </Select>
         </div>
         <div>
-          <Label>Report Cover Page Upload</Label>
+          <Label>Report Cover Page Upload(.docx)</Label>
           <Dropzone
             maxSize={MAX_FILE_SIZE}
             multiple={false}
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 823416c1..b696f45b 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -47,7 +47,6 @@ export default function DynamicTable({
   formCode,
   formId,
 }: DynamicTableProps) {
-  console.log({ columnsJSON });
   const params = useParams();
   const lng = (params?.lng as string) || "ko";
   const { t } = useTranslation(lng, "translation");
diff --git a/lib/docuSign/docuSignFns.ts b/lib/docuSign/docuSignFns.ts
index 662ff23a..e42babaa 100644
--- a/lib/docuSign/docuSignFns.ts
+++ b/lib/docuSign/docuSignFns.ts
@@ -103,7 +103,7 @@ export async function requestContractSign(
   let accountInfo = await authenticate();
   if (accountInfo) {
     const { accessToken, basePath, apiAccountId } = accountInfo;
-    console.log({ basePath });
+    
     const {
       email: subEmail,
       name: subConName,
-- 
cgit v1.2.3

