"use client"; import * as React from "react"; import dynamic from "next/dynamic"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { GenericData } from "./export-excel-form"; import * as GC from "@mescius/spread-sheets"; import { toast } from "sonner"; import { updateFormDataInDB } from "@/lib/forms/services"; import { Loader, Save } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; // SpreadSheets를 동적으로 import (SSR 비활성화) const SpreadSheets = dynamic( () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), { ssr: false, loading: () => (
Loading SpreadSheets...
) } ); // 라이센스 키 설정을 클라이언트에서만 실행 if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; } interface TemplateItem { TMPL_ID: string; NAME: string; TMPL_TYPE: string; SPR_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; CONTENT?: string; DATA_SHEETS: Array<{ SHEET_NAME: string; REG_TYPE_ID: string; MAP_CELL_ATT: Array<{ ATT_ID: string; IN: string; }>; }>; }; GRD_LST_SETUP: { REG_TYPE_ID: string; SPR_ITM_IDS: Array; ATTS: Array<{}>; }; SPR_ITM_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; CONTENT?: string; DATA_SHEETS: Array<{ SHEET_NAME: string; REG_TYPE_ID: string; MAP_CELL_ATT: Array<{ ATT_ID: string; IN: string; }>; }>; }; } interface TemplateViewDialogProps { isOpen: boolean; onClose: () => void; templateData: TemplateItem[] | any; selectedRow: GenericData; formCode: string; contractItemId: number; editableFieldsMap?: Map; // 편집 가능 필드 정보 onUpdateSuccess?: (updatedValues: Record) => void; } export function TemplateViewDialog({ isOpen, onClose, templateData, selectedRow, formCode, contractItemId, editableFieldsMap = new Map(), onUpdateSuccess }: TemplateViewDialogProps) { const [hostStyle, setHostStyle] = React.useState({ width: '100%', height: '100%' }); const [isPending, setIsPending] = React.useState(false); const [hasChanges, setHasChanges] = React.useState(false); const [currentSpread, setCurrentSpread] = React.useState(null); const [selectedTemplateId, setSelectedTemplateId] = React.useState(""); const [cellMappings, setCellMappings] = React.useState>([]); const [isClient, setIsClient] = React.useState(false); // 클라이언트 사이드에서만 렌더링되도록 보장 React.useEffect(() => { setIsClient(true); }, []); // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것만 필터링 const normalizedTemplates = React.useMemo((): TemplateItem[] => { if (!templateData) return []; let templates: TemplateItem[]; if (Array.isArray(templateData)) { templates = templateData as TemplateItem[]; } else { templates = [templateData as TemplateItem]; } return templates.filter(template => { const sprContent = template.SPR_LST_SETUP?.CONTENT; const sprItmContent = template.SPR_ITM_LST_SETUP?.CONTENT; return sprContent || sprItmContent; }); }, [templateData]); // 선택된 템플릿 가져오기 const selectedTemplate = React.useMemo(() => { if (!selectedTemplateId) return normalizedTemplates[0]; return normalizedTemplates.find(t => t.TMPL_ID === selectedTemplateId) || normalizedTemplates[0]; }, [normalizedTemplates, selectedTemplateId]); // 현재 TAG의 편집 가능한 필드 목록 가져오기 const editableFields = React.useMemo(() => { if (!selectedRow?.TAG_NO || !editableFieldsMap.has(selectedRow.TAG_NO)) { return []; } return editableFieldsMap.get(selectedRow.TAG_NO) || []; }, [selectedRow?.TAG_NO, editableFieldsMap]); // 필드가 편집 가능한지 판별하는 함수 const isFieldEditable = React.useCallback((attId: string) => { // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 if (attId === "TAG_NO" || attId === "TAG_DESC") { return true; } // editableFieldsMap이 있으면 해당 리스트에 있는지 확인 if (selectedRow?.TAG_NO && editableFieldsMap.has(selectedRow.TAG_NO)) { return editableFields.includes(attId); } return false; }, [selectedRow?.TAG_NO, editableFieldsMap, editableFields]); // 셀 주소를 행과 열로 변환하는 함수 (예: "M1" -> {row: 0, col: 12}) const parseCellAddress = (address: string): {row: number, col: number} | null => { if (!address || address.trim() === "") return null; const match = address.match(/^([A-Z]+)(\d+)$/); if (!match) return null; const [, colStr, rowStr] = match; // 열 문자를 숫자로 변환 (A=0, B=1, ..., Z=25, AA=26, ...) let col = 0; for (let i = 0; i < colStr.length; i++) { col = col * 26 + (colStr.charCodeAt(i) - 65 + 1); } col -= 1; // 0-based index로 변환 const row = parseInt(rowStr) - 1; // 0-based index로 변환 return { row, col }; }; // 템플릿 변경 시 기본 선택 React.useEffect(() => { if (normalizedTemplates.length > 0 && !selectedTemplateId) { setSelectedTemplateId(normalizedTemplates[0].TMPL_ID); } }, [normalizedTemplates, selectedTemplateId]); const initSpread = React.useCallback((spread: any) => { if (!spread || !selectedTemplate || !selectedRow) return; try { setCurrentSpread(spread); setHasChanges(false); // CONTENT 찾기 let contentJson = null; let dataSheets = null; if (selectedTemplate.SPR_LST_SETUP?.CONTENT) { contentJson = selectedTemplate.SPR_LST_SETUP.CONTENT; dataSheets = selectedTemplate.SPR_LST_SETUP.DATA_SHEETS; console.log('Using SPR_LST_SETUP.CONTENT for template:', selectedTemplate.NAME); } else if (selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT) { contentJson = selectedTemplate.SPR_ITM_LST_SETUP.CONTENT; dataSheets = selectedTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS; console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', selectedTemplate.NAME); } if (!contentJson) { console.warn('No CONTENT found in template:', selectedTemplate.NAME); return; } console.log('Loading template content for:', selectedTemplate.NAME); const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; // 렌더링 일시 중단 (성능 향상) spread.suspendPaint(); try { // fromJSON으로 템플릿 구조 로드 spread.fromJSON(jsonData); // 활성 시트 가져오기 const activeSheet = spread.getActiveSheet(); // 시트 보호 먼저 해제 activeSheet.options.isProtected = false; // MAP_CELL_ATT 정보를 사용해서 셀에 데이터 매핑과 스타일을 한번에 처리 if (dataSheets && dataSheets.length > 0) { const mappings: Array<{attId: string, cellAddress: string, isEditable: boolean}> = []; dataSheets.forEach(dataSheet => { if (dataSheet.MAP_CELL_ATT) { dataSheet.MAP_CELL_ATT.forEach(mapping => { const { ATT_ID, IN } = mapping; // 셀 주소가 비어있지 않은 경우만 처리 if (IN && IN.trim() !== "") { const cellPos = parseCellAddress(IN); if (cellPos) { const isEditable = isFieldEditable(ATT_ID); mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: isEditable }); // 셀 객체 가져오기 const cell = activeSheet.getCell(cellPos.row, cellPos.col); // selectedRow에서 해당 값 가져와서 셀에 설정 const value = selectedRow[ATT_ID]; if (value !== undefined && value !== null) { cell.value(value); } // 편집 권한 설정 cell.locked(!isEditable); // 즉시 스타일 적용 (기존 스타일 보존하면서) const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col); if (existingStyle) { // 기존 스타일 복사 const newStyle = Object.assign(new GC.Spread.Sheets.Style(), existingStyle); // 편집 권한에 따라 배경색만 변경 if (isEditable) { newStyle.backColor = "#f0fdf4"; // 연한 녹색 } else { newStyle.backColor = "#f9fafb"; // 연한 회색 newStyle.foreColor = "#6b7280"; // 회색 글자 } // 스타일 적용 activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); } else { // 기존 스타일이 없는 경우 새로운 스타일 생성 const newStyle = new GC.Spread.Sheets.Style(); if (isEditable) { newStyle.backColor = "#f0fdf4"; } else { newStyle.backColor = "#f9fafb"; newStyle.foreColor = "#6b7280"; } activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); } console.log(`Mapped ${ATT_ID} (${value}) to cell ${IN} - ${isEditable ? 'Editable' : 'Read-only'}`); } } }); } }); setCellMappings(mappings); // 시트 보호 설정 activeSheet.options.isProtected = true; activeSheet.options.protectionOptions = { allowSelectLockedCells: true, allowSelectUnlockedCells: true, allowSort: false, allowFilter: false, allowEditObjects: false, allowResizeRows: false, allowResizeColumns: false }; // 이벤트 리스너 추가 activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => { console.log('Cell changed:', info); setHasChanges(true); }); activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => { console.log('Value changed:', info); setHasChanges(true); }); // 편집 시작 시 읽기 전용 셀 확인 activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { const mapping = mappings.find(m => { const cellPos = parseCellAddress(m.cellAddress); return cellPos && cellPos.row === info.row && cellPos.col === info.col; }); if (mapping && !mapping.isEditable) { toast.warning(`${mapping.attId} field is read-only`); info.cancel = true; } }); } } finally { // 렌더링 재개 (모든 변경사항이 한번에 화면에 표시됨) spread.resumePaint(); } } catch (error) { console.error('Error initializing spread:', error); toast.error('Failed to load template'); // 에러 발생 시에도 렌더링 재개 if (spread && spread.resumePaint) { spread.resumePaint(); } } }, [selectedTemplate, selectedRow, isFieldEditable]); // 템플릿 변경 핸들러 const handleTemplateChange = (templateId: string) => { setSelectedTemplateId(templateId); setHasChanges(false); if (currentSpread) { setTimeout(() => { initSpread(currentSpread); }, 100); } }; // 변경사항 저장 함수 const handleSaveChanges = React.useCallback(async () => { if (!currentSpread || !hasChanges || !selectedRow) { toast.info("No changes to save"); return; } try { setIsPending(true); const activeSheet = currentSpread.getActiveSheet(); const dataToSave = { ...selectedRow }; // cellMappings를 사용해서 편집 가능한 셀의 값만 추출 cellMappings.forEach(mapping => { if (mapping.isEditable) { const cellPos = parseCellAddress(mapping.cellAddress); if (cellPos) { const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); dataToSave[mapping.attId] = cellValue; } } }); // TAG_NO는 절대 변경되지 않도록 원본 값으로 강제 설정 dataToSave.TAG_NO = selectedRow.TAG_NO; console.log('Data to save (TAG_NO preserved):', dataToSave); const { success, message } = await updateFormDataInDB( formCode, contractItemId, dataToSave ); if (!success) { toast.error(message); return; } toast.success("Changes saved successfully!"); const updatedData = { ...selectedRow, ...dataToSave, }; onUpdateSuccess?.(updatedData); setHasChanges(false); } catch (error) { console.error("Error saving changes:", error); toast.error("An unexpected error occurred while saving"); } finally { setIsPending(false); } }, [currentSpread, hasChanges, formCode, contractItemId, selectedRow, onUpdateSuccess, cellMappings]); if (!isOpen) return null; return ( SEDP Template - {formCode} {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`} {hasChanges && ( • Unsaved changes )}
Editable fields Read-only fields {cellMappings.length > 0 && ( {cellMappings.filter(m => m.isEditable).length} of {cellMappings.length} fields editable )}
{/* 템플릿 선택 UI */} {normalizedTemplates.length > 1 && (
({normalizedTemplates.length} templates available)
)} {/* SpreadSheets 컴포넌트 영역 */}
{selectedTemplate && isClient ? ( ) : (
{!isClient ? ( <> Loading... ) : ( "No template available" )}
)}
{hasChanges && ( )}
); }