From e0b2367d88dd80eece67390574e60c9eacdee14d Mon Sep 17 00:00:00 2001
From: rlaks5757 <rlaks5757@gmail.com>
Date: Wed, 26 Mar 2025 16:51:54 +0900
Subject: po, vendor-data-form-report

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

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

diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx
new file mode 100644
index 00000000..5ddc5e0c
--- /dev/null
+++ b/components/form-data/form-data-report-dialog.tsx
@@ -0,0 +1,457 @@
+"use client";
+
+import React, {
+  FC,
+  Dispatch,
+  SetStateAction,
+  useState,
+  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 {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+  DialogDescription,
+  DialogFooter,
+} from "@/components/ui/dialog";
+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";
+
+type ReportData = {
+  [key: string]: any;
+};
+
+interface tempFile {
+  fileName: string;
+  filePath: string;
+}
+
+interface FormDataReportDialogProps {
+  columnsJSON: DataTableColumnJSON[];
+  reportData: ReportData[];
+  setReportData: Dispatch<SetStateAction<ReportData[]>>;
+  packageId: number;
+  formId: number;
+  formCode: string;
+}
+
+export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
+  columnsJSON,
+  reportData,
+  setReportData,
+  packageId,
+  formId,
+}) => {
+  const [tempList, setTempList] = useState<tempFile[]>([]);
+  const [selectTemp, setSelectTemp] = useState<string>("");
+  const [instance, setInstance] = useState<null | WebViewerInstance>(null);
+  const [fileLoading, setFileLoading] = useState<boolean>(true);
+
+  useEffect(() => {
+    updateReportTempList(packageId, formId, setTempList);
+  }, [packageId, formId]);
+
+  const onClose = async (value: boolean) => {
+    if (fileLoading) {
+      return;
+    }
+    if (!value) {
+      setTimeout(() => cleanupHtmlStyle(), 1000);
+      setReportData([]);
+    }
+  };
+
+  const downloadFileData = async () => {
+    if (instance) {
+      const { UI, Core } = instance;
+      const { documentViewer } = Core;
+
+      const doc = documentViewer.getDocument();
+      const fileName = doc.getFilename();
+      const fileData = await doc.getFileData({
+        includeAnnotations: true, // 사용자가 추가한 폼 필드 및 입력 포함
+        // officeOptions: {
+        //   outputFormat: "docx",
+        // },
+      });
+
+      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);
+
+      // 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);
+      // }
+    }
+  };
+
+  return (
+    <Dialog open={reportData.length > 0} onOpenChange={onClose}>
+      <DialogContent className="w-[70vw]" style={{ maxWidth: "none" }}>
+        <DialogHeader>
+          <DialogTitle>Report</DialogTitle>
+          <DialogDescription>
+            사용하시고자 하는 Report Template를 선택하여 주시기 바랍니다.
+          </DialogDescription>
+        </DialogHeader>
+        <div className="h-[60px]">
+          <Label>Report Template Select</Label>
+          <Select
+            value={selectTemp}
+            onValueChange={setSelectTemp}
+            disabled={instance === null}
+          >
+            <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 className="h-[calc(70vh-60px)]">
+          <ReportWebViewer
+            columnsJSON={columnsJSON}
+            reportTempPath={selectTemp}
+            reportDatas={reportData}
+            instance={instance}
+            setInstance={setInstance}
+            setFileLoading={setFileLoading}
+          />
+        </div>
+
+        <DialogFooter>
+          <Button onClick={downloadFileData} disabled={selectTemp.length === 0}>
+            다운로드
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+};
+
+interface ReportWebViewerProps {
+  columnsJSON: DataTableColumnJSON[];
+  reportTempPath: string;
+  reportDatas: ReportData[];
+  instance: null | WebViewerInstance;
+  setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>;
+  setFileLoading: Dispatch<SetStateAction<boolean>>;
+}
+
+const ReportWebViewer: FC<ReportWebViewerProps> = ({
+  columnsJSON,
+  reportTempPath,
+  reportDatas,
+  instance,
+  setInstance,
+  setFileLoading,
+}) => {
+  const [viwerLoading, setViewerLoading] = useState<boolean>(true);
+  const viewer = useRef<HTMLDivElement>(null);
+  const initialized = React.useRef(false);
+  const isCancelled = React.useRef(false); // 초기화 중단용 flag
+
+  useEffect(() => {
+    if (!initialized.current) {
+      initialized.current = true;
+      isCancelled.current = false; // 다시 열릴 때는 false로 리셋
+
+      requestAnimationFrame(() => {
+        if (viewer.current) {
+          import("@pdftron/webviewer").then(({ default: WebViewer }) => {
+            console.log(isCancelled.current);
+            if (isCancelled.current) {
+              console.log("📛 WebViewer 초기화 취소됨 (Dialog 닫힘)");
+
+              return;
+            }
+
+            WebViewer(
+              {
+                path: "/pdftronWeb",
+                licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
+                fullAPI: true,
+              },
+              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);
+            });
+          });
+        }
+      });
+    }
+
+    return () => {
+      // cleanup 시에는 중단 flag 세움
+      if (instance) {
+        instance.UI.dispose();
+      }
+      setTimeout(() => cleanupHtmlStyle(), 500);
+    };
+  }, []);
+
+  useEffect(() => {
+    importReportData(
+      columnsJSON,
+      instance,
+      reportDatas,
+      reportTempPath,
+      setFileLoading
+    );
+  }, [reportTempPath, reportDatas, instance, columnsJSON]);
+
+  return (
+    <div ref={viewer} className="h-[100%]">
+      {viwerLoading && (
+        <div className="flex flex-col items-center justify-center py-12">
+          <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+          <p className="text-sm text-muted-foreground">문서 뷰어 로딩 중...</p>
+        </div>
+      )}
+    </div>
+  );
+};
+
+const cleanupHtmlStyle = () => {
+  const htmlElement = document.documentElement;
+
+  // 기존 style 속성 가져오기
+  const originalStyle = htmlElement.getAttribute("style") || "";
+
+  // "color-scheme: light" 또는 "color-scheme: dark" 찾기
+  const colorSchemeStyle = originalStyle
+    .split(";")
+    .map((s) => s.trim())
+    .find((s) => s.startsWith("color-scheme:"));
+
+  // 새로운 스타일 적용 (color-scheme만 유지)
+  if (colorSchemeStyle) {
+    htmlElement.setAttribute("style", colorSchemeStyle + ";");
+  } else {
+    htmlElement.removeAttribute("style"); // color-scheme도 없으면 style 속성 자체 삭제
+  }
+
+  console.log("html style 삭제");
+};
+
+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) : "";
+  }
+};
+
+type ImportReportData = (
+  columnsJSON: DataTableColumnJSON[],
+  instance: null | WebViewerInstance,
+  reportDatas: ReportData[],
+  reportTempPath: string,
+  setFileLoading: Dispatch<SetStateAction<boolean>>
+) => void;
+
+const importReportData: ImportReportData = async (
+  columnsJSON,
+  instance,
+  reportDatas,
+  reportTempPath,
+  setFileLoading
+) => {
+  setFileLoading(true);
+  try {
+    if (instance && reportDatas.length > 0 && reportTempPath.length > 0) {
+      const { UI, Core } = instance;
+      const { documentViewer, createDocument } = Core;
+
+      const getFileData = await fetch(reportTempPath);
+      const reportFileBlob = await getFileData.blob();
+
+      const reportData = reportDatas[0];
+      const reportValue = stringifyAllValues(reportData);
+
+      const reportValueMapping: { [key: string]: any } = {};
+
+      columnsJSON.forEach((c) => {
+        const { key, label } = c;
+
+        const objKey = label.split(" ").join("_");
+
+        reportValueMapping[objKey] = reportValue[key];
+      });
+
+      const doc = await createDocument(reportFileBlob, {
+        extension: "docx",
+      });
+
+      await doc.applyTemplateValues(reportValueMapping);
+
+      documentViewer.loadDocument(doc, {
+        extension: "docx",
+        enableOfficeEditing: true,
+        officeOptions: {
+          formatOptions: {
+            applyPageBreaksToSheet: true,
+          },
+        },
+      });
+    }
+  } catch (err) {
+  } finally {
+    setFileLoading(false);
+  }
+};
+
+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,
+  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 };
+    })
+  );
+};
-- 
cgit v1.2.3


From 23db698279eb8ea5f73f678ce6deb93267c4705e Mon Sep 17 00:00:00 2001
From: rlaks5757 <rlaks5757@gmail.com>
Date: Thu, 27 Mar 2025 11:53:12 +0900
Subject: report batch download 개발 중
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .env.development                                   |   4 +-
 .env.production                                    |   4 +-
 .gitignore                                         |   1 +
 .../form-data/form-data-report-batch-dialog.tsx    | 307 ++++++++++++++
 components/form-data/form-data-report-dialog.tsx   |   2 +-
 components/form-data/form-data-table.tsx           |  23 ++
 db/migrations/meta/_journal.json                   |   2 +-
 package-lock.json                                  | 445 ++++++++++++++++++++-
 package.json                                       |   3 +
 pages/api/pdftron/createVendorDataReports.ts       |  36 ++
 10 files changed, 822 insertions(+), 5 deletions(-)
 create mode 100644 components/form-data/form-data-report-batch-dialog.tsx
 create mode 100644 pages/api/pdftron/createVendorDataReports.ts

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

diff --git a/.env.development b/.env.development
index 435884dd..183c6f93 100644
--- a/.env.development
+++ b/.env.development
@@ -10,7 +10,9 @@ NEXT_PUBLIC_MUI_KEY=da30586e1f20b93856a9783012fc9258Tz04ODI0MyxFPTE3NDQ0NTM2Nzgw
  NEXT_PUBLIC_URL=http://3.36.56.124:3000
  NEXT_PUBLIC_BASE_URL=http://3.36.56.124:3001
 
- NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY=demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd
+# PDFTRON KEYS
+NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY=demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd
+NEXT_PUBLIC_PDFTRON_SERVER_KEY=demo:1740034881027:6175a0fc0300000000f155d153480e5ba091f17922a109cbd7cf6e40b3
 
 # 기간계 시스템 연동 설정
 ERP_API_URL=https://erp.example.com/api/vendors
diff --git a/.env.production b/.env.production
index 997653c5..1ada78ec 100644
--- a/.env.production
+++ b/.env.production
@@ -10,7 +10,9 @@ NEXT_PUBLIC_MUI_KEY=da30586e1f20b93856a9783012fc9258Tz04ODI0MyxFPTE3NDQ0NTM2Nzgw
  NEXT_PUBLIC_URL=https://evcp.dtsolution.io
  NEXT_PUBLIC_BASE_URL=https://evcp.dtsolution.io
 
- NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY=demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd
+# PDFTRON KEYS
+NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY=demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd
+NEXT_PUBLIC_PDFTRON_SERVER_KEY=demo:1740034881027:6175a0fc0300000000f155d153480e5ba091f17922a109cbd7cf6e40b3
 
 # 기간계 시스템 연동 설정
 ERP_API_URL=https://erp.example.com/api/vendors
diff --git a/.gitignore b/.gitignore
index 7a6dfa0e..4aa665ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,6 +51,7 @@ next-env.d.ts
 /public/uploads
 /public/vendors
 /public/profiles
+/public/vendorFormData
 
 # 직접 참조가 불가능해 복사가 필요했던 라이브러리 (pdftrone)
 # node_modules/@pdftron/public 경로에서 core 및 ui 경로를 복사해 사용
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..e3fd7ea2
--- /dev/null
+++ b/components/form-data/form-data-report-batch-dialog.tsx
@@ -0,0 +1,307 @@
+"use client";
+
+import React, {
+  FC,
+  Dispatch,
+  SetStateAction,
+  useState,
+  useEffect,
+  useRef,
+} 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,
+  SelectGroup,
+  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 } 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 totalFiles = selectedFiles.length;
+      let successCount = 0;
+
+      for (let i = 0; i < totalFiles; i++) {
+        const file = selectedFiles[i];
+
+        const formData = new FormData();
+        formData.append("file", file);
+        formData.append("customFileName", file.name);
+
+        // await uploadReportTemp(packageId, formId, formData);
+
+        successCount++;
+      }
+    } catch (err) {
+      console.error(err);
+      toast({
+        title: "Error",
+        description: "파일 업로드 중 오류가 발생했습니다.",
+        variant: "destructive",
+      });
+    } finally {
+      setIsUploading(false);
+      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}
+            onClick={submitData}
+          >
+            다운로드
+          </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 };
+    })
+  );
+};
diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx
index 5ddc5e0c..deb0873b 100644
--- a/components/form-data/form-data-report-dialog.tsx
+++ b/components/form-data/form-data-report-dialog.tsx
@@ -348,7 +348,7 @@ const importReportData: ImportReportData = async (
 
         const objKey = label.split(" ").join("_");
 
-        reportValueMapping[objKey] = reportValue[key];
+        reportValueMapping[objKey] = reportValue?.[key] ?? "";
       });
 
       const doc = await createDocument(reportFileBlob, {
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index e3c5af8f..9feaf3b2 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -23,6 +23,7 @@ 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";
 
 interface GenericData {
   [key: string]: any;
@@ -57,6 +58,7 @@ export default function DynamicTable({
   const [isSaving, setIsSaving] = React.useState(false);
   const [tempUpDialog, setTempUpDialog] = React.useState(false);
   const [reportData, setReportData] = React.useState<GenericData[]>([]);
+  const [batchDownDialog, setBatchDownDialog] = React.useState(false);
 
   // Reference to the table instance
   const tableRef = React.useRef(null);
@@ -524,6 +526,13 @@ export default function DynamicTable({
         {/* 버튼 그룹 */}
         <div className="flex items-center gap-2">
           {/* 태그 불러오기 버튼 */}
+          <Button
+            variant="default"
+            size="sm"
+            onClick={() => setBatchDownDialog(true)}
+          >
+            Report Batch
+          </Button>
           <Button
             variant="default"
             size="sm"
@@ -621,6 +630,20 @@ export default function DynamicTable({
           formId={formId}
         />
       )}
+
+      {batchDownDialog && (
+        <FormDataReportBatchDialog
+          open={batchDownDialog}
+          setOpen={setBatchDownDialog}
+          columnsJSON={columnsJSON}
+          reportData={tableData}
+          packageId={contractItemId}
+          formCode={formCode}
+          formId={formId}
+        />
+      )}
     </>
   );
 }
+
+
diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json
index a84b0a28..f2725ef7 100644
--- a/db/migrations/meta/_journal.json
+++ b/db/migrations/meta/_journal.json
@@ -659,6 +659,6 @@
       "when": 1742834014376,
       "tag": "0093_young_the_hunter",
       "breakpoints": true
-    }
+    }    
   ]
 }
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index e0a4ffee..d41582f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
         "@mui/material": "^6.2.1",
         "@mui/x-data-grid-premium": "^7.23.3",
         "@mui/x-tree-view": "^7.23.6",
+        "@pdftron/pdfnet-node": "^11.3.0",
         "@pdftron/webviewer": "^11.3.0",
         "@radix-ui/primitive": "^1.1.1",
         "@radix-ui/react-accordion": "^1.2.2",
@@ -52,6 +53,7 @@
         "@t3-oss/env-nextjs": "^0.11.1",
         "@tanstack/react-table": "^8.20.6",
         "@types/docusign-esign": "^5.19.8",
+        "@types/formidable": "^3.4.5",
         "accept-language": "^3.0.20",
         "class-variance-authority": "^0.7.1",
         "clsx": "^2.1.1",
@@ -63,6 +65,7 @@
         "embla-carousel-react": "^8.5.1",
         "exceljs": "^4.4.0",
         "file-saver": "^2.0.5",
+        "formidable": "^3.5.2",
         "handlebars": "^4.7.8",
         "i18n-iso-countries": "^7.13.0",
         "i18next": "^24.1.2",
@@ -2122,6 +2125,60 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "node_modules/@mapbox/node-pre-gyp": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+      "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+      "dependencies": {
+        "detect-libc": "^2.0.0",
+        "https-proxy-agent": "^5.0.0",
+        "make-dir": "^3.1.0",
+        "node-fetch": "^2.6.7",
+        "nopt": "^5.0.0",
+        "npmlog": "^5.0.1",
+        "rimraf": "^3.0.2",
+        "semver": "^7.3.5",
+        "tar": "^6.1.11"
+      },
+      "bin": {
+        "node-pre-gyp": "bin/node-pre-gyp"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/@mui/core-downloads-tracker": {
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.1.tgz",
@@ -2741,6 +2798,20 @@
         "url": "https://github.com/sponsors/panva"
       }
     },
+    "node_modules/@pdftron/pdfnet-node": {
+      "version": "11.3.0",
+      "resolved": "https://registry.npmjs.org/@pdftron/pdfnet-node/-/pdfnet-node-11.3.0.tgz",
+      "integrity": "sha512-FhZD9Z6S/m6VUV23fFc2wtCOt50taV78rbM01+dotlb1FXu87el470EZ1SnRbdcCWegk16IfGeLIGKCuSn+oOw==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@mapbox/node-pre-gyp": "^1.0.10",
+        "underscore": "^1.13.6",
+        "xhr2": "^0.2.1"
+      },
+      "engines": {
+        "node": ">=8 <=22"
+      }
+    },
     "node_modules/@pdftron/webviewer": {
       "version": "11.3.0",
       "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-11.3.0.tgz",
@@ -4303,6 +4374,14 @@
       "integrity": "sha512-xrCYOdHh5zA3LUrn6CvspYwlzSWxPso11Lx32WnAG6KvLCRecKZ/Rh21PLXUkzUFsQmrGcx/traJAFjR6dVS5Q==",
       "license": "MIT"
     },
+    "node_modules/@types/formidable": {
+      "version": "3.4.5",
+      "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.5.tgz",
+      "integrity": "sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/hoist-non-react-statics": {
       "version": "3.3.6",
       "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
@@ -4706,6 +4785,11 @@
         "url": "https://opencollective.com/typescript-eslint"
       }
     },
+    "node_modules/abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+    },
     "node_modules/accept-language": {
       "version": "3.0.20",
       "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.20.tgz",
@@ -4751,6 +4835,17 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
     "node_modules/ajv": {
       "version": "6.12.6",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -4814,6 +4909,11 @@
         "node": ">= 8"
       }
     },
+    "node_modules/aproba": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+      "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
+    },
     "node_modules/archiver": {
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
@@ -4910,6 +5010,19 @@
         "safe-buffer": "~5.1.0"
       }
     },
+    "node_modules/are-we-there-yet": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+      "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^3.6.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/arg": {
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -5102,6 +5215,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/asap": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+      "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
+    },
     "node_modules/ast-types-flow": {
       "version": "0.0.8",
       "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@@ -5544,6 +5662,14 @@
         "node": ">= 6"
       }
     },
+    "node_modules/chownr": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/class-variance-authority": {
       "version": "0.7.1",
       "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -5630,6 +5756,14 @@
         "simple-swizzle": "^0.2.2"
       }
     },
+    "node_modules/color-support": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+      "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+      "bin": {
+        "color-support": "bin.js"
+      }
+    },
     "node_modules/colorette": {
       "version": "2.0.20",
       "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
@@ -5678,6 +5812,11 @@
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
       "license": "MIT"
     },
+    "node_modules/console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
+    },
     "node_modules/convert-source-map": {
       "version": "1.9.0",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
@@ -6099,12 +6238,16 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
+    },
     "node_modules/detect-libc": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
       "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
       "license": "Apache-2.0",
-      "optional": true,
       "engines": {
         "node": ">=8"
       }
@@ -6115,6 +6258,15 @@
       "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
       "license": "MIT"
     },
+    "node_modules/dezalgo": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+      "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+      "dependencies": {
+        "asap": "^2.0.0",
+        "wrappy": "1"
+      }
+    },
     "node_modules/diacritics": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
@@ -7456,12 +7608,47 @@
         "node": ">= 6"
       }
     },
+    "node_modules/formidable": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz",
+      "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==",
+      "dependencies": {
+        "dezalgo": "^1.0.4",
+        "hexoid": "^2.0.0",
+        "once": "^1.4.0"
+      },
+      "funding": {
+        "url": "https://ko-fi.com/tunnckoCore/commissions"
+      }
+    },
     "node_modules/fs-constants": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
       "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
       "license": "MIT"
     },
+    "node_modules/fs-minipass": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/fs-minipass/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -7537,6 +7724,68 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/gauge": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+      "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "aproba": "^1.0.3 || ^2.0.0",
+        "color-support": "^1.1.2",
+        "console-control-strings": "^1.0.0",
+        "has-unicode": "^2.0.1",
+        "object-assign": "^4.1.1",
+        "signal-exit": "^3.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "wide-align": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/gauge/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/gauge/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/gauge/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+    },
+    "node_modules/gauge/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/gauge/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/get-intrinsic": {
       "version": "1.2.6",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
@@ -7809,6 +8058,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
+    },
     "node_modules/hasown": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -7827,6 +8081,14 @@
       "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
       "license": "MIT"
     },
+    "node_modules/hexoid": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
+      "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/hoist-non-react-statics": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -7845,6 +8107,18 @@
         "void-elements": "3.1.0"
       }
     },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/i18n-iso-countries": {
       "version": "7.13.0",
       "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.13.0.tgz",
@@ -9020,6 +9294,28 @@
         "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
       }
     },
+    "node_modules/make-dir": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+      "dependencies": {
+        "semver": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/make-dir/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/make-error": {
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -9109,6 +9405,29 @@
         "node": ">=16 || 14 >=14.17"
       }
     },
+    "node_modules/minizlib": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "dependencies": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minizlib/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/mitt": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
@@ -9391,6 +9710,20 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/nopt": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+      "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+      "dependencies": {
+        "abbrev": "1"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -9400,6 +9733,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/npmlog": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+      "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+      "deprecated": "This package is no longer supported.",
+      "dependencies": {
+        "are-we-there-yet": "^2.0.0",
+        "console-control-strings": "^1.1.0",
+        "gauge": "^3.0.0",
+        "set-blocking": "^2.0.0"
+      }
+    },
     "node_modules/nuqs": {
       "version": "2.2.3",
       "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.2.3.tgz",
@@ -10970,6 +11315,11 @@
         "node": ">=10"
       }
     },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
+    },
     "node_modules/set-function-length": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -11663,6 +12013,22 @@
         "node": ">=6"
       }
     },
+    "node_modules/tar": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+      "dependencies": {
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "minipass": "^5.0.0",
+        "minizlib": "^2.1.1",
+        "mkdirp": "^1.0.3",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/tar-stream": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@@ -11679,6 +12045,25 @@
         "node": ">=6"
       }
     },
+    "node_modules/tar/node_modules/minipass": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+      "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tar/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/thenify": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -12434,6 +12819,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/underscore": {
+      "version": "1.13.7",
+      "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
+      "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
+    },
     "node_modules/undici-types": {
       "version": "6.19.8",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@@ -12742,6 +13132,51 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/wide-align": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+      "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+      "dependencies": {
+        "string-width": "^1.0.2 || 2 || 3 || 4"
+      }
+    },
+    "node_modules/wide-align/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wide-align/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/wide-align/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wide-align/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/word-wrap": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -12852,6 +13287,14 @@
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
       "license": "ISC"
     },
+    "node_modules/xhr2": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz",
+      "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/xmlchars": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
diff --git a/package.json b/package.json
index c5306242..af0efae7 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
     "@mui/material": "^6.2.1",
     "@mui/x-data-grid-premium": "^7.23.3",
     "@mui/x-tree-view": "^7.23.6",
+    "@pdftron/pdfnet-node": "^11.3.0",
     "@pdftron/webviewer": "^11.3.0",
     "@radix-ui/primitive": "^1.1.1",
     "@radix-ui/react-accordion": "^1.2.2",
@@ -54,6 +55,7 @@
     "@t3-oss/env-nextjs": "^0.11.1",
     "@tanstack/react-table": "^8.20.6",
     "@types/docusign-esign": "^5.19.8",
+    "@types/formidable": "^3.4.5",
     "accept-language": "^3.0.20",
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
@@ -65,6 +67,7 @@
     "embla-carousel-react": "^8.5.1",
     "exceljs": "^4.4.0",
     "file-saver": "^2.0.5",
+    "formidable": "^3.5.2",
     "handlebars": "^4.7.8",
     "i18n-iso-countries": "^7.13.0",
     "i18next": "^24.1.2",
diff --git a/pages/api/pdftron/createVendorDataReports.ts b/pages/api/pdftron/createVendorDataReports.ts
new file mode 100644
index 00000000..47f6055d
--- /dev/null
+++ b/pages/api/pdftron/createVendorDataReports.ts
@@ -0,0 +1,36 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import formidable from "formidable";
+
+export const config = {
+  api: {
+    bodyParser: false, // ✅ 이게 false면 안 됨!
+  },
+};
+
+export default async function handler(
+  req: NextApiRequest,
+  res: NextApiResponse
+) {
+  if (req.method !== "POST") {
+    return res.status(405).end();
+  }
+
+  const form = formidable({ multiples: true });
+
+  form.parse(req, async (err, fields, files) => {
+    if (err) {
+      console.error(err);
+      return res.status(500).json({ error: "Error parsing form" });
+    }
+
+    try {
+      const additionalData = JSON.parse((fields?.additionalData ?? "") as string);
+      console.log("📦 additionalData:", additionalData);
+      console.log("📎 files:", files.files); // files.files는 array or single file
+      
+      return res.status(200).json({ success: true });
+    } catch (e) {
+      return res.status(400).json({ error: "Invalid additionalData" });
+    }
+  });
+}
-- 
cgit v1.2.3

