summaryrefslogtreecommitdiff
path: root/components/form-data/form-data-table.tsx
diff options
context:
space:
mode:
authorrlaks5757 <rlaks5757@gmail.com>2025-03-26 16:51:54 +0900
committerrlaks5757 <rlaks5757@gmail.com>2025-03-27 17:32:42 +0900
commit92ddb4f13d48cbf344dc2bf63df4457b3c713608 (patch)
tree38108e1ca08a86c1b36941d39acc47601529a14a /components/form-data/form-data-table.tsx
parent2ca4c91514feadb5edd0c9411670c7d9964d21e3 (diff)
feat: report batch download 기능 완료
Diffstat (limited to 'components/form-data/form-data-table.tsx')
-rw-r--r--components/form-data/form-data-table.tsx564
1 files changed, 334 insertions, 230 deletions
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
+ );
+}
+
+