summaryrefslogtreecommitdiff
path: root/components/form-data/form-data-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/form-data/form-data-table.tsx')
-rw-r--r--components/form-data/form-data-table.tsx948
1 files changed, 524 insertions, 424 deletions
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 4caee44f..05278375 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
-import { useParams } from "next/navigation";
+import { useParams, useRouter } from "next/navigation";
import { useTranslation } from "@/i18n/client";
import { ClientDataTable } from "../client-data-table/data-table";
@@ -13,20 +13,88 @@ import {
} 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 {
+ Download,
+ Loader,
+ Save,
+ Upload,
+ Plus,
+ Tag,
+ TagsIcon,
+ FileText,
+ FileSpreadsheet,
+ FileOutput,
+ Clipboard,
+ Send,
+ GitCompareIcon,
+ RefreshCcw
+} from "lucide-react";
import { toast } from "sonner";
-import { syncMissingTags, updateFormDataInDB } from "@/lib/forms/services";
+import {
+ getProjectCodeById,
+ getReportTempList,
+ sendFormDataToSEDP,
+ 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";
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ DropdownMenuSeparator,
+} from "@/components/ui/dropdown-menu";
+import { AddFormTagDialog } from "./add-formTag-dialog";
+import { importExcelData } from "./import-excel-form";
+import { exportExcelData } from "./export-excel-form";
+import { SEDPConfirmationDialog, SEDPStatusDialog } from "./sedp-components";
+import { SEDPCompareDialog } from "./sedp-compare-dialog";
+import { getSEDPToken } from "@/lib/sedp/sedp-token";
+
+
+async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise<any> {
+ try {
+ // Get the token
+ const apiKey = await getSEDPToken();
+
+ // Define the API base URL
+ const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api';
+
+ // Make the API call
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/Data/GetPubData`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ REG_TYPE_ID: formCode,
+ ContainDeleted: false
+ })
+ }
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`);
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error: any) {
+ console.error('Error calling SEDP API:', error);
+ throw new Error(`Failed to fetch data from SEDP API: ${error.message || 'Unknown error'}`);
+ }
+}
interface GenericData {
[key: string]: any;
@@ -38,6 +106,10 @@ export interface DynamicTableProps {
contractItemId: number;
formCode: string;
formId: number;
+ projectId: number;
+ formName?: string;
+ objectCode?: string;
+ mode: "IM" | "ENG"; // 모드 속성
}
export default function DynamicTable({
@@ -46,28 +118,98 @@ export default function DynamicTable({
contractItemId,
formCode,
formId,
+ projectId,
+ mode = "IM", // 기본값 설정
+ formName = `${formCode}`, // Default form name based on formCode
}: DynamicTableProps) {
const params = useParams();
+ const router = useRouter();
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 [tableData, setTableData] = React.useState<GenericData[]>(dataJSON);
+
+ // Update tableData when dataJSON changes
+ React.useEffect(() => {
+ setTableData(dataJSON);
+ }, [dataJSON]);
+
+ // 폴링 상태 관리를 위한 ref
+ const pollingRef = React.useRef<NodeJS.Timeout | null>(null);
+ const [syncId, setSyncId] = React.useState<string | null>(null);
+
+ // Separate loading states for different operations
+ const [isSyncingTags, setIsSyncingTags] = React.useState(false);
+ const [isImporting, setIsImporting] = React.useState(false);
+ const [isExporting, setIsExporting] = React.useState(false);
const [isSaving, setIsSaving] = React.useState(false);
+ const [isSendingSEDP, setIsSendingSEDP] = React.useState(false);
+ const [isLoadingTags, setIsLoadingTags] = React.useState(false);
+
+ // Any operation in progress
+ const isAnyOperationPending = isSyncingTags || isImporting || isExporting || isSaving || isSendingSEDP || isLoadingTags;
+
+ // SEDP dialogs state
+ const [sedpConfirmOpen, setSedpConfirmOpen] = React.useState(false);
+ const [sedpStatusOpen, setSedpStatusOpen] = React.useState(false);
+ const [sedpStatusData, setSedpStatusData] = React.useState({
+ status: 'success' as 'success' | 'error' | 'partial',
+ message: '',
+ successCount: 0,
+ errorCount: 0,
+ totalCount: 0
+ });
+
+ // SEDP compare dialog state
+ const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false);
+ const [projectCode, setProjectCode] = React.useState<string>('');
+
const [tempUpDialog, setTempUpDialog] = React.useState(false);
const [reportData, setReportData] = React.useState<GenericData[]>([]);
const [batchDownDialog, setBatchDownDialog] = React.useState(false);
+ const [tempCount, setTempCount] = React.useState(0);
+ const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false);
+
+ // Clean up polling on unmount
+ React.useEffect(() => {
+ return () => {
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ }
+ };
+ }, []);
+
+ React.useEffect(() => {
+ const getTempCount = async () => {
+ const tempList = await getReportTempList(contractItemId, formId);
+ setTempCount(tempList.length);
+ };
+
+ getTempCount();
+ }, [contractItemId, formId, tempUpDialog]);
+
+ // Get project code when component mounts
+ React.useEffect(() => {
+ const getProjectCode = async () => {
+ try {
+ const code = await getProjectCodeById(projectId);
+ setProjectCode(code);
+ } catch (error) {
+ console.error("Error fetching project code:", error);
+ toast.error("Failed to fetch project code");
+ }
+ };
- // Reference to the table instance
- const tableRef = React.useRef(null);
+ if (projectId) {
+ getProjectCode();
+ }
+ }, [projectId]);
const columns = React.useMemo(
- () => getColumns<GenericData>({ columnsJSON, setRowAction, setReportData }),
- [columnsJSON, setRowAction, setReportData]
+ () => getColumns<GenericData>({ columnsJSON, setRowAction, setReportData, tempCount }),
+ [columnsJSON, setRowAction, setReportData, tempCount]
);
function mapColumnTypeToAdvancedFilterType(
@@ -79,11 +221,8 @@ export default function DynamicTable({
case "NUMBER":
return "number";
case "LIST":
- // "select"로 하셔도 되고 "multi-select"로 하셔도 됩니다.
return "select";
- // 그 외 다른 타입들도 적절히 추가 매핑
default:
- // 예: 못 매핑한 경우 기본적으로 "text" 적용
return "text";
}
}
@@ -102,10 +241,10 @@ export default function DynamicTable({
}));
}, [columnsJSON]);
- // 1) 태그 불러오기 (기존)
+ // IM 모드: 태그 동기화 함수
async function handleSyncTags() {
try {
- setIsPending(true);
+ setIsSyncingTags(true);
const result = await syncMissingTags(contractItemId, formCode);
// Prepare the toast messages based on what changed
@@ -120,7 +259,7 @@ export default function DynamicTable({
if (changes.length > 0) {
// If any changes were made, show success message and reload
toast.success(`동기화 완료: ${changes.join(", ")}`);
- location.reload();
+ router.refresh(); // Use router.refresh instead of location.reload
} else {
// If no changes were made, show an info message
toast.info("변경사항이 없습니다. 모든 태그가 최신 상태입니다.");
@@ -129,487 +268,393 @@ export default function DynamicTable({
console.error(err);
toast.error("태그 동기화 중 에러가 발생했습니다.");
} finally {
- setIsPending(false);
+ setIsSyncingTags(false);
}
}
- // 2) Excel Import (새로운 기능)
- async function handleImportExcel(e: React.ChangeEvent<HTMLInputElement>) {
- const file = e.target.files?.[0];
- if (!file) return;
-
+
+ // ENG 모드: 태그 가져오기 함수
+ const handleGetTags = async () => {
try {
- setIsPending(true);
-
- // 기존 테이블 데이터의 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 worksheet = workbook.worksheets[0];
-
- // (A) 헤더 파싱 (Error 열 추가 전에 먼저 체크)
- const headerRow = worksheet.getRow(1);
- const headerRowValues = headerRow.values as ExcelJS.CellValue[];
-
- // 디버깅용 로그
- console.log("원본 헤더 값:", headerRowValues);
-
- // Excel의 헤더와 columnsJSON의 label 매핑 생성
- // Excel의 행은 1부터 시작하므로 headerRowValues[0]은 undefined
- const headerToIndexMap = new Map<string, number>();
- for (let i = 1; i < headerRowValues.length; i++) {
- const headerValue = String(headerRowValues[i] || "").trim();
- if (headerValue) {
- headerToIndexMap.set(headerValue, i);
- }
- }
-
- // (B) 헤더 검사
- let headerErrorMessage = "";
-
- // (1) "columnsJSON에 있는데 엑셀에 없는" 라벨
- columnsJSON.forEach((col) => {
- const label = col.label;
- if (!headerToIndexMap.has(label)) {
- headerErrorMessage += `Column "${label}" is missing. `;
- }
- });
-
- // (2) "엑셀에는 있는데 columnsJSON에 없는" 라벨 검사
- headerToIndexMap.forEach((index, headerLabel) => {
- const found = columnsJSON.some((col) => col.label === headerLabel);
- if (!found) {
- headerErrorMessage += `Unexpected column "${headerLabel}" found in Excel. `;
- }
+ setIsLoadingTags(true);
+
+ // API 엔드포인트 호출 - 작업 시작만 요청
+ const response = await fetch('/api/cron/form-tags/start', {
+ method: 'POST',
+ body: JSON.stringify({ projectCode ,formCode ,contractItemId })
});
-
- // (C) 이제 Error 열 추가
- const lastColIndex = worksheet.columnCount + 1;
- worksheet.getRow(1).getCell(lastColIndex).value = "Error";
-
- // 헤더 에러가 있으면 기록 후 다운로드하고 중단
- if (headerErrorMessage) {
- headerRow.getCell(lastColIndex).value = headerErrorMessage.trim();
-
- 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;
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to start tag import');
}
-
- // -- 여기까지 왔다면, 헤더는 문제 없음 --
-
- // 컬럼 키-인덱스 매핑 생성 (실제 데이터 행 파싱용)
- // columnsJSON의 key와 Excel 열 인덱스 간의 매핑
- const keyToIndexMap = new Map<string, number>();
- columnsJSON.forEach((col) => {
- const index = headerToIndexMap.get(col.label);
- if (index !== undefined) {
- keyToIndexMap.set(col.key, index);
+
+ const data = await response.json();
+
+ // 작업 ID 저장
+ if (data.syncId) {
+ setSyncId(data.syncId);
+ toast.info('Tag import started. This may take a while...');
+
+ // 상태 확인을 위한 폴링 시작
+ startPolling(data.syncId);
+ } else {
+ throw new Error('No import ID returned from server');
+ }
+ } catch (error) {
+ console.error('Error starting tag import:', error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : 'An error occurred while starting tag import'
+ );
+ setIsLoadingTags(false);
+ }
+ };
+
+ const startPolling = (id: string) => {
+ // 이전 폴링이 있다면 제거
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ }
+
+ // 5초마다 상태 확인
+ pollingRef.current = setInterval(async () => {
+ try {
+ const response = await fetch(`/api/cron/form-tags/status?id=${id}`);
+
+ if (!response.ok) {
+ throw new Error('Failed to get tag import status');
}
- });
-
- // 데이터 파싱
- 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; // 빈 행 스킵
-
- let errorMessage = "";
- const rowObj: Record<string, any> = {};
-
- // 각 열에 대해 처리
- columnsJSON.forEach((col) => {
- const colIndex = keyToIndexMap.get(col.key);
- if (colIndex === undefined) return;
-
- const cellValue = rowValues[colIndex] ?? "";
- let stringVal = String(cellValue).trim();
-
- // 타입별 검사
- switch (col.type) {
- case "STRING":
- if (!stringVal && col.key === "tagNumber") {
- errorMessage += `[${col.label}] is empty. `;
- }
- rowObj[col.key] = stringVal;
- break;
-
- case "NUMBER":
- if (stringVal) {
- const num = parseFloat(stringVal);
- if (isNaN(num)) {
- errorMessage += `[${col.label}] '${stringVal}' is not a valid number. `;
- } else {
- rowObj[col.key] = num;
- }
- } else {
- rowObj[col.key] = null;
- }
- break;
-
- case "LIST":
- if (
- stringVal &&
- col.options &&
- !col.options.includes(stringVal)
- ) {
- errorMessage += `[${
- col.label
- }] '${stringVal}' not in ${col.options.join(", ")}. `;
- }
- rowObj[col.key] = stringVal;
- break;
-
- default:
- rowObj[col.key] = stringVal;
- break;
+
+ const data = await response.json();
+
+ if (data.status === 'completed') {
+ // 폴링 중지
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+
+ router.refresh();
+
+ // 상태 초기화
+ setIsLoadingTags(false);
+ setSyncId(null);
+
+ // 성공 메시지 표시
+ toast.success(
+ `Tags imported successfully! ${data.result?.processedCount || 0} items processed.`
+ );
+
+ } else if (data.status === 'failed') {
+ // 에러 처리
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+
+ setIsLoadingTags(false);
+ setSyncId(null);
+ toast.error(data.error || 'Import failed');
+ } else if (data.status === 'processing') {
+ // 진행 상태 업데이트 (선택적)
+ if (data.progress) {
+ toast.info(`Import in progress: ${data.progress}%`, {
+ id: `import-progress-${id}`,
+ });
}
- });
-
- // tagNumber 검사
- const tagNum = rowObj["tagNumber"];
- if (!tagNum) {
- errorMessage += `No tagNumber found. `;
- } else if (!existingTagNumbers.has(tagNum)) {
- errorMessage += `TagNumber '${tagNum}' is not in current data. `;
- }
-
- if (errorMessage) {
- row.getCell(lastColIndex).value = errorMessage.trim();
- errorCount++;
- } else {
- importedData.push(rowObj);
}
+ } catch (error) {
+ console.error('Error checking importing status:', error);
}
+ }, 5000); // 5초마다 체크
+ };
+
+ // Excel Import - Modified to directly save to DB
+ async function handleImportExcel(e: React.ChangeEvent<HTMLInputElement>) {
+ const file = e.target.files?.[0];
+ if (!file) return;
- // 에러가 있으면 재다운로드 후 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;
- }
-
- // 에러 없으니 tableData 병합
- setTableData((prev) => {
- const newDataMap = new Map<string, GenericData>();
-
- // 기존 데이터를 맵에 추가
- prev.forEach((item) => {
- if (item.tagNumber) {
- 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 });
- });
-
- return Array.from(newDataMap.values());
+ try {
+ setIsImporting(true);
+
+ // Call the updated importExcelData function with direct save capability
+ const result = await importExcelData({
+ file,
+ tableData,
+ columnsJSON,
+ formCode, // Pass formCode for direct save
+ contractItemId, // Pass contractItemId for direct save
+ onPendingChange: setIsImporting,
+ onDataUpdate: (newData) => {
+ // This is called only after successful DB save
+ setTableData(Array.isArray(newData) ? newData : newData(tableData));
+ }
});
-
- toast.success(`Imported ${importedData.length} rows successfully.`);
- } catch (err) {
- console.error("Excel import error:", err);
- toast.error("Excel import failed.");
+
+ // If import and save was successful, refresh the page
+ if (result.success) {
+ router.refresh();
+ }
+ } catch (error) {
+ console.error("Import failed:", error);
+ toast.error("Failed to import Excel data");
} finally {
- setIsPending(false);
+ // Always clear the file input value
e.target.value = "";
+ setIsImporting(false);
+ }
+ }
+
+ // SEDP Send handler (with confirmation)
+ function handleSEDPSendClick() {
+ if (tableData.length === 0) {
+ toast.error("No data to send to SEDP");
+ return;
}
+
+ // Open confirmation dialog
+ setSedpConfirmOpen(true);
+ }
+
+ // Handle SEDP compare button click
+ function handleSEDPCompareClick() {
+ if (tableData.length === 0) {
+ toast.error("No data to compare with SEDP");
+ return;
+ }
+
+ if (!projectCode) {
+ toast.error("Project code is not available");
+ return;
+ }
+
+ // Open compare dialog
+ setSedpCompareOpen(true);
}
- // 3) Save -> 서버에 전체 tableData를 저장
- async function handleSave() {
+ // Actual SEDP send after confirmation
+ async function handleSEDPSendConfirmed() {
try {
- setIsSaving(true);
-
- // 유효성 검사
- const invalidData = tableData.filter((item) => !item.tagNumber?.trim());
+ setIsSendingSEDP(true);
+
+ // Validate data
+ const invalidData = tableData.filter((item) => !item.TAG_NO?.trim());
if (invalidData.length > 0) {
- toast.error(
- `태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.`
- );
+ toast.error(`태그 번호가 없는 항목이 ${invalidData.length}개 있습니다.`);
+ setSedpConfirmOpen(false);
return;
}
- // 서버 액션 호출
- const result = await updateFormDataInDB(
- formCode,
- contractItemId,
- tableData
+ // Then send to SEDP - pass formCode instead of formName
+ const sedpResult = await sendFormDataToSEDP(
+ formCode, // Send formCode instead of formName
+ projectId, // Project ID
+ tableData, // Table data
+ columnsJSON // Column definitions
);
- if (result.success) {
- toast.success(result.message);
+ // Close confirmation dialog
+ setSedpConfirmOpen(false);
+
+ // Set status data based on result
+ if (sedpResult.success) {
+ setSedpStatusData({
+ status: 'success',
+ message: "Data successfully sent to SEDP",
+ successCount: tableData.length,
+ errorCount: 0,
+ totalCount: tableData.length
+ });
} else {
- toast.error(result.message);
+ setSedpStatusData({
+ status: 'error',
+ message: sedpResult.message || "Failed to send data to SEDP",
+ successCount: 0,
+ errorCount: tableData.length,
+ totalCount: tableData.length
+ });
}
- } catch (err) {
- console.error("Save error:", err);
- toast.error("데이터 저장 중 오류가 발생했습니다.");
+
+ // Open status dialog to show result
+ setSedpStatusOpen(true);
+
+ // Refresh the route to get fresh data
+ router.refresh();
+
+ } catch (err: any) {
+ console.error("SEDP error:", err);
+
+ // Set error status
+ setSedpStatusData({
+ status: 'error',
+ message: err.message || "An unexpected error occurred",
+ successCount: 0,
+ errorCount: tableData.length,
+ totalCount: tableData.length
+ });
+
+ // Close confirmation and open status
+ setSedpConfirmOpen(false);
+ setSedpStatusOpen(true);
+
} finally {
- setIsSaving(false);
+ setIsSendingSEDP(false);
}
}
-
- // 4) NEW: Excel Export with data validation for select columns using a hidden validation sheet
+
+ // Template Export
async function handleExportExcel() {
try {
- setIsPending(true);
-
- // Create a new workbook
- const workbook = new ExcelJS.Workbook();
-
- // 데이터 시트 생성
- const worksheet = workbook.addWorksheet("Data");
-
- // 유효성 검사용 숨김 시트 생성
- 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 validationRanges = new Map<string, string>();
-
- selectColumns.forEach((col, idx) => {
- const colIndex = idx + 1;
- const colLetter = validationSheet.getColumn(colIndex).letter;
-
- // 헤더 추가 (컬럼 레이블)
- validationSheet.getCell(`${colLetter}1`).value = col.label;
-
- // 옵션 추가
- if (col.options) {
- col.options.forEach((option, optIdx) => {
- validationSheet.getCell(`${colLetter}${optIdx + 2}`).value = option;
- });
-
- // 유효성 검사 범위 저장 (ValidationData!$A$2:$A$4 형식)
- validationRanges.set(
- col.key,
- `ValidationData!${colLetter}$2:${colLetter}${
- col.options.length + 1
- }`
- );
- }
- });
-
- // 2. 데이터 시트에 헤더 추가
- const headers = columnsJSON.map((col) => col.label);
- 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" },
- };
- });
-
- // 3. 데이터 행 추가
- 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; // 데이터 유효성 검사를 적용할 최대 행 수
-
- 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 validation = {
- type: "list" as const,
- allowBlank: true,
- formulae: [validationRange],
- showErrorMessage: true,
- 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;
- }
-
- // 빈 행에도 적용 (최대 maxRows까지)
- if (tableData.length + 1 < maxRows) {
- 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);
-
- // 최적 너비 계산
- let maxLength = col.label.length;
- tableData.forEach((row) => {
- const value = row[col.key];
- 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);
+ setIsExporting(true);
+ await exportExcelData({
+ tableData,
+ columnsJSON,
+ formCode,
+ onPendingChange: setIsExporting
});
-
- // 6. 파일 다운로드
- const buffer = await workbook.xlsx.writeBuffer();
- saveAs(
- new Blob([buffer]),
- `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx`
- );
-
- toast.success("Excel 내보내기 완료!");
- } catch (err) {
- console.error("Excel export error:", err);
- toast.error("Excel 내보내기 실패.");
} finally {
- setIsPending(false);
+ setIsExporting(false);
}
}
+ // Handle batch document check
+ const handleBatchDocument = () => {
+ if (tempCount > 0) {
+ setBatchDownDialog(true);
+ } else {
+ toast.error("업로드된 Template File이 없습니다.");
+ }
+ };
+
return (
<>
<ClientDataTable
data={tableData}
columns={columns}
advancedFilterFields={advancedFilterFields}
- // tableRef={tableRef}
>
{/* 버튼 그룹 */}
<div className="flex items-center gap-2">
- {/* 태그 불러오기 버튼 */}
- <Popover>
- <PopoverTrigger asChild>
- <Button variant="default" size="sm">
- Vendor Document
+ {/* 태그 관리 드롭다운 */}
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="outline" size="sm" disabled={isAnyOperationPending}>
+ {(isSyncingTags || isLoadingTags) && (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ )}
+ <TagsIcon className="size-4" />
+ Tag Operations
</Button>
- </PopoverTrigger>
- <PopoverContent className="flex flex-row gap-2 w-auto">
- <Button
- variant="outline"
- size="sm"
- onClick={() => setTempUpDialog(true)}
- >
- Template Upload
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end">
+ {/* 모드에 따라 다른 태그 작업 표시 */}
+ {mode === "IM" ? (
+ <DropdownMenuItem onClick={handleSyncTags} disabled={isAnyOperationPending}>
+ <Tag className="mr-2 h-4 w-4" />
+ Sync Tags
+ </DropdownMenuItem>
+ ) : (
+ <DropdownMenuItem onClick={handleGetTags} disabled={isAnyOperationPending}>
+ <RefreshCcw className="mr-2 h-4 w-4" />
+ Get Tags
+ </DropdownMenuItem>
+ )}
+ <DropdownMenuItem onClick={() => setAddTagDialogOpen(true)} disabled={isAnyOperationPending}>
+ <Plus className="mr-2 h-4 w-4" />
+ Add Tags
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+
+ {/* 리포트 관리 드롭다운 */}
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="outline" size="sm" disabled={isAnyOperationPending}>
+ <Clipboard className="size-4" />
+ Report Operations
</Button>
- <Button
- variant="outline"
- size="sm"
- onClick={() => setBatchDownDialog(true)}
- >
- Vendor Document Create
- </Button>
- </PopoverContent>
- </Popover>
- <Button
- variant="default"
- size="sm"
- onClick={handleSyncTags}
- disabled={isPending}
- >
- {isPending && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- Sync Tags
- </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end">
+ <DropdownMenuItem onClick={() => setTempUpDialog(true)} disabled={isAnyOperationPending}>
+ <Upload className="mr-2 h-4 w-4" />
+ Upload Template
+ </DropdownMenuItem>
+ <DropdownMenuItem onClick={handleBatchDocument} disabled={isAnyOperationPending}>
+ <FileOutput className="mr-2 h-4 w-4" />
+ Batch Document
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
{/* IMPORT 버튼 (파일 선택) */}
- <Button asChild variant="outline" size="sm" disabled={isPending}>
+ <Button asChild variant="outline" size="sm" disabled={isAnyOperationPending}>
<label>
- <Upload className="size-4" />
+ {isImporting ? (
+ <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
+ ) : (
+ <Upload className="size-4" />
+ )}
Import
<input
type="file"
accept=".xlsx,.xls"
onChange={handleImportExcel}
style={{ display: "none" }}
+ disabled={isAnyOperationPending}
/>
</label>
</Button>
- {/* EXPORT 버튼 (새로 추가) */}
+ {/* EXPORT 버튼 */}
<Button
variant="outline"
size="sm"
onClick={handleExportExcel}
- disabled={isPending}
+ disabled={isAnyOperationPending}
>
- <Download className="mr-2 size-4" />
- Export Template
+ {isExporting ? (
+ <Loader className="mr-2 size-4 animate-spin" />
+ ) : (
+ <Download className="mr-2 size-4" />
+ )}
+ Export
</Button>
- {/* SAVE 버튼 */}
+ {/* COMPARE WITH SEDP 버튼 */}
<Button
variant="outline"
size="sm"
- onClick={handleSave}
- disabled={isPending || isSaving}
+ onClick={handleSEDPCompareClick}
+ disabled={isAnyOperationPending}
>
- {isSaving ? (
+ <GitCompareIcon className="mr-2 size-4" />
+ Compare with SEDP
+ </Button>
+
+ {/* SEDP 전송 버튼 */}
+ <Button
+ variant="samsung"
+ size="sm"
+ onClick={handleSEDPSendClick}
+ disabled={isAnyOperationPending}
+ >
+ {isSendingSEDP ? (
<>
<Loader className="mr-2 size-4 animate-spin" />
- 저장 중...
+ SEDP 전송 중...
</>
) : (
<>
- <Save className="mr-2 size-4" />
- Save
+ <Send className="size-4" />
+ Send to SHI
</>
)}
</Button>
</div>
</ClientDataTable>
+ {/* Modal dialog for tag update */}
<UpdateTagSheet
open={rowAction?.type === "update"}
onOpenChange={(open) => {
@@ -619,7 +664,62 @@ export default function DynamicTable({
rowData={rowAction?.row.original ?? null}
formCode={formCode}
contractItemId={contractItemId}
+ onUpdateSuccess={(updatedValues) => {
+ // Update the specific row in tableData when a single row is updated
+ if (rowAction?.row.original?.TAG_NO) {
+ const tagNo = rowAction.row.original.TAG_NO;
+ setTableData(prev =>
+ prev.map(item =>
+ item.TAG_NO === tagNo ? updatedValues : item
+ )
+ );
+ }
+ }}
/>
+
+ {/* Dialog for adding tags */}
+ <AddFormTagDialog
+ projectId={projectId}
+ formCode={formCode}
+ formName={`Form ${formCode}`}
+ contractItemId={contractItemId}
+ open={addTagDialogOpen}
+ onOpenChange={setAddTagDialogOpen}
+ />
+
+ {/* SEDP Confirmation Dialog */}
+ <SEDPConfirmationDialog
+ isOpen={sedpConfirmOpen}
+ onClose={() => setSedpConfirmOpen(false)}
+ onConfirm={handleSEDPSendConfirmed}
+ formName={formName}
+ tagCount={tableData.length}
+ isLoading={isSendingSEDP}
+ />
+
+ {/* SEDP Status Dialog */}
+ <SEDPStatusDialog
+ isOpen={sedpStatusOpen}
+ onClose={() => setSedpStatusOpen(false)}
+ status={sedpStatusData.status}
+ message={sedpStatusData.message}
+ successCount={sedpStatusData.successCount}
+ errorCount={sedpStatusData.errorCount}
+ totalCount={sedpStatusData.totalCount}
+ />
+
+ {/* SEDP Compare Dialog */}
+ <SEDPCompareDialog
+ isOpen={sedpCompareOpen}
+ onClose={() => setSedpCompareOpen(false)}
+ tableData={tableData}
+ columnsJSON={columnsJSON}
+ projectCode={projectCode}
+ formCode={formCode}
+ fetchTagDataFromSEDP={fetchTagDataFromSEDP}
+ />
+
+ {/* Other dialogs */}
{tempUpDialog && (
<FormDataReportTempUploadDialog
columnsJSON={columnsJSON}
@@ -656,4 +756,4 @@ export default function DynamicTable({
)}
</>
);
-}
+} \ No newline at end of file