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

---
 components/form-data/form-data-table.tsx | 564 ++++++++++++++++++-------------
 1 file changed, 334 insertions(+), 230 deletions(-)

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

diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 14fff12e..50c4f267 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -1,36 +1,40 @@
-"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";
+import { FormDataReportBatchDialog } from "./form-data-report-batch-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 +42,476 @@ 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[]>([]);
+  const [batchDownDialog, setBatchDownDialog] = React.useState(false);
 
   // 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 +521,34 @@ 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={() => setBatchDownDialog(true)}
+          >
+            Report Download
+          </Button>
+          <Button
+            variant="default"
+            size="sm"
+            onClick={() => setTempUpDialog(true)}
+          >
+            Template 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 +567,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 +602,48 @@ 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}
+        />
+      )}
+
+      {batchDownDialog && (
+        <FormDataReportBatchDialog
+          open={batchDownDialog}
+          setOpen={setBatchDownDialog}
+          columnsJSON={columnsJSON}
+          reportData={tableData}
+          packageId={contractItemId}
+          formCode={formCode}
+          formId={formId}
+        />
+      )}
     </>
-  )
-}
\ No newline at end of file
+  );
+}
+
+
-- 
cgit v1.2.3


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

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

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

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


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

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

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

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

