"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 (
);
}