diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-09 12:19:05 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-09 12:19:05 +0000 |
| commit | 6d654b1ba2c19e0bf1745b636908e3b00a0f02c7 (patch) | |
| tree | f6d48c0d3a65b428a828acea5db65db8e7bf0db8 /components/form-data/spreadJS-dialog copy.tsx | |
| parent | 44794a8628997c0d979adb5bd6711cd848b3e397 (diff) | |
(대표님) 20250709 변경사항 (약 18시 30분까지)
Diffstat (limited to 'components/form-data/spreadJS-dialog copy.tsx')
| -rw-r--r-- | components/form-data/spreadJS-dialog copy.tsx | 539 |
1 files changed, 539 insertions, 0 deletions
diff --git a/components/form-data/spreadJS-dialog copy.tsx b/components/form-data/spreadJS-dialog copy.tsx new file mode 100644 index 00000000..5a51c2b5 --- /dev/null +++ b/components/form-data/spreadJS-dialog copy.tsx @@ -0,0 +1,539 @@ +"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: () => ( + <div className="flex items-center justify-center h-full"> + <Loader className="mr-2 h-4 w-4 animate-spin" /> + Loading SpreadSheets... + </div> + ) + } +); + +// 라이센스 키 설정을 클라이언트에서만 실행 +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<string>; + 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<string>; + ATTS: Array<{}>; + }; + SPR_ITM_LST_SETUP: { + ACT_SHEET: string; + HIDN_SHEETS: Array<string>; + 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<string, string[]>; // 편집 가능 필드 정보 + onUpdateSuccess?: (updatedValues: Record<string, any>) => 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<any>(null); + const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); + const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]); + 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 ( + <Dialog open={isOpen} onOpenChange={onClose}> + <DialogContent + className="w-[80%] max-w-none h-[80vh] flex flex-col" + style={{maxWidth:"80vw"}} + > + <DialogHeader className="flex-shrink-0"> + <DialogTitle>SEDP Template - {formCode}</DialogTitle> + <DialogDescription> + {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`} + {hasChanges && ( + <span className="ml-2 text-orange-600 font-medium"> + • Unsaved changes + </span> + )} + <br /> + <div className="flex items-center gap-4 mt-2"> + <span className="text-xs text-muted-foreground"> + <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1"></span> + Editable fields + </span> + <span className="text-xs text-muted-foreground"> + <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span> + Read-only fields + </span> + {cellMappings.length > 0 && ( + <span className="text-xs text-blue-600"> + {cellMappings.filter(m => m.isEditable).length} of {cellMappings.length} fields editable + </span> + )} + </div> + </DialogDescription> + </DialogHeader> + + {/* 템플릿 선택 UI */} + {normalizedTemplates.length > 1 && ( + <div className="flex-shrink-0 px-4 py-2 border-b"> + <div className="flex items-center gap-2"> + <label className="text-sm font-medium">Template:</label> + <Select value={selectedTemplateId} onValueChange={handleTemplateChange}> + <SelectTrigger className="w-64"> + <SelectValue placeholder="Select a template" /> + </SelectTrigger> + <SelectContent> + {normalizedTemplates.map((template) => ( + <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> + <div className="flex flex-col"> + <span>{template.NAME || `Template ${template.TMPL_ID.slice(0, 8)}`}</span> + <span className="text-xs text-muted-foreground">{template.TMPL_TYPE}</span> + </div> + </SelectItem> + ))} + </SelectContent> + </Select> + <span className="text-xs text-muted-foreground"> + ({normalizedTemplates.length} templates available) + </span> + </div> + </div> + )} + + {/* SpreadSheets 컴포넌트 영역 */} + <div className="flex-1 overflow-hidden"> + {selectedTemplate && isClient ? ( + <SpreadSheets + key={selectedTemplateId} + workbookInitialized={initSpread} + hostStyle={hostStyle} + /> + ) : ( + <div className="flex items-center justify-center h-full text-muted-foreground"> + {!isClient ? ( + <> + <Loader className="mr-2 h-4 w-4 animate-spin" /> + Loading... + </> + ) : ( + "No template available" + )} + </div> + )} + </div> + + <DialogFooter className="flex-shrink-0"> + <Button variant="outline" onClick={onClose}> + Close + </Button> + + {hasChanges && ( + <Button + variant="default" + onClick={handleSaveChanges} + disabled={isPending} + > + {isPending ? ( + <> + <Loader className="mr-2 h-4 w-4 animate-spin" /> + Saving... + </> + ) : ( + <> + <Save className="mr-2 h-4 w-4" /> + Save Changes + </> + )} + </Button> + )} + + </DialogFooter> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
