From 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Tue, 25 Mar 2025 15:55:45 +0900
Subject: initial commit

---
 components/form-data/form-data-table-columns.tsx | 138 +++++++++++++++++++++++
 1 file changed, 138 insertions(+)
 create mode 100644 components/form-data/form-data-table-columns.tsx

(limited to 'components/form-data/form-data-table-columns.tsx')

diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx
new file mode 100644
index 00000000..d44616f8
--- /dev/null
+++ b/components/form-data/form-data-table-columns.tsx
@@ -0,0 +1,138 @@
+import type { ColumnDef, Row } from "@tanstack/react-table"
+import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header"
+import { Button } from "@/components/ui/button"
+import { Ellipsis } from "lucide-react"
+import { formatDate } from "@/lib/utils"
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuRadioGroup,
+  DropdownMenuRadioItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuSub,
+  DropdownMenuSubContent,
+  DropdownMenuSubTrigger,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+/** row 액션 관련 타입 */
+export interface DataTableRowAction<TData> {
+  row: Row<TData>
+  type: "open" | "edit" | "update"
+}
+
+/** 컬럼 타입 (필요에 따라 확장) */
+export type ColumnType = "STRING" | "NUMBER" | "LIST" 
+
+
+export interface DataTableColumnJSON {
+  key: string
+  /** 실제 Excel 등에서 구분용으로 쓰이는 label (고정) */
+  label: string
+
+  /** UI 표시용 label (예: 단위를 함께 표시) */
+  displayLabel?: string
+
+  type: ColumnType
+  options?: string[]
+  uom?: string
+}
+/** 
+ * getColumns 함수에 필요한 props 
+ * - TData: 테이블에 표시할 행(Row)의 타입
+ */
+interface GetColumnsProps<TData> {
+  columnsJSON: DataTableColumnJSON[]
+  setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<TData> | null>>
+}
+
+/**
+ * getColumns 함수
+ * 1) columnsJSON 배열을 순회하면서 accessorKey / header / cell 등을 설정
+ * 2) 마지막에 "Action" 칼럼(예: update 버튼) 추가
+ */
+export function getColumns<TData extends object>({
+  columnsJSON,
+  setRowAction,
+}: GetColumnsProps<TData>): ColumnDef<TData>[] {
+
+  // (1) 기본 컬럼들
+  const baseColumns: ColumnDef<TData>[] = columnsJSON.map((col) => ({
+    accessorKey: col.key,
+    header: ({ column }) => (
+      <ClientDataTableColumnHeaderSimple
+        column={column}
+        title={col.displayLabel || col.label}
+      />
+    ),
+
+    meta: {
+      excelHeader: col.label,
+      minWidth: 80,
+      paddingFactor: 1.2,
+      maxWidth: col.key ==="tagNumber"?120:150,
+    },
+    // (2) 실제 셀(cell) 렌더링: type에 따라 분기 가능
+    cell: ({ row }) => {
+      const cellValue = row.getValue(col.key)
+
+      // 데이터 타입별 처리
+      switch (col.type) {
+        case "NUMBER":
+          // 예: number인 경우 콤마 등 표시
+          return <div>{cellValue ? Number(cellValue).toLocaleString() : ""}</div>
+
+        // case "date":
+        //   // 예: 날짜 포맷팅
+        //   // 실제론 dayjs / date-fns 등으로 포맷
+        //   if (!cellValue) return <div></div>
+        //   const dateString = cellValue as string
+        //   if (!dateString) return null
+        //   return formatDate(new Date(dateString))
+
+        case "LIST":
+          // 예: select인 경우 label만 표시
+          return  <div>{String(cellValue ?? "")}</div>
+
+        case "STRING":
+        default:
+          return <div>{String(cellValue ?? "")}</div>
+      }
+    },
+  }))
+
+  // (3) 액션 칼럼 - update 버튼 예시
+  const actionColumn: ColumnDef<TData> = {
+    id: "update",
+    header: "",
+    cell: ({ row }) => (
+      <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button
+          aria-label="Open menu"
+          variant="ghost"
+          className="flex size-8 p-0 data-[state=open]:bg-muted"
+        >
+          <Ellipsis className="size-4" aria-hidden="true" />
+        </Button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align="end" className="w-40">
+        <DropdownMenuItem
+          onSelect={() => setRowAction({ row, type: "update" })}
+        >
+          Edit
+        </DropdownMenuItem>
+      </DropdownMenuContent>
+    </DropdownMenu>
+    ),
+    size:40,
+    meta:{
+      maxWidth:40
+    },
+    enablePinning: true,
+  }
+
+  // (4) 최종 반환
+  return [...baseColumns, actionColumn]
+}
\ No newline at end of file
-- 
cgit v1.2.3


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

---
 .env.development                                   |   2 +
 .env.production                                    |   2 +
 .../vendor-data/form/[packageId]/[formId]/page.tsx |  38 +-
 components/form-data/form-data-report-dialog.tsx   | 457 +++++++++++++++++
 .../form-data-report-temp-upload-dialog.tsx        | 305 ++++++++++++
 components/form-data/form-data-table-columns.tsx   | 112 +++--
 components/form-data/form-data-table.tsx           | 541 ++++++++++++---------
 components/form-data/update-form-sheet.tsx         | 137 +++---
 db/schema/vendorData.ts                            | 325 ++++++++-----
 lib/docuSign/docuSignFns.ts                        |   7 +-
 lib/forms/services.ts                              | 466 ++++++++++++------
 lib/po/service.ts                                  |  39 +-
 pages/api/po/sendDocuSign.ts                       |   8 +-
 pages/api/po/webhook.ts                            |  45 +-
 14 files changed, 1796 insertions(+), 688 deletions(-)
 create mode 100644 components/form-data/form-data-report-dialog.tsx
 create mode 100644 components/form-data/form-data-report-temp-upload-dialog.tsx

(limited to 'components/form-data/form-data-table-columns.tsx')

diff --git a/.env.development b/.env.development
index 203da32d..435884dd 100644
--- a/.env.development
+++ b/.env.development
@@ -10,6 +10,8 @@ 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
+
 # 기간계 시스템 연동 설정
 ERP_API_URL=https://erp.example.com/api/vendors
 ERP_API_KEY=your-erp-api-key
diff --git a/.env.production b/.env.production
index d2522400..997653c5 100644
--- a/.env.production
+++ b/.env.production
@@ -10,6 +10,8 @@ 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
+
 # 기간계 시스템 연동 설정
 ERP_API_URL=https://erp.example.com/api/vendors
 ERP_API_KEY=your-erp-api-key
diff --git a/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx b/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx
index 248bd7fc..01f5b501 100644
--- a/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx
+++ b/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/page.tsx
@@ -1,30 +1,35 @@
-import DynamicTable from "@/components/form-data/form-data-table"
-import { getFormData } from "@/lib/forms/services"
+import DynamicTable from "@/components/form-data/form-data-table";
+import { getFormData, getFormId } from "@/lib/forms/services";
 
 interface IndexPageProps {
   params: {
-    lng: string
-    packageId: string
-    formId: string
-  }
+    lng: string;
+    packageId: string;
+    formId: string;
+  };
 }
 
 export default async function FormPage({ params }: IndexPageProps) {
   // 1) 구조 분해 할당
-  const resolvedParams = await params
-  
+  const resolvedParams = await params;
+
   // 2) 구조 분해 할당
-  const { lng, packageId, formId } = resolvedParams
+  const { lng, packageId, formId: formCode } = resolvedParams;
 
   // 2) 변환
-  const packageIdAsNumber = Number(packageId)
+  const packageIdAsNumber = Number(packageId);
 
   // 3) DB 조회
-  const { columns, data } = await getFormData(formId, packageIdAsNumber)
+  const { columns, data } = await getFormData(formCode, packageIdAsNumber);
 
-  // 4) 예외 처리
+  // 4) formId 및 report temp file 조회
+  const { formId } = await getFormId(packageId, formCode);
+
+  // 5) 예외 처리
   if (!columns) {
-    return <p className="text-red-500">해당 폼의 메타 정보를 불러올 수 없습니다.</p>
+    return (
+      <p className="text-red-500">해당 폼의 메타 정보를 불러올 수 없습니다.</p>
+    );
   }
 
   // 5) 렌더링
@@ -32,10 +37,11 @@ export default async function FormPage({ params }: IndexPageProps) {
     <div className="space-y-6">
       <DynamicTable
         contractItemId={packageIdAsNumber}
-        formCode={formId}
+        formCode={formCode}
+        formId={formId}
         columnsJSON={columns}
         dataJSON={data}
       />
     </div>
-  )
-}
\ No newline at end of file
+  );
+}
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 };
+    })
+  );
+};
diff --git a/components/form-data/form-data-report-temp-upload-dialog.tsx b/components/form-data/form-data-report-temp-upload-dialog.tsx
new file mode 100644
index 00000000..b646c3e6
--- /dev/null
+++ b/components/form-data/form-data-report-temp-upload-dialog.tsx
@@ -0,0 +1,305 @@
+"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 { Button } from "@/components/ui/button";
+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 { getReportTempList, uploadReportTemp } from "@/lib/forms/services";
+import { VendorDataReportTemps } from "@/db/schema/vendorData";
+
+interface FormDataReportTempUploadDialogProps {
+  open: boolean;
+  setOpen: Dispatch<SetStateAction<boolean>>;
+  packageId: number;
+  formCode: string;
+  formId: number;
+  uploaderType: string;
+}
+
+// 최대 파일 크기 설정 (3000MB)
+const MAX_FILE_SIZE = 3000000;
+
+export const FormDataReportTempUploadDialog: FC<
+  FormDataReportTempUploadDialogProps
+> = ({ open, setOpen, packageId, formId, uploaderType }) => {
+  const { toast } = useToast();
+  const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
+  const [isUploading, setIsUploading] = useState(false);
+  const [uploadProgress, setUploadProgress] = useState(0);
+  const [prevReportTemp, setPrevReportTemp] = useState<VendorDataReportTemps[]>(
+    []
+  );
+
+  useEffect(() => {
+    updateReportTempList(packageId, formId, setPrevReportTemp);
+  }, [packageId, formId]);
+
+  // 드롭존 - 파일 드랍 처리
+  const handleDropAccepted = (acceptedFiles: File[]) => {
+    const newFiles = [...selectedFiles, ...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);
+    setUploadProgress(0);
+    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);
+        formData.append("uploaderType", uploaderType);
+
+        await uploadReportTemp(packageId, formId, formData);
+
+        successCount++;
+        setUploadProgress(Math.round((successCount / totalFiles) * 100));
+      }
+    } catch (err) {
+      console.error(err);
+      toast({
+        title: "Error",
+        description: "파일 업로드 중 오류가 발생했습니다.",
+        variant: "destructive",
+      });
+    } finally {
+      setIsUploading(false);
+      setUploadProgress(0);
+      updateReportTempList(packageId, formId, setPrevReportTemp);
+      setOpen(false);
+    }
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={setOpen}>
+      <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
+        <DialogHeader>
+          <DialogTitle>Report Template Upload</DialogTitle>
+          <DialogDescription>
+            사용하시고자 하는 Report Template를 업로드 하여주시기 바랍니다.
+          </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>
+          </>
+        )} */}
+
+        <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>
+              </DropzoneZone>
+              <Label className="text-xs text-muted-foreground">
+                여러 파일을 선택할 수 있습니다.
+              </Label>
+            </>
+          )}
+        </Dropzone>
+
+        {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>
+        )}
+
+        {isUploading && <UploadProgressBox uploadProgress={uploadProgress} />}
+        <DialogFooter>
+          <Button onClick={() => setOpen(false)}>취소</Button>
+          <Button disabled={selectedFiles.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>
+  );
+};
+
+const UploadProgressBox: FC<{ uploadProgress: number }> = ({
+  uploadProgress,
+}) => {
+  return (
+    <div className="flex flex-col gap-1 mt-2">
+      <div className="flex items-center gap-2">
+        <Loader2 className="h-4 w-4 animate-spin" />
+        <span className="text-sm">{uploadProgress}% 업로드 중...</span>
+      </div>
+      <div className="h-2 w-full bg-muted rounded-full overflow-hidden">
+        <div
+          className="h-full bg-primary rounded-full transition-all"
+          style={{ width: `${uploadProgress}%` }}
+        />
+      </div>
+    </div>
+  );
+};
+
+const generateFileName = (
+  packageId: number,
+  formId: number,
+  originalFileName: string,
+  index: number,
+  totalFiles: number
+) => {
+  // Get the file extension
+  const extension = originalFileName.split(".").pop() || "";
+
+  // Base name without extension
+  const baseName = `${packageId}_${formId}`;
+
+  // For multiple files, add a suffix
+  if (totalFiles > 1) {
+    return `${baseName}_${index + 1}.${extension}`;
+  }
+
+  // For a single file, no suffix needed
+  return `${baseName}.${extension}`;
+};
+
+type UpdateReportTempList = (
+  packageId: number,
+  formId: number,
+  setPrevReportTemp: Dispatch<SetStateAction<VendorDataReportTemps[]>>
+) => void;
+
+const updateReportTempList: UpdateReportTempList = async (
+  packageId,
+  formId,
+  setPrevReportTemp
+) => {
+  const tempList = await getReportTempList(packageId, formId);
+  setPrevReportTemp(tempList);
+};
diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx
index d44616f8..b23b2e70 100644
--- a/components/form-data/form-data-table-columns.tsx
+++ b/components/form-data/form-data-table-columns.tsx
@@ -1,8 +1,8 @@
-import type { ColumnDef, Row } from "@tanstack/react-table"
-import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header"
-import { Button } from "@/components/ui/button"
-import { Ellipsis } from "lucide-react"
-import { formatDate } from "@/lib/utils"
+import type { ColumnDef, Row } from "@tanstack/react-table";
+import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header";
+import { Button } from "@/components/ui/button";
+import { Ellipsis } from "lucide-react";
+import { formatDate } from "@/lib/utils";
 import {
   DropdownMenu,
   DropdownMenuContent,
@@ -15,36 +15,38 @@ import {
   DropdownMenuSubContent,
   DropdownMenuSubTrigger,
   DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
+} from "@/components/ui/dropdown-menu";
 /** row 액션 관련 타입 */
 export interface DataTableRowAction<TData> {
-  row: Row<TData>
-  type: "open" | "edit" | "update"
+  row: Row<TData>;
+  type: "open" | "edit" | "update";
 }
 
 /** 컬럼 타입 (필요에 따라 확장) */
-export type ColumnType = "STRING" | "NUMBER" | "LIST" 
-
+export type ColumnType = "STRING" | "NUMBER" | "LIST";
 
 export interface DataTableColumnJSON {
-  key: string
+  key: string;
   /** 실제 Excel 등에서 구분용으로 쓰이는 label (고정) */
-  label: string
+  label: string;
 
   /** UI 표시용 label (예: 단위를 함께 표시) */
-  displayLabel?: string
+  displayLabel?: string;
 
-  type: ColumnType
-  options?: string[]
-  uom?: string
+  type: ColumnType;
+  options?: string[];
+  uom?: string;
 }
-/** 
- * getColumns 함수에 필요한 props 
+/**
+ * getColumns 함수에 필요한 props
  * - TData: 테이블에 표시할 행(Row)의 타입
  */
 interface GetColumnsProps<TData> {
-  columnsJSON: DataTableColumnJSON[]
-  setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<TData> | null>>
+  columnsJSON: DataTableColumnJSON[];
+  setRowAction: React.Dispatch<
+    React.SetStateAction<DataTableRowAction<TData> | null>
+  >;
+  setReportData: React.Dispatch<React.SetStateAction<{ [key: string]: any }[]>>;
 }
 
 /**
@@ -55,8 +57,8 @@ interface GetColumnsProps<TData> {
 export function getColumns<TData extends object>({
   columnsJSON,
   setRowAction,
+  setReportData,
 }: GetColumnsProps<TData>): ColumnDef<TData>[] {
-
   // (1) 기본 컬럼들
   const baseColumns: ColumnDef<TData>[] = columnsJSON.map((col) => ({
     accessorKey: col.key,
@@ -71,17 +73,19 @@ export function getColumns<TData extends object>({
       excelHeader: col.label,
       minWidth: 80,
       paddingFactor: 1.2,
-      maxWidth: col.key ==="tagNumber"?120:150,
+      maxWidth: col.key === "tagNumber" ? 120 : 150,
     },
     // (2) 실제 셀(cell) 렌더링: type에 따라 분기 가능
     cell: ({ row }) => {
-      const cellValue = row.getValue(col.key)
+      const cellValue = row.getValue(col.key);
 
       // 데이터 타입별 처리
       switch (col.type) {
         case "NUMBER":
           // 예: number인 경우 콤마 등 표시
-          return <div>{cellValue ? Number(cellValue).toLocaleString() : ""}</div>
+          return (
+            <div>{cellValue ? Number(cellValue).toLocaleString() : ""}</div>
+          );
 
         // case "date":
         //   // 예: 날짜 포맷팅
@@ -93,14 +97,14 @@ export function getColumns<TData extends object>({
 
         case "LIST":
           // 예: select인 경우 label만 표시
-          return  <div>{String(cellValue ?? "")}</div>
+          return <div>{String(cellValue ?? "")}</div>;
 
         case "STRING":
         default:
-          return <div>{String(cellValue ?? "")}</div>
+          return <div>{String(cellValue ?? "")}</div>;
       }
     },
-  }))
+  }));
 
   // (3) 액션 칼럼 - update 버튼 예시
   const actionColumn: ColumnDef<TData> = {
@@ -108,31 +112,39 @@ export function getColumns<TData extends object>({
     header: "",
     cell: ({ row }) => (
       <DropdownMenu>
-      <DropdownMenuTrigger asChild>
-        <Button
-          aria-label="Open menu"
-          variant="ghost"
-          className="flex size-8 p-0 data-[state=open]:bg-muted"
-        >
-          <Ellipsis className="size-4" aria-hidden="true" />
-        </Button>
-      </DropdownMenuTrigger>
-      <DropdownMenuContent align="end" className="w-40">
-        <DropdownMenuItem
-          onSelect={() => setRowAction({ row, type: "update" })}
-        >
-          Edit
-        </DropdownMenuItem>
-      </DropdownMenuContent>
-    </DropdownMenu>
+        <DropdownMenuTrigger asChild>
+          <Button
+            aria-label="Open menu"
+            variant="ghost"
+            className="flex size-8 p-0 data-[state=open]:bg-muted"
+          >
+            <Ellipsis className="size-4" aria-hidden="true" />
+          </Button>
+        </DropdownMenuTrigger>
+        <DropdownMenuContent align="end" className="w-40">
+          <DropdownMenuItem
+            onSelect={() => setRowAction({ row, type: "update" })}
+          >
+            Edit
+          </DropdownMenuItem>
+          <DropdownMenuItem
+            onSelect={() => {
+              const { original } = row;
+              setReportData([original]);
+            }}
+          >
+            Create Report
+          </DropdownMenuItem>
+        </DropdownMenuContent>
+      </DropdownMenu>
     ),
-    size:40,
-    meta:{
-      maxWidth:40
+    size: 40,
+    meta: {
+      maxWidth: 40,
     },
     enablePinning: true,
-  }
+  };
 
   // (4) 최종 반환
-  return [...baseColumns, actionColumn]
-}
\ No newline at end of file
+  return [...baseColumns, actionColumn];
+}
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 14fff12e..e3c5af8f 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -1,36 +1,39 @@
-"use client"
+"use client";
 
-import * as React from "react"
-import { useParams } from "next/navigation"
-import { useTranslation } from "@/i18n/client"
+import * as React from "react";
+import { useParams } from "next/navigation";
+import { useTranslation } from "@/i18n/client";
 
-import { ClientDataTable } from "../client-data-table/data-table"
+import { ClientDataTable } from "../client-data-table/data-table";
 import {
   getColumns,
   DataTableRowAction,
   DataTableColumnJSON,
   ColumnType,
-} from "./form-data-table-columns"
+} 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 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 ExcelJS from "exceljs";
+import { saveAs } from "file-saver";
+import { FormDataReportTempUploadDialog } from "./form-data-report-temp-upload-dialog";
+import { FormDataReportDialog } from "./form-data-report-dialog";
 
 interface GenericData {
-  [key: string]: any
+  [key: string]: any;
 }
 
 export interface DynamicTableProps {
-  dataJSON: GenericData[]
-  columnsJSON: DataTableColumnJSON[]
-  contractItemId: number
-  formCode: string
+  dataJSON: GenericData[];
+  columnsJSON: DataTableColumnJSON[];
+  contractItemId: number;
+  formCode: string;
+  formId: number;
 }
 
 export default function DynamicTable({
@@ -38,437 +41,475 @@ export default function DynamicTable({
   columnsJSON,
   contractItemId,
   formCode,
+  formId,
 }: DynamicTableProps) {
-  const params = useParams()
-  const lng = (params?.lng as string) || "ko"
-  const { t } = useTranslation(lng, "translation")
-
-  const [rowAction, setRowAction] = React.useState<DataTableRowAction<GenericData> | null>(null)
-  const [tableData, setTableData] = React.useState<GenericData[]>(() => dataJSON)
-  const [isPending, setIsPending] = React.useState(false)
-  const [isSaving, setIsSaving] = React.useState(false)
+  console.log({ columnsJSON });
+  const params = useParams();
+  const lng = (params?.lng as string) || "ko";
+  const { t } = useTranslation(lng, "translation");
+
+  const [rowAction, setRowAction] =
+    React.useState<DataTableRowAction<GenericData> | null>(null);
+  const [tableData, setTableData] = React.useState<GenericData[]>(
+    () => dataJSON
+  );
+  const [isPending, setIsPending] = React.useState(false);
+  const [isSaving, setIsSaving] = React.useState(false);
+  const [tempUpDialog, setTempUpDialog] = React.useState(false);
+  const [reportData, setReportData] = React.useState<GenericData[]>([]);
 
   // Reference to the table instance
-  const tableRef = React.useRef(null)
+  const tableRef = React.useRef(null);
 
   const columns = React.useMemo(
-    () => getColumns<GenericData>({ columnsJSON, setRowAction }),
-    [columnsJSON, setRowAction]
-  )
+    () => getColumns<GenericData>({ columnsJSON, setRowAction, setReportData }),
+    [columnsJSON, setRowAction, setReportData]
+  );
 
   function mapColumnTypeToAdvancedFilterType(
     columnType: ColumnType
   ): DataTableAdvancedFilterField<GenericData>["type"] {
     switch (columnType) {
       case "STRING":
-        return "text"
-    case "NUMBER":
-        return "number"
+        return "text";
+      case "NUMBER":
+        return "number";
       case "LIST":
         // "select"로 하셔도 되고 "multi-select"로 하셔도 됩니다.
-        return "select"
+        return "select";
       // 그 외 다른 타입들도 적절히 추가 매핑
       default:
         // 예: 못 매핑한 경우 기본적으로 "text" 적용
-        return "text"
+        return "text";
     }
   }
 
-  const advancedFilterFields = React.useMemo<DataTableAdvancedFilterField<GenericData>[]>(
-    () => {
-      return columnsJSON.map((col) => ({
-        id: col.key,
-        label: col.label,
-        type: mapColumnTypeToAdvancedFilterType(col.type),
-        options:
-          col.type === "LIST"
-            ? col.options?.map((v) => ({ label: v, value: v }))
-            : undefined,
-      }))
-    },
-    [columnsJSON]
-  )
+  const advancedFilterFields = React.useMemo<
+    DataTableAdvancedFilterField<GenericData>[]
+  >(() => {
+    return columnsJSON.map((col) => ({
+      id: col.key,
+      label: col.label,
+      type: mapColumnTypeToAdvancedFilterType(col.type),
+      options:
+        col.type === "LIST"
+          ? col.options?.map((v) => ({ label: v, value: v }))
+          : undefined,
+    }));
+  }, [columnsJSON]);
 
   // 1) 태그 불러오기 (기존)
   async function handleSyncTags() {
     try {
-      setIsPending(true)
-      const result = await syncMissingTags(contractItemId, formCode)
-      
+      setIsPending(true);
+      const result = await syncMissingTags(contractItemId, formCode);
+
       // Prepare the toast messages based on what changed
-      const changes = []
-      if (result.createdCount > 0) changes.push(`${result.createdCount}건 태그 생성`)
-      if (result.updatedCount > 0) changes.push(`${result.updatedCount}건 태그 업데이트`)
-      if (result.deletedCount > 0) changes.push(`${result.deletedCount}건 태그 삭제`)
-      
+      const changes = [];
+      if (result.createdCount > 0)
+        changes.push(`${result.createdCount}건 태그 생성`);
+      if (result.updatedCount > 0)
+        changes.push(`${result.updatedCount}건 태그 업데이트`);
+      if (result.deletedCount > 0)
+        changes.push(`${result.deletedCount}건 태그 삭제`);
+
       if (changes.length > 0) {
         // If any changes were made, show success message and reload
-        toast.success(`동기화 완료: ${changes.join(', ')}`)
-        location.reload()
+        toast.success(`동기화 완료: ${changes.join(", ")}`);
+        location.reload();
       } else {
         // If no changes were made, show an info message
-        toast.info("변경사항이 없습니다. 모든 태그가 최신 상태입니다.")
+        toast.info("변경사항이 없습니다. 모든 태그가 최신 상태입니다.");
       }
     } catch (err) {
-      console.error(err)
-      toast.error("태그 동기화 중 에러가 발생했습니다.")
+      console.error(err);
+      toast.error("태그 동기화 중 에러가 발생했습니다.");
     } finally {
-      setIsPending(false)
+      setIsPending(false);
     }
   }
   // 2) Excel Import (새로운 기능)
   async function handleImportExcel(e: React.ChangeEvent<HTMLInputElement>) {
-    const file = e.target.files?.[0]
-    if (!file) return
+    const file = e.target.files?.[0];
+    if (!file) return;
 
     try {
-      setIsPending(true)
+      setIsPending(true);
 
       // 기존 테이블 데이터의 tagNumber 목록 (이미 있는 태그)
-      const existingTagNumbers = new Set(tableData.map((d) => d.tagNumber))
+      const existingTagNumbers = new Set(tableData.map((d) => d.tagNumber));
 
-      const workbook = new ExcelJS.Workbook()
-      const arrayBuffer = await file.arrayBuffer()
-      await workbook.xlsx.load(arrayBuffer)
+      const workbook = new ExcelJS.Workbook();
+      const arrayBuffer = await file.arrayBuffer();
+      await workbook.xlsx.load(arrayBuffer);
 
-      const worksheet = workbook.worksheets[0]
+      const worksheet = workbook.worksheets[0];
 
       // (A) 헤더 파싱 (Error 열 추가 전에 먼저 체크)
-      const headerRow = worksheet.getRow(1)
-      const headerRowValues = headerRow.values as ExcelJS.CellValue[]
+      const headerRow = worksheet.getRow(1);
+      const headerRowValues = headerRow.values as ExcelJS.CellValue[];
 
       // 디버깅용 로그
-      console.log("원본 헤더 값:", headerRowValues)
+      console.log("원본 헤더 값:", headerRowValues);
 
       // Excel의 헤더와 columnsJSON의 label 매핑 생성
       // Excel의 행은 1부터 시작하므로 headerRowValues[0]은 undefined
-      const headerToIndexMap = new Map<string, number>()
+      const headerToIndexMap = new Map<string, number>();
       for (let i = 1; i < headerRowValues.length; i++) {
-        const headerValue = String(headerRowValues[i] || "").trim()
+        const headerValue = String(headerRowValues[i] || "").trim();
         if (headerValue) {
-          headerToIndexMap.set(headerValue, i)
+          headerToIndexMap.set(headerValue, i);
         }
       }
 
       // (B) 헤더 검사
-      let headerErrorMessage = ""
+      let headerErrorMessage = "";
 
       // (1) "columnsJSON에 있는데 엑셀에 없는" 라벨
       columnsJSON.forEach((col) => {
-        const label = col.label
+        const label = col.label;
         if (!headerToIndexMap.has(label)) {
-          headerErrorMessage += `Column "${label}" is missing. `
+          headerErrorMessage += `Column "${label}" is missing. `;
         }
-      })
+      });
 
       // (2) "엑셀에는 있는데 columnsJSON에 없는" 라벨 검사
       headerToIndexMap.forEach((index, headerLabel) => {
-        const found = columnsJSON.some((col) => col.label === headerLabel)
+        const found = columnsJSON.some((col) => col.label === headerLabel);
         if (!found) {
-          headerErrorMessage += `Unexpected column "${headerLabel}" found in Excel. `
+          headerErrorMessage += `Unexpected column "${headerLabel}" found in Excel. `;
         }
-      })
+      });
 
       // (C) 이제 Error 열 추가
-      const lastColIndex = worksheet.columnCount + 1
-      worksheet.getRow(1).getCell(lastColIndex).value = "Error"
+      const lastColIndex = worksheet.columnCount + 1;
+      worksheet.getRow(1).getCell(lastColIndex).value = "Error";
 
       // 헤더 에러가 있으면 기록 후 다운로드하고 중단
       if (headerErrorMessage) {
-        headerRow.getCell(lastColIndex).value = headerErrorMessage.trim()
+        headerRow.getCell(lastColIndex).value = headerErrorMessage.trim();
 
-        const outBuffer = await workbook.xlsx.writeBuffer()
-        saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`)
+        const outBuffer = await workbook.xlsx.writeBuffer();
+        saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`);
 
-        toast.error(`Header mismatch found. Please check downloaded file.`)
-        return
+        toast.error(`Header mismatch found. Please check downloaded file.`);
+        return;
       }
 
       // -- 여기까지 왔다면, 헤더는 문제 없음 --
 
       // 컬럼 키-인덱스 매핑 생성 (실제 데이터 행 파싱용)
       // columnsJSON의 key와 Excel 열 인덱스 간의 매핑
-      const keyToIndexMap = new Map<string, number>()
+      const keyToIndexMap = new Map<string, number>();
       columnsJSON.forEach((col) => {
-        const index = headerToIndexMap.get(col.label)
+        const index = headerToIndexMap.get(col.label);
         if (index !== undefined) {
-          keyToIndexMap.set(col.key, index)
+          keyToIndexMap.set(col.key, index);
         }
-      })
+      });
 
       // 데이터 파싱
-      const importedData: GenericData[] = []
-      const lastRowNumber = worksheet.lastRow?.number || 1
-      let errorCount = 0
+      const importedData: GenericData[] = [];
+      const lastRowNumber = worksheet.lastRow?.number || 1;
+      let errorCount = 0;
 
       // 실제 데이터 행 파싱
       for (let rowNum = 2; rowNum <= lastRowNumber; rowNum++) {
-        const row = worksheet.getRow(rowNum)
-        const rowValues = row.values as ExcelJS.CellValue[]
-        if (!rowValues || rowValues.length <= 1) continue // 빈 행 스킵
+        const row = worksheet.getRow(rowNum);
+        const rowValues = row.values as ExcelJS.CellValue[];
+        if (!rowValues || rowValues.length <= 1) continue; // 빈 행 스킵
 
-        let errorMessage = ""
-        const rowObj: Record<string, any> = {}
+        let errorMessage = "";
+        const rowObj: Record<string, any> = {};
 
         // 각 열에 대해 처리
         columnsJSON.forEach((col) => {
-          const colIndex = keyToIndexMap.get(col.key)
-          if (colIndex === undefined) return
+          const colIndex = keyToIndexMap.get(col.key);
+          if (colIndex === undefined) return;
 
-          const cellValue = rowValues[colIndex] ?? ""
-          let stringVal = String(cellValue).trim()
+          const cellValue = rowValues[colIndex] ?? "";
+          let stringVal = String(cellValue).trim();
 
           // 타입별 검사
           switch (col.type) {
             case "STRING":
               if (!stringVal && col.key === "tagNumber") {
-                errorMessage += `[${col.label}] is empty. `
+                errorMessage += `[${col.label}] is empty. `;
               }
-              rowObj[col.key] = stringVal
-              break
+              rowObj[col.key] = stringVal;
+              break;
 
             case "NUMBER":
               if (stringVal) {
-                const num = parseFloat(stringVal)
+                const num = parseFloat(stringVal);
                 if (isNaN(num)) {
-                  errorMessage += `[${col.label}] '${stringVal}' is not a valid number. `
+                  errorMessage += `[${col.label}] '${stringVal}' is not a valid number. `;
                 } else {
-                  rowObj[col.key] = num
+                  rowObj[col.key] = num;
                 }
               } else {
-                rowObj[col.key] = null
+                rowObj[col.key] = null;
               }
-              break
+              break;
 
             case "LIST":
-              if (stringVal && col.options && !col.options.includes(stringVal)) {
-                errorMessage += `[${col.label}] '${stringVal}' not in ${col.options.join(", ")}. `
+              if (
+                stringVal &&
+                col.options &&
+                !col.options.includes(stringVal)
+              ) {
+                errorMessage += `[${
+                  col.label
+                }] '${stringVal}' not in ${col.options.join(", ")}. `;
               }
-              rowObj[col.key] = stringVal
-              break
+              rowObj[col.key] = stringVal;
+              break;
 
             default:
-              rowObj[col.key] = stringVal
-              break
+              rowObj[col.key] = stringVal;
+              break;
           }
-        })
+        });
 
         // tagNumber 검사
-        const tagNum = rowObj["tagNumber"]
+        const tagNum = rowObj["tagNumber"];
         if (!tagNum) {
-          errorMessage += `No tagNumber found. `
+          errorMessage += `No tagNumber found. `;
         } else if (!existingTagNumbers.has(tagNum)) {
-          errorMessage += `TagNumber '${tagNum}' is not in current data. `
+          errorMessage += `TagNumber '${tagNum}' is not in current data. `;
         }
 
         if (errorMessage) {
-          row.getCell(lastColIndex).value = errorMessage.trim()
-          errorCount++
+          row.getCell(lastColIndex).value = errorMessage.trim();
+          errorCount++;
         } else {
-          importedData.push(rowObj)
+          importedData.push(rowObj);
         }
       }
 
       // 에러가 있으면 재다운로드 후 import 중단
       if (errorCount > 0) {
-        const outBuffer = await workbook.xlsx.writeBuffer()
-        saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`)
-        toast.error(`There are ${errorCount} error row(s). Please check downloaded file.`)
-        return
+        const outBuffer = await workbook.xlsx.writeBuffer();
+        saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`);
+        toast.error(
+          `There are ${errorCount} error row(s). Please check downloaded file.`
+        );
+        return;
       }
 
       // 에러 없으니 tableData 병합
       setTableData((prev) => {
-        const newDataMap = new Map<string, GenericData>()
+        const newDataMap = new Map<string, GenericData>();
 
         // 기존 데이터를 맵에 추가
         prev.forEach((item) => {
           if (item.tagNumber) {
-            newDataMap.set(item.tagNumber, { ...item })
+            newDataMap.set(item.tagNumber, { ...item });
           }
-        })
+        });
 
         // 임포트 데이터로 기존 데이터 업데이트
         importedData.forEach((item) => {
-          const tag = item.tagNumber
-          if (!tag) return
-          const oldItem = newDataMap.get(tag) || {}
-          newDataMap.set(tag, { ...oldItem, ...item })
-        })
+          const tag = item.tagNumber;
+          if (!tag) return;
+          const oldItem = newDataMap.get(tag) || {};
+          newDataMap.set(tag, { ...oldItem, ...item });
+        });
 
-        return Array.from(newDataMap.values())
-      })
+        return Array.from(newDataMap.values());
+      });
 
-      toast.success(`Imported ${importedData.length} rows successfully.`)
+      toast.success(`Imported ${importedData.length} rows successfully.`);
     } catch (err) {
-      console.error("Excel import error:", err)
-      toast.error("Excel import failed.")
+      console.error("Excel import error:", err);
+      toast.error("Excel import failed.");
     } finally {
-      setIsPending(false)
-      e.target.value = ""
+      setIsPending(false);
+      e.target.value = "";
     }
   }
 
   // 3) Save -> 서버에 전체 tableData를 저장
   async function handleSave() {
     try {
-      setIsSaving(true)
-      
+      setIsSaving(true);
+
       // 유효성 검사
-      const invalidData = tableData.filter(item => !item.tagNumber?.trim())
+      const invalidData = tableData.filter((item) => !item.tagNumber?.trim());
       if (invalidData.length > 0) {
-        toast.error(`태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.`)
-        return
+        toast.error(
+          `태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.`
+        );
+        return;
       }
-      
+
       // 서버 액션 호출
-      const result = await updateFormDataInDB(formCode, contractItemId,  tableData)
-      
+      const result = await updateFormDataInDB(
+        formCode,
+        contractItemId,
+        tableData
+      );
+
       if (result.success) {
-        toast.success(result.message)
+        toast.success(result.message);
       } else {
-        toast.error(result.message)
+        toast.error(result.message);
       }
     } catch (err) {
-      console.error("Save error:", err)
-      toast.error("데이터 저장 중 오류가 발생했습니다.")
+      console.error("Save error:", err);
+      toast.error("데이터 저장 중 오류가 발생했습니다.");
     } finally {
-      setIsSaving(false)
+      setIsSaving(false);
     }
   }
 
   // 4) NEW: Excel Export with data validation for select columns using a hidden validation sheet
   async function handleExportExcel() {
     try {
-      setIsPending(true)
+      setIsPending(true);
 
       // Create a new workbook
-      const workbook = new ExcelJS.Workbook()
+      const workbook = new ExcelJS.Workbook();
 
       // 데이터 시트 생성
-      const worksheet = workbook.addWorksheet("Data")
+      const worksheet = workbook.addWorksheet("Data");
 
       // 유효성 검사용 숨김 시트 생성
-      const validationSheet = workbook.addWorksheet("ValidationData")
-      validationSheet.state = 'hidden' // 시트 숨김 처리
+      const validationSheet = workbook.addWorksheet("ValidationData");
+      validationSheet.state = "hidden"; // 시트 숨김 처리
 
       // 1. 유효성 검사 시트에 select 옵션 추가
-      const selectColumns = columnsJSON.filter(col =>
-        col.type === "LIST" && col.options && col.options.length > 0
-      )
+      const selectColumns = columnsJSON.filter(
+        (col) => col.type === "LIST" && col.options && col.options.length > 0
+      );
 
       // 유효성 검사 범위 저장 맵 (컬럼 키 -> 유효성 검사 범위)
-      const validationRanges = new Map<string, string>()
+      const validationRanges = new Map<string, string>();
 
       selectColumns.forEach((col, idx) => {
-        const colIndex = idx + 1
-        const colLetter = validationSheet.getColumn(colIndex).letter
+        const colIndex = idx + 1;
+        const colLetter = validationSheet.getColumn(colIndex).letter;
 
         // 헤더 추가 (컬럼 레이블)
-        validationSheet.getCell(`${colLetter}1`).value = col.label
+        validationSheet.getCell(`${colLetter}1`).value = col.label;
 
         // 옵션 추가
         if (col.options) {
           col.options.forEach((option, optIdx) => {
-            validationSheet.getCell(`${colLetter}${optIdx + 2}`).value = option
-          })
+            validationSheet.getCell(`${colLetter}${optIdx + 2}`).value = option;
+          });
 
           // 유효성 검사 범위 저장 (ValidationData!$A$2:$A$4 형식)
           validationRanges.set(
             col.key,
-            `ValidationData!${colLetter}$2:${colLetter}${col.options.length + 1}`
-          )
+            `ValidationData!${colLetter}$2:${colLetter}${
+              col.options.length + 1
+            }`
+          );
         }
-      })
+      });
 
       // 2. 데이터 시트에 헤더 추가
-      const headers = columnsJSON.map(col => col.label)
-      worksheet.addRow(headers)
+      const headers = columnsJSON.map((col) => col.label);
+      worksheet.addRow(headers);
 
       // 헤더 스타일 적용
-      const headerRow = worksheet.getRow(1)
-      headerRow.font = { bold: true }
-      headerRow.alignment = { horizontal: 'center' }
+      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' }
-        }
-      })
+          type: "pattern",
+          pattern: "solid",
+          fgColor: { argb: "FFCCCCCC" },
+        };
+      });
 
       // 3. 데이터 행 추가
-      tableData.forEach(row => {
-        const rowValues = columnsJSON.map(col => {
-          const value = row[col.key]
-          return value !== undefined && value !== null ? value : ''
-        })
-        worksheet.addRow(rowValues)
-      })
+      tableData.forEach((row) => {
+        const rowValues = columnsJSON.map((col) => {
+          const value = row[col.key];
+          return value !== undefined && value !== null ? value : "";
+        });
+        worksheet.addRow(rowValues);
+      });
 
       // 4. 데이터 유효성 검사 적용
-      const maxRows = 5000 // 데이터 유효성 검사를 적용할 최대 행 수
+      const maxRows = 5000; // 데이터 유효성 검사를 적용할 최대 행 수
 
       columnsJSON.forEach((col, idx) => {
         if (col.type === "LIST" && validationRanges.has(col.key)) {
-          const colLetter = worksheet.getColumn(idx + 1).letter
-          const validationRange = validationRanges.get(col.key)!
+          const colLetter = worksheet.getColumn(idx + 1).letter;
+          const validationRange = validationRanges.get(col.key)!;
 
           // 유효성 검사 정의
           const validation = {
-            type: 'list' as const,
+            type: "list" as const,
             allowBlank: true,
             formulae: [validationRange],
             showErrorMessage: true,
-            errorStyle: 'warning' as const,
-            errorTitle: '유효하지 않은 값',
-            error: '목록에서 값을 선택해주세요.'
-          }
+            errorStyle: "warning" as const,
+            errorTitle: "유효하지 않은 값",
+            error: "목록에서 값을 선택해주세요.",
+          };
 
           // 모든 데이터 행에 유효성 검사 적용 (최대 maxRows까지)
-          for (let rowIdx = 2; rowIdx <= Math.min(tableData.length + 1, maxRows); rowIdx++) {
-            worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation = validation
+          for (
+            let rowIdx = 2;
+            rowIdx <= Math.min(tableData.length + 1, maxRows);
+            rowIdx++
+          ) {
+            worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation =
+              validation;
           }
 
           // 빈 행에도 적용 (최대 maxRows까지)
           if (tableData.length + 1 < maxRows) {
-            for (let rowIdx = tableData.length + 2; rowIdx <= maxRows; rowIdx++) {
-              worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation = validation
+            for (
+              let rowIdx = tableData.length + 2;
+              rowIdx <= maxRows;
+              rowIdx++
+            ) {
+              worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation =
+                validation;
             }
           }
         }
-      })
+      });
 
       // 5. 컬럼 너비 자동 조정
       columnsJSON.forEach((col, idx) => {
-        const column = worksheet.getColumn(idx + 1)
+        const column = worksheet.getColumn(idx + 1);
 
         // 최적 너비 계산
-        let maxLength = col.label.length
-        tableData.forEach(row => {
-          const value = row[col.key]
+        let maxLength = col.label.length;
+        tableData.forEach((row) => {
+          const value = row[col.key];
           if (value !== undefined && value !== null) {
-            const valueLength = String(value).length
+            const valueLength = String(value).length;
             if (valueLength > maxLength) {
-              maxLength = valueLength
+              maxLength = valueLength;
             }
           }
-        })
+        });
 
         // 너비 설정 (최소 10, 최대 50)
-        column.width = Math.min(Math.max(maxLength + 2, 10), 50)
-      })
+        column.width = Math.min(Math.max(maxLength + 2, 10), 50);
+      });
 
       // 6. 파일 다운로드
-      const buffer = await workbook.xlsx.writeBuffer()
-      saveAs(new Blob([buffer]), `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx`)
+      const buffer = await workbook.xlsx.writeBuffer();
+      saveAs(
+        new Blob([buffer]),
+        `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx`
+      );
 
-      toast.success("Excel 내보내기 완료!")
+      toast.success("Excel 내보내기 완료!");
     } catch (err) {
-      console.error("Excel export error:", err)
-      toast.error("Excel 내보내기 실패.")
+      console.error("Excel export error:", err);
+      toast.error("Excel 내보내기 실패.");
     } finally {
-      setIsPending(false)
+      setIsPending(false);
     }
   }
 
@@ -478,13 +519,27 @@ export default function DynamicTable({
         data={tableData}
         columns={columns}
         advancedFilterFields={advancedFilterFields}
-      // tableRef={tableRef}
+        // tableRef={tableRef}
       >
         {/* 버튼 그룹 */}
         <div className="flex items-center gap-2">
           {/* 태그 불러오기 버튼 */}
-          <Button variant="default" size="sm" onClick={handleSyncTags} disabled={isPending}>
-            {isPending && <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />}
+          <Button
+            variant="default"
+            size="sm"
+            onClick={() => setTempUpDialog(true)}
+          >
+            Temp Upload
+          </Button>
+          <Button
+            variant="default"
+            size="sm"
+            onClick={handleSyncTags}
+            disabled={isPending}
+          >
+            {isPending && (
+              <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+            )}
             Sync Tags
           </Button>
 
@@ -503,7 +558,12 @@ export default function DynamicTable({
           </Button>
 
           {/* EXPORT 버튼 (새로 추가) */}
-          <Button variant="outline" size="sm" onClick={handleExportExcel} disabled={isPending}>
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={handleExportExcel}
+            disabled={isPending}
+          >
             <Download className="mr-2 size-4" />
             Export Template
           </Button>
@@ -533,13 +593,34 @@ export default function DynamicTable({
       <UpdateTagSheet
         open={rowAction?.type === "update"}
         onOpenChange={(open) => {
-          if (!open) setRowAction(null)
+          if (!open) setRowAction(null);
         }}
         columns={columnsJSON}
         rowData={rowAction?.row.original ?? null}
         formCode={formCode}
         contractItemId={contractItemId}
       />
+      {tempUpDialog && (
+        <FormDataReportTempUploadDialog
+          open={tempUpDialog}
+          setOpen={setTempUpDialog}
+          packageId={contractItemId}
+          formCode={formCode}
+          formId={formId}
+          uploaderType="vendor"
+        />
+      )}
+
+      {reportData.length > 0 && (
+        <FormDataReportDialog
+          columnsJSON={columnsJSON}
+          reportData={reportData}
+          setReportData={setReportData}
+          packageId={contractItemId}
+          formCode={formCode}
+          formId={formId}
+        />
+      )}
     </>
-  )
-}
\ No newline at end of file
+  );
+}
diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx
index d5f7d21b..c52b6833 100644
--- a/components/form-data/update-form-sheet.tsx
+++ b/components/form-data/update-form-sheet.tsx
@@ -1,11 +1,11 @@
-"use client"
+"use client";
 
-import * as React from "react"
-import { z } from "zod"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { Loader } from "lucide-react"
-import { toast } from "sonner"
+import * as React from "react";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { Loader } from "lucide-react";
+import { toast } from "sonner";
 
 import {
   Sheet,
@@ -15,9 +15,9 @@ import {
   SheetFooter,
   SheetHeader,
   SheetTitle,
-} from "@/components/ui/sheet"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
+} from "@/components/ui/sheet";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
 import {
   Form,
   FormField,
@@ -25,21 +25,28 @@ import {
   FormLabel,
   FormControl,
   FormMessage,
-} from "@/components/ui/form"
-import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"
-
-import { DataTableColumnJSON } from "./form-data-table-columns"
-import { updateFormDataInDB } from "@/lib/forms/services"
-
-interface UpdateTagSheetProps extends React.ComponentPropsWithoutRef<typeof Sheet> {
-  open: boolean
-  onOpenChange: (open: boolean) => void
-  columns: DataTableColumnJSON[]
-  rowData: Record<string, any> | null
-  formCode: string
-  contractItemId: number
+} from "@/components/ui/form";
+import {
+  Select,
+  SelectTrigger,
+  SelectContent,
+  SelectItem,
+  SelectValue,
+} from "@/components/ui/select";
+
+import { DataTableColumnJSON } from "./form-data-table-columns";
+import { updateFormDataInDB } from "@/lib/forms/services";
+
+interface UpdateTagSheetProps
+  extends React.ComponentPropsWithoutRef<typeof Sheet> {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  columns: DataTableColumnJSON[];
+  rowData: Record<string, any> | null;
+  formCode: string;
+  contractItemId: number;
   /** 업데이트 성공 시 호출될 콜백 */
-  onUpdateSuccess?: (updatedValues: Record<string, any>) => void
+  onUpdateSuccess?: (updatedValues: Record<string, any>) => void;
 }
 
 export function UpdateTagSheet({
@@ -52,57 +59,61 @@ export function UpdateTagSheet({
   onUpdateSuccess,
   ...props
 }: UpdateTagSheetProps) {
-  const [isPending, startTransition] = React.useTransition()
+  const [isPending, startTransition] = React.useTransition();
 
   // 1) zod 스키마
   const dynamicSchema = React.useMemo(() => {
-    const shape: Record<string, z.ZodType<any>> = {}
+    const shape: Record<string, z.ZodType<any>> = {};
     for (const col of columns) {
       if (col.type === "NUMBER") {
         shape[col.key] = z
           .union([z.coerce.number(), z.nan()])
           .transform((val) => (isNaN(val) ? undefined : val))
-          .optional()
+          .optional();
       } else {
-        shape[col.key] = z.string().optional()
+        shape[col.key] = z.string().optional();
       }
     }
-    return z.object(shape)
-  }, [columns])
+    return z.object(shape);
+  }, [columns]);
 
   // 2) form init
   const form = useForm({
     resolver: zodResolver(dynamicSchema),
     defaultValues: React.useMemo(() => {
-      if (!rowData) return {}
-      const defaults: Record<string, any> = {}
+      if (!rowData) return {};
+      const defaults: Record<string, any> = {};
       for (const col of columns) {
-        defaults[col.key] = rowData[col.key] ?? ""
+        defaults[col.key] = rowData[col.key] ?? "";
       }
-      return defaults
+      return defaults;
     }, [rowData, columns]),
-  })
+  });
 
   React.useEffect(() => {
     if (!rowData) {
-      form.reset({})
-      return
+      form.reset({});
+      return;
     }
-    const defaults: Record<string, any> = {}
+    const defaults: Record<string, any> = {};
     for (const col of columns) {
-      defaults[col.key] = rowData[col.key] ?? ""
+      defaults[col.key] = rowData[col.key] ?? "";
     }
-    form.reset(defaults)
-  }, [rowData, columns, form])
+    form.reset(defaults);
+  }, [rowData, columns, form]);
 
   async function onSubmit(values: Record<string, any>) {
     startTransition(async () => {
-      const { success, message } = await updateFormDataInDB(formCode, contractItemId, values)
+      const { success, message } = await updateFormDataInDB(
+        formCode,
+        contractItemId,
+        values
+      );
       if (!success) {
-        toast.error(message)
-        return
+        toast.error(message);
+        return;
       }
-      toast.success("Updated successfully!")
+      toast.success("Updated successfully!");
 
       // (A) 수정된 값(폼 데이터)을 부모 콜백에 전달
       onUpdateSuccess?.({
@@ -111,10 +122,10 @@ export function UpdateTagSheet({
         ...rowData,
         ...values,
         tagNumber: rowData?.tagNumber,
-      })
+      });
 
-      onOpenChange(false)
-    })
+      onOpenChange(false);
+    });
   }
 
   return (
@@ -128,11 +139,15 @@ export function UpdateTagSheet({
         </SheetHeader>
 
         <Form {...form}>
-          <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
+          <form
+            onSubmit={form.handleSubmit(onSubmit)}
+            className="flex flex-col gap-4"
+          >
             <div className="overflow-y-auto max-h-[80vh] flex-1 pr-4 -mr-4">
               <div className="flex flex-col gap-4 pt-2">
                 {columns.map((col) => {
-                  const isTagNumberField = col.key === "tagNumber" || col.key === "tagDescription"
+                  const isTagNumberField =
+                    col.key === "tagNumber" || col.key === "tagDescription";
                   return (
                     <FormField
                       key={col.key}
@@ -149,15 +164,15 @@ export function UpdateTagSheet({
                                     type="number"
                                     readOnly={isTagNumberField}
                                     onChange={(e) => {
-                                      const num = parseFloat(e.target.value)
-                                      field.onChange(isNaN(num) ? "" : num)
+                                      const num = parseFloat(e.target.value);
+                                      field.onChange(isNaN(num) ? "" : num);
                                     }}
                                     value={field.value ?? ""}
                                   />
                                 </FormControl>
                                 <FormMessage />
                               </FormItem>
-                            )
+                            );
 
                           case "LIST":
                             return (
@@ -181,7 +196,7 @@ export function UpdateTagSheet({
                                 </Select>
                                 <FormMessage />
                               </FormItem>
-                            )
+                            );
 
                           // case "date":
                           //   return (
@@ -205,17 +220,19 @@ export function UpdateTagSheet({
                               <FormItem>
                                 <FormLabel>{col.label}</FormLabel>
                                 <FormControl>
-                                  <Input readOnly={isTagNumberField} {...field} />
+                                  <Input
+                                    readOnly={isTagNumberField}
+                                    {...field}
+                                  />
                                 </FormControl>
                                 <FormMessage />
                               </FormItem>
-                            )
+                            );
                         }
                       }}
                     />
-                  )
+                  );
                 })}
-
               </div>
             </div>
 
@@ -235,5 +252,5 @@ export function UpdateTagSheet({
         </Form>
       </SheetContent>
     </Sheet>
-  )
-}
\ No newline at end of file
+  );
+}
diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts
index f7baa883..048e5ef8 100644
--- a/db/schema/vendorData.ts
+++ b/db/schema/vendorData.ts
@@ -3,32 +3,41 @@ import {
     text,
     varchar,
     timestamp,
-    integer, numeric, date, unique, serial, jsonb, uniqueIndex
-} from "drizzle-orm/pg-core"
-import { contractItems } from "./contract"
-
-export const forms = pgTable("forms", {
-    id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-    contractItemId: integer("contract_item_id")
+    integer,
+    numeric,
+    date,
+    unique,
+    serial,
+    jsonb,
+    uniqueIndex,
+  } from "drizzle-orm/pg-core";
+  import { contractItems } from "./contract";
+  
+  export const forms = pgTable(
+    "forms",
+    {
+      id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+      contractItemId: integer("contract_item_id")
         .notNull()
         .references(() => contractItems.id, { onDelete: "cascade" }),
-    formCode: varchar("form_code", { length: 100 }).notNull(),
-    formName: varchar("form_name", { length: 255 }).notNull(),
-    // tagType: varchar("tag_type", { length: 50 }).notNull(),
-    // class: varchar("class", { length: 100 }).notNull(),
-    createdAt: timestamp("created_at").defaultNow().notNull(),
-    updatedAt: timestamp("updated_at").defaultNow().notNull(),
-}, (table) => {
-    return {
+      formCode: varchar("form_code", { length: 100 }).notNull(),
+      formName: varchar("form_name", { length: 255 }).notNull(),
+      // tagType: varchar("tag_type", { length: 50 }).notNull(),
+      // class: varchar("class", { length: 100 }).notNull(),
+      createdAt: timestamp("created_at").defaultNow().notNull(),
+      updatedAt: timestamp("updated_at").defaultNow().notNull(),
+    },
+    (table) => {
+      return {
         // contractItemId와 formCode의 조합을 유니크하게 설정
-        contractItemFormCodeUnique: uniqueIndex("contract_item_form_code_unique").on(
-            table.contractItemId,
-            table.formCode
-        ),
+        contractItemFormCodeUnique: uniqueIndex(
+          "contract_item_form_code_unique"
+        ).on(table.contractItemId, table.formCode),
+      };
     }
-})
-
-export const rfqAttachments = pgTable("form_templates", {
+  );
+  
+  export const rfqAttachments = pgTable("form_templates", {
     id: serial("id").primaryKey(),
     formId: integer("form_id").references(() => forms.id),
     fileName: varchar("file_name", { length: 255 }).notNull(),
@@ -36,170 +45,220 @@ export const rfqAttachments = pgTable("form_templates", {
   
     createdAt: timestamp("created_at").defaultNow().notNull(),
     udpatedAt: timestamp("updated_at").defaultNow().notNull(),
+  });
   
-});
-
-
-export const formMetas = pgTable("form_metas", {
+  export const formMetas = pgTable("form_metas", {
     id: serial("id").primaryKey(),
     formCode: varchar("form_code", { length: 50 }).notNull(),
     formName: varchar("form_name", { length: 255 }).notNull(),
     columns: jsonb("columns").notNull(),
-    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
-    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-export const formEntries = pgTable("form_entries", {
+    createdAt: timestamp("created_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+    updatedAt: timestamp("updated_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+  });
+  
+  export const formEntries = pgTable("form_entries", {
     id: serial("id").primaryKey(),
     formCode: varchar("form_code", { length: 50 }).notNull(),
     data: jsonb("data").notNull(),
     contractItemId: integer("contract_item_id")
-        .notNull()
-        .references(() => contractItems.id, { onDelete: "cascade" }),
-    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
-    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-
-// ============ tags (각 계약 아이템에 대한 Tag) ============
-// "어느 계약의 어느 아이템에 대한 태그"임을 나타내려면 contract_items를 참조
-export const tags = pgTable("tags", {
+      .notNull()
+      .references(() => contractItems.id, { onDelete: "cascade" }),
+    createdAt: timestamp("created_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+    updatedAt: timestamp("updated_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+  });
+  
+  // ============ tags (각 계약 아이템에 대한 Tag) ============
+  // "어느 계약의 어느 아이템에 대한 태그"임을 나타내려면 contract_items를 참조
+  export const tags = pgTable("tags", {
     id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
+  
     // 이 Tag가 속한 "계약 내 아이템" (즉 contract_items.id)
     contractItemId: integer("contract_item_id")
-        .notNull()
-        .references(() => contractItems.id, { onDelete: "cascade" }),
-
-    formId: integer("form_id")
-        .references(() => forms.id, { onDelete: "set null" }),
-
+      .notNull()
+      .references(() => contractItems.id, { onDelete: "cascade" }),
+  
+    formId: integer("form_id").references(() => forms.id, {
+      onDelete: "set null",
+    }),
+  
     tagNo: varchar("tag_no", { length: 100 }).notNull(),
     tagType: varchar("tag_type", { length: 50 }).notNull(),
     class: varchar("class", { length: 100 }).notNull(),
     description: text("description"),
-
+  
     createdAt: timestamp("created_at").defaultNow().notNull(),
     updatedAt: timestamp("updated_at").defaultNow().notNull(),
-})
-
-export type Tag = typeof tags.$inferSelect
-export type Form = typeof forms.$inferSelect
-export type NewTag = typeof tags.$inferInsert
-
-export const tagTypes = pgTable("tag_types", {
+  });
+  
+  export type Tag = typeof tags.$inferSelect;
+  export type Form = typeof forms.$inferSelect;
+  export type NewTag = typeof tags.$inferInsert;
+  
+  export const tagTypes = pgTable("tag_types", {
     code: varchar("code", { length: 50 }).primaryKey(),
     description: text("description").notNull(),
-
-    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
-    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-export const tagSubfields = pgTable("tag_subfields", {
-    id: serial("id").primaryKey(),
-
-    // 외래키: tagTypeCode -> tagTypes.code
-    tagTypeCode: varchar("tag_type_code", { length: 50 })
+  
+    createdAt: timestamp("created_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+    updatedAt: timestamp("updated_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+  });
+  
+  export const tagSubfields = pgTable(
+    "tag_subfields",
+    {
+      id: serial("id").primaryKey(),
+  
+      // 외래키: tagTypeCode -> tagTypes.code
+      tagTypeCode: varchar("tag_type_code", { length: 50 })
         .notNull()
         .references(() => tagTypes.code, { onDelete: "cascade" }),
-
-    /**
-     * 나머지 필드
-     */
-    // tagTypeDescription: -> 이제 불필요. tagTypes.description로 join
-    attributesId: varchar("attributes_id", { length: 50 }).notNull(),
-    attributesDescription: text("attributes_description").notNull(),
-
-    expression: text("expression"),
-    delimiter: varchar("delimiter", { length: 10 }),
-
-    sortOrder: integer("sort_order").default(0).notNull(),
-
-    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
-    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-}, (table) => {
-    return {
+  
+      /**
+       * 나머지 필드
+       */
+      // tagTypeDescription: -> 이제 불필요. tagTypes.description로 join
+      attributesId: varchar("attributes_id", { length: 50 }).notNull(),
+      attributesDescription: text("attributes_description").notNull(),
+  
+      expression: text("expression"),
+      delimiter: varchar("delimiter", { length: 10 }),
+  
+      sortOrder: integer("sort_order").default(0).notNull(),
+  
+      createdAt: timestamp("created_at", { withTimezone: true })
+        .defaultNow()
+        .notNull(),
+      updatedAt: timestamp("updated_at", { withTimezone: true })
+        .defaultNow()
+        .notNull(),
+    },
+    (table) => {
+      return {
         uniqTagTypeAttribute: unique("uniq_tag_type_attribute").on(
-            table.tagTypeCode,
-            table.attributesId
+          table.tagTypeCode,
+          table.attributesId
         ),
-    };
-});
-
-export const tagSubfieldOptions = pgTable("tag_subfield_options", {
+      };
+    }
+  );
+  
+  export const tagSubfieldOptions = pgTable("tag_subfield_options", {
     id: serial("id").primaryKey(),
-
+  
     // 어떤 subfield에 속하는 옵션인지
     attributesId: varchar("attributes_id", { length: 50 })
-        .notNull()
-        .references(() => tagSubfields.attributesId, { onDelete: "cascade" }),
-
+      .notNull()
+      .references(() => tagSubfields.attributesId, { onDelete: "cascade" }),
+  
     /**
      * 실제 코드 (예: "PM", "AA", "VB", "VAR", "01", "02" ...)
      */
     code: varchar("code", { length: 50 }).notNull(),
-
+  
     /**
      * 사용자에게 보여줄 레이블 (예: "Pump", "Pneumatic Motor", "Ball Valve", ...)
      */
     label: text("label").notNull(),
-
+  
     /**
      * 생성/수정 시각 (선택)
      */
-    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
-    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-export const tagClasses = pgTable("tag_classes", {
+    createdAt: timestamp("created_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+    updatedAt: timestamp("updated_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+  });
+  
+  export const tagClasses = pgTable("tag_classes", {
     id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
+  
     // 기존 code/label
     code: varchar("code", { length: 100 }).notNull(),
     label: text("label").notNull(),
-
+  
     // 새 필드: tagTypeCode -> references tagTypes.code
     tagTypeCode: varchar("tag_type_code", { length: 50 })
-        .notNull()
-        .references(() => tagTypes.code, { onDelete: "cascade" }),
-
+      .notNull()
+      .references(() => tagTypes.code, { onDelete: "cascade" }),
+  
     createdAt: timestamp("created_at").defaultNow().notNull(),
     updatedAt: timestamp("updated_at").defaultNow().notNull(),
-})
-
-export const tagTypeClassFormMappings = pgTable("tag_type_class_form_mappings", {
-    id: serial("id").primaryKey(),
-
-    tagTypeLabel: varchar("tag_type_label", { length: 255 }).notNull(),
-    classLabel: varchar("class_label", { length: 255 }).notNull(),
-
-    formCode: varchar("form_code", { length: 50 }).notNull(),
-    formName: varchar("form_name", { length: 255 }).notNull(),
-
-    createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
-    updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
-})
-
-export type TagTypeClassFormMappings = typeof tagTypeClassFormMappings.$inferSelect
-export type TagSubfields = typeof tagSubfields.$inferSelect
-export type TagSubfieldOption = typeof tagSubfieldOptions.$inferSelect
-export type TagClasses = typeof tagClasses.$inferSelect
-
-
-export const viewTagSubfields = pgTable("view_tag_subfields", {
+  });
+  
+  export const tagTypeClassFormMappings = pgTable(
+    "tag_type_class_form_mappings",
+    {
+      id: serial("id").primaryKey(),
+  
+      tagTypeLabel: varchar("tag_type_label", { length: 255 }).notNull(),
+      classLabel: varchar("class_label", { length: 255 }).notNull(),
+  
+      formCode: varchar("form_code", { length: 50 }).notNull(),
+      formName: varchar("form_name", { length: 255 }).notNull(),
+  
+      createdAt: timestamp("created_at", { withTimezone: true })
+        .defaultNow()
+        .notNull(),
+      updatedAt: timestamp("updated_at", { withTimezone: true })
+        .defaultNow()
+        .notNull(),
+    }
+  );
+  
+  export type TagTypeClassFormMappings =
+    typeof tagTypeClassFormMappings.$inferSelect;
+  export type TagSubfields = typeof tagSubfields.$inferSelect;
+  export type TagSubfieldOption = typeof tagSubfieldOptions.$inferSelect;
+  export type TagClasses = typeof tagClasses.$inferSelect;
+  
+  export const viewTagSubfields = pgTable("view_tag_subfields", {
     id: integer("id").primaryKey(),
-
+  
     tagTypeCode: varchar("tag_type_code", { length: 50 }).notNull(),
     tagTypeDescription: text("tag_type_description"),
     attributesId: varchar("attributes_id", { length: 50 }).notNull(),
     attributesDescription: text("attributes_description").notNull(),
-
+  
     expression: text("expression"),
     delimiter: varchar("delimiter", { length: 10 }),
     sortOrder: integer("sort_order").default(0).notNull(),
-
+  
     createdAt: timestamp("created_at", { withTimezone: true }),
     updatedAt: timestamp("updated_at", { withTimezone: true }),
-})
-
-export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect
+  });
+  
+  export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect;
+  
+  export const vendorDataReportTemps = pgTable("vendor_data_report_temps", {
+    id: serial("id").primaryKey(),
+    contractItemId: integer("contract_item_id")
+      .notNull()
+      .references(() => contractItems.id, { onDelete: "cascade" }),
+    formId: integer("form_id")
+      .notNull()
+      .references(() => forms.id, { onDelete: "cascade" }),
+    fileName: varchar("file_name", { length: 255 }).notNull(),
+    filePath: varchar("file_path", { length: 1024 }).notNull(),
+    createdAt: timestamp("created_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+    updatedAt: timestamp("updated_at", { withTimezone: true })
+      .defaultNow()
+      .notNull(),
+  });
+  
+  export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect;
+  
\ No newline at end of file
diff --git a/lib/docuSign/docuSignFns.ts b/lib/docuSign/docuSignFns.ts
index 87977a0b..662ff23a 100644
--- a/lib/docuSign/docuSignFns.ts
+++ b/lib/docuSign/docuSignFns.ts
@@ -103,6 +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,
@@ -362,11 +363,11 @@ export async function getRecipients(
         result: false,
         message: "해당 Recipient id를 가진 서명자를 찾을 수 없습니다.",
       };
-    }    
+    }
 
-    const { autoRespondedReason, status } = signer;
+    const { autoRespondedReason } = signer;
 
-    if (autoRespondedReason || status === "status") {
+    if (autoRespondedReason) {
       return {
         result: false,
         message: autoRespondedReason,
diff --git a/lib/forms/services.ts b/lib/forms/services.ts
index e5fc8666..ff21626c 100644
--- a/lib/forms/services.ts
+++ b/lib/forms/services.ts
@@ -1,18 +1,29 @@
 // lib/forms/services.ts
-"use server"
+"use server";
 
+import path from "path";
+import fs from "fs/promises";
+import { v4 as uuidv4 } from "uuid";
 import db from "@/db/db";
-import { formEntries, formMetas, forms, tags, tagTypeClassFormMappings } from "@/db/schema/vendorData"
-import { eq, and, desc, sql, DrizzleError, or } from "drizzle-orm"
-import { unstable_cache } from "next/cache"
-import { revalidateTag } from "next/cache"
+import {
+  formEntries,
+  formMetas,
+  forms,
+  tags,
+  tagTypeClassFormMappings,
+  vendorDataReportTemps,
+  VendorDataReportTemps,
+} from "@/db/schema/vendorData";
+import { eq, and, desc, sql, DrizzleError, or } from "drizzle-orm";
+import { unstable_cache } from "next/cache";
+import { revalidateTag } from "next/cache";
 import { getErrorMessage } from "../handle-error";
 import { DataTableColumnJSON } from "@/components/form-data/form-data-table-columns";
 
 export interface FormInfo {
-  id: number
-  formCode: string
-  formName: string
+  id: number;
+  formCode: string;
+  formName: string;
   // tagType: string
 }
 
@@ -29,7 +40,9 @@ export async function getFormsByContractItemId(contractItemId: number | null) {
   try {
     return unstable_cache(
       async () => {
-        console.log(`[Forms Service] Fetching forms for contractItemId: ${contractItemId}`);
+        console.log(
+          `[Forms Service] Fetching forms for contractItemId: ${contractItemId}`
+        );
 
         try {
           // 데이터베이스에서 폼 조회
@@ -38,38 +51,48 @@ export async function getFormsByContractItemId(contractItemId: number | null) {
               id: forms.id,
               formCode: forms.formCode,
               formName: forms.formName,
-                // tagType: forms.tagType,
+              // tagType: forms.tagType,
             })
             .from(forms)
             .where(eq(forms.contractItemId, contractItemId));
 
-          console.log(`[Forms Service] Found ${formRecords.length} forms for contractItemId: ${contractItemId}`);
+          console.log(
+            `[Forms Service] Found ${formRecords.length} forms for contractItemId: ${contractItemId}`
+          );
 
           // 결과가 배열인지 확인
           if (!Array.isArray(formRecords)) {
-            getErrorMessage(`Unexpected result format for contractItemId ${contractItemId} ${formRecords}`);
+            getErrorMessage(
+              `Unexpected result format for contractItemId ${contractItemId} ${formRecords}`
+            );
             return { forms: [] };
           }
 
           return { forms: formRecords };
         } catch (error) {
-          getErrorMessage(`Database error for contractItemId ${contractItemId}: ${error}`);
+          getErrorMessage(
+            `Database error for contractItemId ${contractItemId}: ${error}`
+          );
           throw error; // 캐시 함수에서 에러를 던져 캐싱이 발생하지 않도록 함
         }
       },
       [cacheKey],
       {
         // 캐시 시간 단축
-        revalidate: 60, // 1분으로 줄임 
-        tags: [cacheKey]
+        revalidate: 60, // 1분으로 줄임
+        tags: [cacheKey],
       }
     )();
   } catch (error) {
-    getErrorMessage(`Cache operation failed for contractItemId ${contractItemId}: ${error}`);
+    getErrorMessage(
+      `Cache operation failed for contractItemId ${contractItemId}: ${error}`
+    );
 
     // 캐시 문제 시 직접 쿼리 시도
     try {
-      console.log(`[Forms Service] Fallback: Direct query for contractItemId: ${contractItemId}`);
+      console.log(
+        `[Forms Service] Fallback: Direct query for contractItemId: ${contractItemId}`
+      );
 
       const formRecords = await db
         .select({
@@ -83,7 +106,9 @@ export async function getFormsByContractItemId(contractItemId: number | null) {
 
       return { forms: formRecords };
     } catch (dbError) {
-      getErrorMessage(`Fallback query failed for contractItemId ${contractItemId}:${dbError}`);
+      getErrorMessage(
+        `Fallback query failed for contractItemId ${contractItemId}:${dbError}`
+      );
       return { forms: [] };
     }
   }
@@ -113,7 +138,7 @@ export async function revalidateForms(contractItemId: number) {
  */
 export async function getFormData(formCode: string, contractItemId: number) {
   // 고유 캐시 키 (formCode + contractItemId)
-  const cacheKey = `form-data-${formCode}-${contractItemId}`
+  const cacheKey = `form-data-${formCode}-${contractItemId}`;
 
   try {
     // 1) unstable_cache로 전체 로직을 감싼다
@@ -126,24 +151,29 @@ export async function getFormData(formCode: string, contractItemId: number) {
           .from(formMetas)
           .where(eq(formMetas.formCode, formCode))
           .orderBy(desc(formMetas.updatedAt))
-          .limit(1)
+          .limit(1);
 
-        const meta = metaRows[0] ?? null
+        const meta = metaRows[0] ?? null;
         if (!meta) {
-          return { columns: null, data: [] }
+          return { columns: null, data: [] };
         }
         // (2) form_entries에서 (formCode, contractItemId)에 해당하는 "가장 최신" 한 행
         const entryRows = await db
           .select()
           .from(formEntries)
-          .where(and(eq(formEntries.formCode, formCode), eq(formEntries.contractItemId, contractItemId)))
+          .where(
+            and(
+              eq(formEntries.formCode, formCode),
+              eq(formEntries.contractItemId, contractItemId)
+            )
+          )
           .orderBy(desc(formEntries.updatedAt))
-          .limit(1)
+          .limit(1);
 
-        const entry = entryRows[0] ?? null
+        const entry = entryRows[0] ?? null;
 
         // columns: DB에 저장된 JSON (DataTableColumnJSON[])
-        const columns = meta.columns as DataTableColumnJSON[]
+        const columns = meta.columns as DataTableColumnJSON[];
 
         columns.forEach((col) => {
           // 이미 displayLabel이 있으면 그대로 두고,
@@ -151,40 +181,44 @@ export async function getFormData(formCode: string, contractItemId: number) {
           // 둘 다 없으면 label만 쓴다.
           if (!col.displayLabel) {
             if (col.uom) {
-              col.displayLabel = `${col.label} (${col.uom})`
+              col.displayLabel = `${col.label} (${col.uom})`;
             } else {
-              col.displayLabel = col.label
+              col.displayLabel = col.label;
             }
           }
-        })
-        
+        });
+
         // data: 만약 entry가 없거나, data가 아닌 형태면 빈 배열
-        let data: Array<Record<string, any>> = []
+        let data: Array<Record<string, any>> = [];
         if (entry) {
           if (Array.isArray(entry.data)) {
-            data = entry.data
+            data = entry.data;
           } else {
-            console.warn("formEntries data was not an array. Using empty array.")
+            console.warn(
+              "formEntries data was not an array. Using empty array."
+            );
           }
         }
 
-        return { columns, data }
+        return { columns, data };
         // --- 기존 로직 끝 ---
       },
       [cacheKey], // 캐시 키 의존성
       {
-        revalidate: 60,    // 1분 캐시
-        tags: [cacheKey],  // 캐시 태그
+        revalidate: 60, // 1분 캐시
+        tags: [cacheKey], // 캐시 태그
       }
-    )()
+    )();
 
-    return result
+    return result;
   } catch (cacheError) {
-    console.error(`[getFormData] Cache operation failed:`, cacheError)
+    console.error(`[getFormData] Cache operation failed:`, cacheError);
 
     // --- fallback: 캐시 문제 시 직접 쿼리 시도 ---
     try {
-      console.log(`[getFormData] Fallback DB query for (${formCode}, ${contractItemId})`)
+      console.log(
+        `[getFormData] Fallback DB query for (${formCode}, ${contractItemId})`
+      );
 
       // (1) form_metas
       const metaRows = await db
@@ -192,24 +226,29 @@ export async function getFormData(formCode: string, contractItemId: number) {
         .from(formMetas)
         .where(eq(formMetas.formCode, formCode))
         .orderBy(desc(formMetas.updatedAt))
-        .limit(1)
+        .limit(1);
 
-      const meta = metaRows[0] ?? null
+      const meta = metaRows[0] ?? null;
       if (!meta) {
-        return { columns: null, data: [] }
+        return { columns: null, data: [] };
       }
 
       // (2) form_entries
       const entryRows = await db
         .select()
         .from(formEntries)
-        .where(and(eq(formEntries.formCode, formCode), eq(formEntries.contractItemId, contractItemId)))
+        .where(
+          and(
+            eq(formEntries.formCode, formCode),
+            eq(formEntries.contractItemId, contractItemId)
+          )
+        )
         .orderBy(desc(formEntries.updatedAt))
-        .limit(1)
+        .limit(1);
 
-      const entry = entryRows[0] ?? null
+      const entry = entryRows[0] ?? null;
 
-      const columns = meta.columns as DataTableColumnJSON[]
+      const columns = meta.columns as DataTableColumnJSON[];
 
       columns.forEach((col) => {
         // 이미 displayLabel이 있으면 그대로 두고,
@@ -217,33 +256,34 @@ export async function getFormData(formCode: string, contractItemId: number) {
         // 둘 다 없으면 label만 쓴다.
         if (!col.displayLabel) {
           if (col.uom) {
-            col.displayLabel = `${col.label} (${col.uom})`
+            col.displayLabel = `${col.label} (${col.uom})`;
           } else {
-            col.displayLabel = col.label
+            col.displayLabel = col.label;
           }
         }
-      })
+      });
 
-      let data: Array<Record<string, any>> = []
+      let data: Array<Record<string, any>> = [];
       if (entry) {
         if (Array.isArray(entry.data)) {
-          data = entry.data
+          data = entry.data;
         } else {
-          console.warn("formEntries data was not an array. Using empty array (fallback).")
+          console.warn(
+            "formEntries data was not an array. Using empty array (fallback)."
+          );
         }
       }
 
-      return { columns, data }
+      return { columns, data };
     } catch (dbError) {
-      console.error(`[getFormData] Fallback DB query failed:`, dbError)
-      return { columns: null, data: [] }
+      console.error(`[getFormData] Fallback DB query failed:`, dbError);
+      return { columns: null, data: [] };
     }
   }
 }
 
 // export async function syncMissingTags(contractItemId: number, formCode: string) {
 
-
 //   // (1) forms 테이블에서 (contractItemId, formCode) 찾기
 //   const [formRow] = await db
 //     .select()
@@ -320,50 +360,55 @@ export async function getFormData(formCode: string, contractItemId: number) {
 //       .where(eq(formEntries.id, entry.id))
 //   }
 
-
 //   revalidateTag(`form-data-${formCode}-${contractItemId}`);
 
 //   return { createdCount }
 // }
 
-export async function syncMissingTags(contractItemId: number, formCode: string) {
+export async function syncMissingTags(
+  contractItemId: number,
+  formCode: string
+) {
   // (1) Ensure there's a row in `forms` matching (contractItemId, formCode).
   const [formRow] = await db
     .select()
     .from(forms)
     .where(
-      and(eq(forms.contractItemId, contractItemId), eq(forms.formCode, formCode))
+      and(
+        eq(forms.contractItemId, contractItemId),
+        eq(forms.formCode, formCode)
+      )
     )
-    .limit(1)
+    .limit(1);
 
   if (!formRow) {
     throw new Error(
       `Form not found for contractItemId=${contractItemId}, formCode=${formCode}`
-    )
+    );
   }
 
   // (2) Get all mappings from `tagTypeClassFormMappings` for this formCode.
   const formMappings = await db
     .select()
     .from(tagTypeClassFormMappings)
-    .where(eq(tagTypeClassFormMappings.formCode, formCode))
+    .where(eq(tagTypeClassFormMappings.formCode, formCode));
 
   // If no mappings are found, there's nothing to sync.
   if (formMappings.length === 0) {
-    console.log(`No mappings found for formCode=${formCode}`)
-    return { createdCount: 0, updatedCount: 0, deletedCount: 0 }
+    console.log(`No mappings found for formCode=${formCode}`);
+    return { createdCount: 0, updatedCount: 0, deletedCount: 0 };
   }
 
   // Build a dynamic OR clause to match (tagType, class) pairs from the mappings.
   const orConditions = formMappings.map((m) =>
     and(eq(tags.tagType, m.tagTypeLabel), eq(tags.class, m.classLabel))
-  )
+  );
 
   // (3) Fetch all matching `tags` for the contractItemId + any of the (tagType, class) pairs.
   const tagRows = await db
     .select()
     .from(tags)
-    .where(and(eq(tags.contractItemId, contractItemId), or(...orConditions)))
+    .where(and(eq(tags.contractItemId, contractItemId), or(...orConditions)));
 
   // (4) Fetch (or create) a single `formEntries` row for (contractItemId, formCode).
   let [entry] = await db
@@ -375,7 +420,7 @@ export async function syncMissingTags(contractItemId: number, formCode: string)
         eq(formEntries.formCode, formCode)
       )
     )
-    .limit(1)
+    .limit(1);
 
   if (!entry) {
     const [inserted] = await db
@@ -385,64 +430,64 @@ export async function syncMissingTags(contractItemId: number, formCode: string)
         formCode,
         data: [], // Initialize with empty array
       })
-      .returning()
-    entry = inserted
+      .returning();
+    entry = inserted;
   }
 
   // entry.data는 [{ tagNumber: string, tagDescription?: string }, ...] 형태라고 가정
   const existingData = entry.data as Array<{
-    tagNumber: string
-    tagDescription?: string
-  }>
+    tagNumber: string;
+    tagDescription?: string;
+  }>;
 
   // Create a Set of valid tagNumbers from tagRows for efficient lookup
-  const validTagNumbers = new Set(tagRows.map(tag => tag.tagNo))
+  const validTagNumbers = new Set(tagRows.map((tag) => tag.tagNo));
 
   // Copy existing data to work with
   let updatedData: Array<{
-    tagNumber: string
-    tagDescription?: string
-  }> = []
-  
-  let createdCount = 0
-  let updatedCount = 0
-  let deletedCount = 0
+    tagNumber: string;
+    tagDescription?: string;
+  }> = [];
+
+  let createdCount = 0;
+  let updatedCount = 0;
+  let deletedCount = 0;
 
   // First, filter out items that should be deleted (not in validTagNumbers)
   for (const item of existingData) {
     if (validTagNumbers.has(item.tagNumber)) {
-      updatedData.push(item)
+      updatedData.push(item);
     } else {
-      deletedCount++
+      deletedCount++;
     }
   }
 
   // (5) For each tagRow, if it's missing in updatedData, push it in.
   //     이미 있는 경우에도 description이 달라지면 업데이트할 수 있음.
   for (const tagRow of tagRows) {
-    const { tagNo, description } = tagRow
-    
+    const { tagNo, description } = tagRow;
+
     // 5-1. 기존 데이터에서 tagNumber 매칭
     const existingIndex = updatedData.findIndex(
       (item) => item.tagNumber === tagNo
-    )
-    
+    );
+
     // 5-2. 없다면 새로 추가
     if (existingIndex === -1) {
       updatedData.push({
         tagNumber: tagNo,
         tagDescription: description ?? "",
-      })
-      createdCount++
+      });
+      createdCount++;
     } else {
       // 5-3. 이미 있으면, description이 다를 때만 업데이트(선택 사항)
-      const existingItem = updatedData[existingIndex]
+      const existingItem = updatedData[existingIndex];
       if (existingItem.tagDescription !== description) {
         updatedData[existingIndex] = {
           ...existingItem,
           tagDescription: description ?? "",
-        }
-        updatedCount++
+        };
+        updatedCount++;
       }
     }
   }
@@ -452,13 +497,13 @@ export async function syncMissingTags(contractItemId: number, formCode: string)
     await db
       .update(formEntries)
       .set({ data: updatedData })
-      .where(eq(formEntries.id, entry.id))
+      .where(eq(formEntries.id, entry.id));
   }
 
   // 캐시 무효화 등 후처리
-  revalidateTag(`form-data-${formCode}-${contractItemId}`)
+  revalidateTag(`form-data-${formCode}-${contractItemId}`);
 
-  return { createdCount, updatedCount, deletedCount }
+  return { createdCount, updatedCount, deletedCount };
 }
 
 /**
@@ -468,10 +513,10 @@ export async function syncMissingTags(contractItemId: number, formCode: string)
  * 업데이트 후, revalidateTag()로 캐시 무효화.
  */
 type UpdateResponse = {
-  success: boolean
-  message: string
-  data?: any
-}
+  success: boolean;
+  message: string;
+  data?: any;
+};
 
 export async function updateFormDataInDB(
   formCode: string,
@@ -480,12 +525,12 @@ export async function updateFormDataInDB(
 ): Promise<UpdateResponse> {
   try {
     // 1) tagNumber로 식별
-    const tagNumber = newData.tagNumber
+    const tagNumber = newData.tagNumber;
     if (!tagNumber) {
       return {
         success: false,
-        message: "tagNumber는 필수 항목입니다."
-      }
+        message: "tagNumber는 필수 항목입니다.",
+      };
     }
 
     // 2) row 찾기 (단 하나)
@@ -498,52 +543,52 @@ export async function updateFormDataInDB(
           eq(formEntries.contractItemId, contractItemId)
         )
       )
-      .limit(1)
+      .limit(1);
 
     if (!entries || entries.length === 0) {
       return {
         success: false,
-        message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, contractItemId=${contractItemId})`
-      }
+        message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, contractItemId=${contractItemId})`,
+      };
     }
 
-    const entry = entries[0]
+    const entry = entries[0];
 
     // 3) data가 배열인지 확인
     if (!entry.data) {
       return {
         success: false,
-        message: "폼 데이터가 없습니다."
-      }
+        message: "폼 데이터가 없습니다.",
+      };
     }
 
-    const dataArray = entry.data as Array<Record<string, any>>
+    const dataArray = entry.data as Array<Record<string, any>>;
     if (!Array.isArray(dataArray)) {
       return {
         success: false,
-        message: "폼 데이터가 올바른 형식이 아닙니다. 배열 형식이어야 합니다."
-      }
+        message: "폼 데이터가 올바른 형식이 아닙니다. 배열 형식이어야 합니다.",
+      };
     }
 
     // 4) tagNumber = newData.tagNumber 항목 찾기
-    const idx = dataArray.findIndex((item) => item.tagNumber === tagNumber)
+    const idx = dataArray.findIndex((item) => item.tagNumber === tagNumber);
     if (idx < 0) {
       return {
         success: false,
-        message: `태그 번호 "${tagNumber}"를 가진 항목을 찾을 수 없습니다.`
-      }
+        message: `태그 번호 "${tagNumber}"를 가진 항목을 찾을 수 없습니다.`,
+      };
     }
 
     // 5) 병합
-    const oldItem = dataArray[idx]
+    const oldItem = dataArray[idx];
     const updatedItem = {
       ...oldItem,
       ...newData,
       tagNumber: oldItem.tagNumber, // tagNumber 변경 불가 시 유지
-    }
+    };
 
-    const updatedArray = [...dataArray]
-    updatedArray[idx] = updatedItem
+    const updatedArray = [...dataArray];
+    updatedArray[idx] = updatedItem;
 
     // 6) DB UPDATE
     try {
@@ -551,67 +596,70 @@ export async function updateFormDataInDB(
         .update(formEntries)
         .set({
           data: updatedArray,
-          updatedAt: new Date() // 업데이트 시간도 갱신
+          updatedAt: new Date(), // 업데이트 시간도 갱신
         })
-        .where(eq(formEntries.id, entry.id))
+        .where(eq(formEntries.id, entry.id));
     } catch (dbError) {
-      console.error("Database update error:", dbError)
+      console.error("Database update error:", dbError);
 
       if (dbError instanceof DrizzleError) {
         return {
           success: false,
-          message: `데이터베이스 업데이트 오류: ${dbError.message}`
-        }
+          message: `데이터베이스 업데이트 오류: ${dbError.message}`,
+        };
       }
 
       return {
         success: false,
-        message: "데이터베이스 업데이트 중 오류가 발생했습니다."
-      }
+        message: "데이터베이스 업데이트 중 오류가 발생했습니다.",
+      };
     }
 
     // 7) Cache 무효화
     try {
       // 캐시 태그를 form-data-${formCode}-${contractItemId} 형태로 가정
-      const cacheTag = `form-data-${formCode}-${contractItemId}`
-      revalidateTag(cacheTag)
+      const cacheTag = `form-data-${formCode}-${contractItemId}`;
+      revalidateTag(cacheTag);
     } catch (cacheError) {
-      console.warn("Cache revalidation warning:", cacheError)
+      console.warn("Cache revalidation warning:", cacheError);
       // 캐시 무효화는 실패해도 업데이트 자체는 성공했으므로 경고만 로그로 남김
     }
 
     return {
       success: true,
-      message: '데이터가 성공적으로 업데이트되었습니다.',
+      message: "데이터가 성공적으로 업데이트되었습니다.",
       data: {
         tagNumber,
-        updatedFields: Object.keys(newData).filter(key => key !== 'tagNumber')
-      }
-    }
+        updatedFields: Object.keys(newData).filter(
+          (key) => key !== "tagNumber"
+        ),
+      },
+    };
   } catch (error) {
     // 예상치 못한 오류 처리
-    console.error("Unexpected error in updateFormDataInDB:", error)
+    console.error("Unexpected error in updateFormDataInDB:", error);
     return {
       success: false,
-      message: error instanceof Error
-        ? `예상치 못한 오류가 발생했습니다: ${error.message}`
-        : "알 수 없는 오류가 발생했습니다."
-    }
+      message:
+        error instanceof Error
+          ? `예상치 못한 오류가 발생했습니다: ${error.message}`
+          : "알 수 없는 오류가 발생했습니다.",
+    };
   }
 }
 
 // FormColumn Type (동일)
 export interface FormColumn {
-  key: string
-  type: string
-  label: string
-  options?: string[]
+  key: string;
+  type: string;
+  label: string;
+  options?: string[];
 }
 
 interface MetadataResult {
-  formName: string
-  formCode: string
-  columns: FormColumn[]
+  formName: string;
+  formCode: string;
+  columns: FormColumn[];
 }
 
 /**
@@ -620,26 +668,140 @@ interface MetadataResult {
  * { formName, formCode, columns } 형태로 반환.
  * 없으면 null.
  */
-export async function fetchFormMetadata(formCode: string): Promise<MetadataResult | null> {
+export async function fetchFormMetadata(
+  formCode: string
+): Promise<MetadataResult | null> {
   try {
     // 기존 방식: select().from().where()
     const rows = await db
       .select()
       .from(formMetas)
       .where(eq(formMetas.formCode, formCode))
-      .limit(1)
+      .limit(1);
 
     // rows는 배열
-    const metaData = rows[0]
-    if (!metaData) return null
+    const metaData = rows[0];
+    if (!metaData) return null;
 
     return {
       formCode: metaData.formCode,
       formName: metaData.formName,
-      columns: metaData.columns as FormColumn[]
+      columns: metaData.columns as FormColumn[],
+    };
+  } catch (err) {
+    console.error("Error in fetchFormMetadata:", err);
+    return null;
+  }
+}
+
+type GetReportFileList = (
+  packageId: string,
+  formCode: string
+) => Promise<{
+  formId: number;
+}>;
+
+export const getFormId: GetReportFileList = async (packageId, formCode) => {
+  const result: { formId: number } = {
+    formId: 0,
+  };
+  try {
+    const [targetForm] = await db
+      .select()
+      .from(forms)
+      .where(
+        and(
+          eq(forms.formCode, formCode),
+          eq(forms.contractItemId, Number(packageId))
+        )
+      );
+
+    if (!targetForm) {
+      throw new Error("Not Found Target Form");
     }
+
+    const { id: formId } = targetForm;
+
+    result.formId = formId;
   } catch (err) {
-    console.error("Error in fetchFormMetadata:", err)
-    return null
+  } finally {
+    return result;
   }
-}
\ No newline at end of file
+};
+
+type getReportTempList = (
+  packageId: number,
+  formId: number
+) => Promise<VendorDataReportTemps[]>;
+
+export const getReportTempList: getReportTempList = async (
+  packageId,
+  formId
+) => {
+  let result: VendorDataReportTemps[] = [];
+
+  try {
+    result = await db
+      .select()
+      .from(vendorDataReportTemps)
+      .where(
+        and(
+          eq(vendorDataReportTemps.contractItemId, packageId),
+          eq(vendorDataReportTemps.formId, formId)
+        )
+      );
+  } catch (err) {
+  } finally {
+    return result;
+  }
+};
+
+export async function uploadReportTemp(
+  packageId: number,
+  formId: number,
+  formData: FormData
+) {
+  const file = formData.get("file") as File | null;
+  const customFileName = formData.get("customFileName") as string;
+  const uploaderType = (formData.get("uploaderType") as string) || "vendor";
+
+  if (!["vendor", "client", "shi"].includes(uploaderType)) {
+    throw new Error(
+      `Invalid uploaderType: ${uploaderType}. Must be one of: vendor, client, shi`
+    );
+  }
+  if (file && file.size > 0) {
+    const originalName = customFileName;
+    const ext = path.extname(originalName);
+    const uniqueName = uuidv4() + ext;
+    const baseDir = path.join(
+      process.cwd(),
+      "public",
+      "vendorFormData",
+      packageId.toString(),
+      formId.toString()
+    );
+
+    const savePath = path.join(baseDir, uniqueName);
+
+    const arrayBuffer = await file.arrayBuffer();
+    const buffer = Buffer.from(arrayBuffer);
+
+    await fs.mkdir(baseDir, { recursive: true });
+
+    await fs.writeFile(savePath, buffer);
+
+    return db.transaction(async (tx) => {
+      // 파일 정보를 테이블에 저장
+      await tx
+        .insert(vendorDataReportTemps)
+        .values({
+          contractItemId: packageId,
+          formId: formId,
+          fileName: originalName,
+          filePath: `/vendorFormData/${packageId.toString()}/${formId.toString()}/${uniqueName}`,
+        })
+        .returning();
+    });
+  }
+}
diff --git a/lib/po/service.ts b/lib/po/service.ts
index dc398201..0653c11a 100644
--- a/lib/po/service.ts
+++ b/lib/po/service.ts
@@ -1,5 +1,6 @@
 "use server";
-
+import path from "path";
+import { v4 as uuidv4 } from "uuid";
 import { headers } from "next/headers";
 import db from "@/db/db";
 import { GetPOSchema } from "./validations";
@@ -323,13 +324,26 @@ Remarks:${contract.remarks}`,
 
       const { success: sendDocuSignResult, envelopeId } = sendDocuSign;
 
+      await tx
+        .update(contracts)
+        .set({
+          status: sendDocuSignResult ? "PENDING_SIGNATURE" : "Docu Sign Failed",
+        })
+        .where(eq(contracts.id, validatedData.contractId));
+
       if (!sendDocuSignResult) {
         return {
           success: false,
-          message: "DocuSign 전자 서명 발송에 실패하였습니다.",
+          message: "DocuSign Mail 발송에 실패하였습니다.",
         };
       }
 
+      // Update contract status to indicate pending signatures
+
+      const fileName = `${contractNo}-signature.pdf`;
+      const ext = path.extname(fileName);
+      const uniqueName = uuidv4() + ext;
+
       // Create a single envelope for all signers
       const [newEnvelope] = await tx
         .insert(contractEnvelopes)
@@ -338,7 +352,7 @@ Remarks:${contract.remarks}`,
           envelopeId: envelopeId,
           envelopeStatus: "sent",
           fileName: `${contractNo}-signature.pdf`, // Required field
-          filePath: `/contracts/${validatedData.contractId}/signatures`, // Required field
+          filePath: `/contracts/${validatedData.contractId}/signatures/${uniqueName}`, // Required field
           // Add any other required fields based on your schema
         })
         .returning();
@@ -368,25 +382,6 @@ Remarks:${contract.remarks}`,
         });
       }
 
-      // Update contract status to indicate pending signatures
-      await tx
-        .update(contracts)
-        .set({ status: "PENDING_SIGNATURE" })
-        .where(eq(contracts.id, validatedData.contractId));
-
-      // In a real implementation, you would send the envelope to DocuSign or similar service
-      // For example:
-      // const docusignResult = await docusignClient.createEnvelope({
-      //   recipients: validatedData.signers.map(signer => ({
-      //     email: signer.signerEmail,
-      //     name: signer.signerName,
-      //     recipientType: signer.signerType === "REQUESTER" ? "signer" : "cc",
-      //     routingOrder: signer.signerType === "REQUESTER" ? 1 : 2,
-      //   })),
-      //   documentId: `contract-${validatedData.contractId}`,
-      //   // other DocuSign-specific parameters
-      // });
-
       // Revalidate the path to refresh the data
       revalidatePath("/po");
 
diff --git a/pages/api/po/sendDocuSign.ts b/pages/api/po/sendDocuSign.ts
index ccb83733..eb218c2c 100644
--- a/pages/api/po/sendDocuSign.ts
+++ b/pages/api/po/sendDocuSign.ts
@@ -5,7 +5,7 @@ export const config = {
 };
 
 import type { NextApiRequest, NextApiResponse } from "next";
-import { requestContractSign } from "@/lib/docuSign/docuSignFns";
+import { requestContractSign, getRecipients } from "@/lib/docuSign/docuSignFns";
 
 export default async function handler(
   req: NextApiRequest,
@@ -36,11 +36,13 @@ export default async function handler(
 
     const { result, envelopeId, error } = docuSignStart;
 
-    res.status(200).json({
+    const sendResult = {
       success: result,
       envelopeId,
       message: error?.message,
-    });
+    };
+
+    res.status(200).json(sendResult);
   } catch (error: any) {
     res
       .status(500)
diff --git a/pages/api/po/webhook.ts b/pages/api/po/webhook.ts
index 50b3c1f4..4a9b6a29 100644
--- a/pages/api/po/webhook.ts
+++ b/pages/api/po/webhook.ts
@@ -3,15 +3,11 @@ export const config = {
     bodyParser: true, // ✅ 이게 false면 안 됨!
   },
 };
-
 import type { NextApiRequest, NextApiResponse } from "next";
 import path from "path";
 import fs from "fs";
 import * as z from "zod";
 import db from "@/db/db";
-import { GetPOSchema } from "@/lib/po/validations";
-import { unstable_cache } from "@/lib/unstable-cache";
-import { filterColumns } from "@/lib/filter-columns";
 import {
   asc,
   desc,
@@ -25,18 +21,15 @@ import {
   eq,
   count,
 } from "drizzle-orm";
-import { countPos, selectPos } from "@/lib/po/repository";
 import {
   contractEnvelopes,
-  contractsDetailView,
   contractSigners,
   contracts,
 } from "@/db/schema/contract";
-import { vendors, vendorContacts } from "@/db/schema/vendors";
-import dayjs from "dayjs";
-
-import { POContent } from "@/lib/docuSign/types";
-import { downloadContractFile, getRecipients } from "@/lib/docuSign/docuSignFns";
+import {
+  downloadContractFile,
+  getRecipients,
+} from "@/lib/docuSign/docuSignFns";
 
 export default async function handler(
   req: NextApiRequest,
@@ -52,8 +45,8 @@ export default async function handler(
     //recipientId === "1" 첫번째 서명자 서명 완료
     //recipientId === "2" 두번쨰 서명자 서명 완료
     const { envelopeId, recipientId } = data;
-    
-    console.log(req.body)
+
+    console.log(req.body);
 
     const contractList = [
       {
@@ -110,7 +103,7 @@ export default async function handler(
 
               await tx
                 .update(contracts)
-                .set({ status: `$FAILED_${safeRole}_SEND_MAIL(${message})` })
+                .set({ status: `FAILED_${safeRole}_SEND_MAIL(${message})` })
                 .where(eq(contracts.id, contractId));
 
               await tx
@@ -133,7 +126,7 @@ export default async function handler(
           //   continue;
           // }
 
-          // const fullFilePath = createFolderTree(fileName, filePath);
+          // const fullFilePath = createFolderTree(filePath);
 
           // fs.writeFileSync(fullFilePath, buffer);
         }
@@ -156,6 +149,10 @@ export default async function handler(
             .where(eq(dbSchma.envelopeId, envelopeId))
             .limit(1);
 
+          if (!targetContract) {
+            continue;
+          }
+
           const { id, contractId } = targetContract;
 
           if (contractId === null || contractId === undefined) {
@@ -212,6 +209,10 @@ export default async function handler(
             .where(eq(dbSchma.envelopeId, envelopeId))
             .limit(1);
 
+          if (!targetContract) {
+            continue;
+          }
+
           const { id, contractId, fileName, filePath } = targetContract;
 
           if (contractId === null || contractId === undefined) {
@@ -229,7 +230,7 @@ export default async function handler(
             continue;
           }
 
-          const fullFilePath = createFolderTree(fileName, filePath);
+          const fullFilePath = createFolderTree(filePath);
 
           fs.writeFileSync(fullFilePath, buffer);
 
@@ -279,6 +280,10 @@ export default async function handler(
             .where(eq(dbSchma.envelopeId, envelopeId))
             .limit(1);
 
+          if (!targetContract) {
+            continue;
+          }
+
           const { contractId, fileName, filePath } = targetContract;
 
           if (contractId === null || contractId === undefined) {
@@ -296,7 +301,7 @@ export default async function handler(
             continue;
           }
 
-          const fullFilePath = createFolderTree(fileName, filePath);
+          const fullFilePath = createFolderTree(filePath);
 
           fs.writeFileSync(fullFilePath, buffer);
 
@@ -324,8 +329,10 @@ export default async function handler(
   }
 }
 
-const createFolderTree = (fileName: string, filePath: string): string => {
-  const cleanedPath = filePath.replace(/^\/+/, "");
+const createFolderTree = (filePath: string): string => {
+  const fileName = path.basename(filePath);
+  const folderPath = path.dirname(filePath);
+  const cleanedPath = folderPath.replace(/^\/+/, "");
   const dirPath = path.resolve(process.cwd(), "public", cleanedPath); // 예: 'contracts/185/signatures'
   const fullFilePath = path.join(dirPath, fileName); // 예: 'contracts/185/signatures/xxx.pdf'
 
-- 
cgit v1.2.3

