summaryrefslogtreecommitdiff
path: root/components/form-data/spreadJS-dialog copy 2.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-19 07:51:27 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-19 07:51:27 +0000
commit9ecdfb23fe3df6a5df86782385002c562dfc1198 (patch)
tree4188cb7e6bf2c862d9c86a59d79946bd41217227 /components/form-data/spreadJS-dialog copy 2.tsx
parentb67861fbb424c7ad47ad1538f75e2945bd8890c5 (diff)
(대표님) rfq 히스토리, swp 등
Diffstat (limited to 'components/form-data/spreadJS-dialog copy 2.tsx')
-rw-r--r--components/form-data/spreadJS-dialog copy 2.tsx1002
1 files changed, 0 insertions, 1002 deletions
diff --git a/components/form-data/spreadJS-dialog copy 2.tsx b/components/form-data/spreadJS-dialog copy 2.tsx
deleted file mode 100644
index 520362ff..00000000
--- a/components/form-data/spreadJS-dialog copy 2.tsx
+++ /dev/null
@@ -1,1002 +0,0 @@
-"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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-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, AlertTriangle } from "lucide-react";
-import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css';
-import { DataTableColumnJSON, ColumnType } from "./form-data-table-columns";
-
-// 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 ValidationError {
- cellAddress: string;
- attId: string;
- value: any;
- expectedType: ColumnType;
- message: string;
-}
-
-interface CellMapping {
- attId: string;
- cellAddress: string;
- isEditable: boolean;
- dataRowIndex?: number;
-}
-
-interface TemplateViewDialogProps {
- isOpen: boolean;
- onClose: () => void;
- templateData: TemplateItem[] | any;
- selectedRow?: GenericData; // SPREAD_ITEM용
- tableData?: GenericData[]; // SPREAD_LIST용
- formCode: string;
- columnsJSON: DataTableColumnJSON[]
- contractItemId: number;
- editableFieldsMap?: Map<string, string[]>;
- onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void;
-}
-
-export function TemplateViewDialog({
- isOpen,
- onClose,
- templateData,
- selectedRow,
- tableData = [],
- formCode,
- contractItemId,
- columnsJSON,
- 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 [cellMappings, setCellMappings] = React.useState<CellMapping[]>([]);
- const [isClient, setIsClient] = React.useState(false);
- const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | null>(null);
- const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]);
- const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>("");
- const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]);
-
- // 클라이언트 사이드에서만 렌더링되도록 보장
- React.useEffect(() => {
- setIsClient(true);
- }, []);
-
- // 사용 가능한 템플릿들을 필터링하고 설정
- React.useEffect(() => {
- if (!templateData) return;
-
- let templates: TemplateItem[];
- if (Array.isArray(templateData)) {
- templates = templateData as TemplateItem[];
- } else {
- templates = [templateData as TemplateItem];
- }
-
- // CONTENT가 있는 템플릿들 필터링
- const validTemplates = templates.filter(template => {
- const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT;
- const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT;
- const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || template.TMPL_TYPE === "SPREAD_ITEM";
-
- return isValidType && (hasSpreadListContent || hasSpreadItemContent);
- });
-
- setAvailableTemplates(validTemplates);
-
- // 첫 번째 유효한 템플릿을 기본으로 선택
- if (validTemplates.length > 0 && !selectedTemplateId) {
- setSelectedTemplateId(validTemplates[0].TMPL_ID);
- setTemplateType(validTemplates[0].TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
- }
- }, [templateData, selectedTemplateId]);
-
- // 선택된 템플릿 변경 처리
- const handleTemplateChange = (templateId: string) => {
- const template = availableTemplates.find(t => t.TMPL_ID === templateId);
- if (template) {
- setSelectedTemplateId(templateId);
- setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
- setHasChanges(false);
- setValidationErrors([]);
-
- // SpreadSheets 재초기화
- if (currentSpread) {
- const template = availableTemplates.find(t => t.TMPL_ID === templateId);
- if (template) {
- initSpread(currentSpread, template);
- }
- }
- }
- };
-
- // 현재 선택된 템플릿 가져오기
- const selectedTemplate = React.useMemo(() => {
- return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId);
- }, [availableTemplates, selectedTemplateId]);
-
- // 편집 가능한 필드 목록 계산
- const editableFields = React.useMemo(() => {
- // SPREAD_ITEM인 경우: selectedRow의 TAG_NO로 확인
- if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) {
- if (!editableFieldsMap.has(selectedRow.TAG_NO)) {
- return [];
- }
- return editableFieldsMap.get(selectedRow.TAG_NO) || [];
- }
-
- // SPREAD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리
- if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
- const firstRowTagNo = tableData[0]?.TAG_NO;
- if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) {
- return editableFieldsMap.get(firstRowTagNo) || [];
- }
- }
-
- return [];
- }, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]);
-
- // 필드가 편집 가능한지 판별하는 함수
- const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => {
- // columnsJSON에서 해당 attId의 shi 값 확인
- const columnConfig = columnsJSON.find(col => col.key === attId);
- if (columnConfig?.shi === true) {
- return false; // columnsJSON에서 shi가 true이면 편집 불가
- }
-
- // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우)
- if (attId === "TAG_NO" || attId === "TAG_DESC") {
- return true;
- }
-
- // SPREAD_ITEM인 경우: editableFields 체크
- if (templateType === 'SPREAD_ITEM') {
- return editableFields.includes(attId);
- }
-
- // SPREAD_LIST인 경우: 개별 행의 편집 가능성도 고려
- if (templateType === 'SPREAD_LIST') {
- // 기본적으로 editableFields에 포함되어야 함
- if (!editableFields.includes(attId)) {
- return false;
- }
-
- // rowData가 제공된 경우 해당 행의 shi 상태도 확인
- if (rowData && rowData.shi === true) {
- return false;
- }
-
- return true;
- }
-
- // 기본적으로는 editableFields 체크
- // return editableFields.includes(attId);
- return true;
- }, [templateType, columnsJSON, editableFields]);
-
- // 편집 가능한 필드 개수 계산
- const editableFieldsCount = React.useMemo(() => {
- return cellMappings.filter(m => m.isEditable).length;
- }, [cellMappings]);
-
- // 셀 주소를 행과 열로 변환하는 함수
- 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;
-
- let col = 0;
- for (let i = 0; i < colStr.length; i++) {
- col = col * 26 + (colStr.charCodeAt(i) - 65 + 1);
- }
- col -= 1;
-
- const row = parseInt(rowStr) - 1;
-
- return { row, col };
- };
-
- // 데이터 타입 검증 함수
- const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => {
- if (value === undefined || value === null || value === "") {
- return null; // 빈 값은 별도 required 검증에서 처리
- }
-
- switch (columnType) {
- case "NUMBER":
- if (isNaN(Number(value))) {
- return "Value must be a valid number";
- }
- break;
- case "LIST":
- if (options && !options.includes(String(value))) {
- return `Value must be one of: ${options.join(", ")}`;
- }
- break;
- case "STRING":
- // STRING 타입은 대부분의 값을 허용
- break;
- default:
- // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음
- break;
- }
-
- return null;
- };
-
- // 전체 데이터 검증 함수
- const validateAllData = React.useCallback(() => {
- if (!currentSpread || !selectedTemplate) return [];
-
- const activeSheet = currentSpread.getActiveSheet();
- const errors: ValidationError[] = [];
-
- cellMappings.forEach(mapping => {
- const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
- if (!columnConfig) return;
-
- const cellPos = parseCellAddress(mapping.cellAddress);
- if (!cellPos) return;
-
- if (templateType === 'SPREAD_ITEM') {
- // 단일 행 검증
- const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
- const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
-
- if (errorMessage) {
- errors.push({
- cellAddress: mapping.cellAddress,
- attId: mapping.attId,
- value: cellValue,
- expectedType: columnConfig.type,
- message: errorMessage
- });
- }
- } else if (templateType === 'SPREAD_LIST') {
- // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴
- const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
- const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
-
- if (errorMessage) {
- errors.push({
- cellAddress: mapping.cellAddress,
- attId: mapping.attId,
- value: cellValue,
- expectedType: columnConfig.type,
- message: errorMessage
- });
- }
- }
- });
-
- setValidationErrors(errors);
- return errors;
- }, [currentSpread, selectedTemplate, cellMappings, columnsJSON, templateType]);
-
- // ═══════════════════════════════════════════════════════════════════════════════
- // 🛠️ 헬퍼 함수들
- // ═══════════════════════════════════════════════════════════════════════════════
-
- // 🎨 셀 스타일 생성
- const createCellStyle = React.useCallback((isEditable: boolean) => {
- const style = new GC.Spread.Sheets.Style();
- if (isEditable) {
- style.backColor = "#f0fdf4"; // 연한 초록 (편집 가능)
- } else {
- style.backColor = "#f9fafb"; // 연한 회색 (읽기 전용)
- style.foreColor = "#6b7280";
- }
- return style;
- }, []);
-
-
-// 🎯 간소화된 드롭다운 설정 - setupSimpleValidation 완전 제거
-
-const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
- try {
- console.log(`🎯 Setting up dropdown for ${rowCount} rows with options:`, options);
-
- // ✅ options 정규화
- const safeOptions = options
- .filter(opt => opt !== null && opt !== undefined && opt !== '')
- .map(opt => String(opt).trim())
- .filter(opt => opt.length > 0)
- .filter((opt, index, arr) => arr.indexOf(opt) === index)
- .slice(0, 20);
-
- if (safeOptions.length === 0) {
- console.warn(`⚠️ No valid options found, skipping`);
- return;
- }
-
- console.log(`📋 Safe options:`, safeOptions);
-
- // ✅ DataValidation용 문자열 준비
- const optionsString = safeOptions.join(',');
-
- // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성!
- for (let i = 0; i < rowCount; i++) {
- try {
- const targetRow = cellPos.row + i;
-
- // ✅ 각 셀마다 새로운 ComboBox 인스턴스 생성
- const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox();
- comboBoxCellType.items(safeOptions); // 배열로 전달
- comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
-
- // ✅ 각 셀마다 새로운 DataValidation 인스턴스 생성
- const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(optionsString);
-
- // ComboBox + DataValidation 둘 다 적용
- activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType);
- activeSheet.setDataValidator(targetRow, cellPos.col, cellValidator);
-
- // 셀 잠금 해제
- const cell = activeSheet.getCell(targetRow, cellPos.col);
- cell.locked(false);
-
- console.log(`✅ Individual dropdown applied to [${targetRow}, ${cellPos.col}]`);
-
- } catch (cellError) {
- console.warn(`⚠️ Failed to apply to row ${cellPos.row + i}:`, cellError);
- }
- }
-
- console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`);
-
- } catch (error) {
- console.error('❌ Dropdown setup failed:', error);
- }
-}, []);
- // 🚀 행 용량 확보
- const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => {
- const currentRowCount = activeSheet.getRowCount();
- if (requiredRowCount > currentRowCount) {
- activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가
- console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`);
- }
- }, []);
-
- // 🛡️ 시트 보호 및 이벤트 설정
- const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => {
- console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`);
-
- // 시트 보호 설정
- activeSheet.options.isProtected = true;
- activeSheet.options.protectionOptions = {
- allowSelectLockedCells: true,
- allowSelectUnlockedCells: true,
- allowSort: false,
- allowFilter: false,
- allowEditObjects: false,
- allowResizeRows: false,
- allowResizeColumns: false
- };
-
- // 🎯 변경 감지 이벤트
- const changeEvents = [
- GC.Spread.Sheets.Events.CellChanged,
- GC.Spread.Sheets.Events.ValueChanged,
- GC.Spread.Sheets.Events.ClipboardPasted
- ];
-
- changeEvents.forEach(eventType => {
- activeSheet.bind(eventType, () => {
- console.log(`📝 ${eventType} detected`);
- setHasChanges(true);
- });
- });
-
- // 🚫 편집 시작 권한 확인 (수정됨)
- activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => {
- console.log(`🎯 EditStarting: Row ${info.row}, Col ${info.col}`);
-
- // ✅ 정확한 매핑 찾기 (행/열 정확히 일치)
- const exactMapping = mappings.find(m => {
- const cellPos = parseCellAddress(m.cellAddress);
- return cellPos && cellPos.row === info.row && cellPos.col === info.col;
- });
-
- if (!exactMapping) {
- console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - allowing edit`);
- return; // 매핑이 없으면 허용 (템플릿 영역 밖)
- }
-
- console.log(`📋 Found mapping: ${exactMapping.attId} at ${exactMapping.cellAddress}`);
-
- // 기본 편집 권한 확인
- if (!exactMapping.isEditable) {
- console.log(`🚫 Field ${exactMapping.attId} is not editable`);
- toast.warning(`${exactMapping.attId} field is read-only`);
- info.cancel = true;
- return;
- }
-
- // SPREAD_LIST 개별 행 SHI 확인
- if (templateType === 'SPREAD_LIST' && exactMapping.dataRowIndex !== undefined) {
- const dataRowIndex = exactMapping.dataRowIndex;
-
- console.log(`🔍 Checking SHI for data row ${dataRowIndex}`);
-
- if (dataRowIndex >= 0 && dataRowIndex < tableData.length) {
- const rowData = tableData[dataRowIndex];
- if (rowData?.shi === true) {
- console.log(`🚫 Row ${dataRowIndex} is in SHI mode`);
- toast.warning(`Row ${dataRowIndex + 1}: ${exactMapping.attId} field is read-only (SHI mode)`);
- info.cancel = true;
- return;
- }
- } else {
- console.warn(`⚠️ Invalid dataRowIndex: ${dataRowIndex} (tableData.length: ${tableData.length})`);
- }
- }
-
- console.log(`✅ Edit allowed for ${exactMapping.attId}`);
- });
-
- // ✅ 편집 완료 검증 (수정됨)
- activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (event: any, info: any) => {
- console.log(`🏁 EditEnded: Row ${info.row}, Col ${info.col}`);
-
- // ✅ 정확한 매핑 찾기
- const exactMapping = mappings.find(m => {
- const cellPos = parseCellAddress(m.cellAddress);
- return cellPos && cellPos.row === info.row && cellPos.col === info.col;
- });
-
- if (!exactMapping) {
- console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - skipping validation`);
- return;
- }
-
- const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId);
- if (columnConfig) {
- const cellValue = activeSheet.getValue(info.row, info.col);
- console.log(`🔍 Validating ${exactMapping.attId}: "${cellValue}"`);
-
- const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
- const cell = activeSheet.getCell(info.row, info.col);
-
- if (errorMessage) {
- console.log(`❌ Validation failed: ${errorMessage}`);
-
- // 🚨 에러 스타일 적용 (편집 가능 상태 유지)
- const errorStyle = new GC.Spread.Sheets.Style();
- errorStyle.backColor = "#fef2f2";
- errorStyle.foreColor = "#dc2626";
- errorStyle.borderLeft = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
- errorStyle.borderRight = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
- errorStyle.borderTop = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
- errorStyle.borderBottom = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
-
- activeSheet.setStyle(info.row, info.col, errorStyle);
- cell.locked(!exactMapping.isEditable);
-
- toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}. Please correct the value.`, { duration: 5000 });
- } else {
- console.log(`✅ Validation passed`);
-
- // ✅ 정상 스타일 복원
- const normalStyle = createCellStyle(exactMapping.isEditable);
- activeSheet.setStyle(info.row, info.col, normalStyle);
- cell.locked(!exactMapping.isEditable);
- }
- }
- });
-
- console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`);
- }, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]);
-
- // ═══════════════════════════════════════════════════════════════════════════════
- // 🏗️ 메인 SpreadSheets 초기화 함수
- // ═══════════════════════════════════════════════════════════════════════════════
-
- const initSpread = React.useCallback((spread: any, template?: TemplateItem) => {
- const workingTemplate = template || selectedTemplate;
- if (!spread || !workingTemplate) return;
-
- try {
- // 🔄 초기 설정
- setCurrentSpread(spread);
- setHasChanges(false);
- setValidationErrors([]);
-
- // 📋 템플릿 콘텐츠 및 데이터 시트 추출
- let contentJson = null;
- let dataSheets = null;
-
- // SPR_LST_SETUP.CONTENT 우선 사용
- if (workingTemplate.SPR_LST_SETUP?.CONTENT) {
- contentJson = workingTemplate.SPR_LST_SETUP.CONTENT;
- dataSheets = workingTemplate.SPR_LST_SETUP.DATA_SHEETS;
- console.log('✅ Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME);
- }
- // SPR_ITM_LST_SETUP.CONTENT 대안 사용
- else if (workingTemplate.SPR_ITM_LST_SETUP?.CONTENT) {
- contentJson = workingTemplate.SPR_ITM_LST_SETUP.CONTENT;
- dataSheets = workingTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS;
- console.log('✅ Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME);
- }
-
- if (!contentJson) {
- console.warn('❌ No CONTENT found in template:', workingTemplate.NAME);
- return;
- }
-
- // 🏗️ SpreadSheets 초기화
- const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
-
- // 성능을 위한 렌더링 일시 중단
- spread.suspendPaint();
-
- try {
- // 템플릿 구조 로드
- spread.fromJSON(jsonData);
- const activeSheet = spread.getActiveSheet();
-
- // 시트 보호 해제 (편집을 위해)
- activeSheet.options.isProtected = false;
-
- // 📊 셀 매핑 및 데이터 처리
- if (dataSheets && dataSheets.length > 0) {
- const mappings: CellMapping[] = [];
-
- // 🔄 각 데이터 시트의 매핑 정보 처리
- 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 columnConfig = columnsJSON.find(col => col.key === ATT_ID);
-
- // 🎯 템플릿 타입별 데이터 처리
- if (templateType === 'SPREAD_ITEM' && selectedRow) {
- // 📝 단일 행 처리 (SPREAD_ITEM)
- const isEditable = isFieldEditable(ATT_ID);
-
- // 매핑 정보 저장
- mappings.push({
- attId: ATT_ID,
- cellAddress: IN,
- isEditable: isEditable,
- dataRowIndex: 0
- });
-
- const cell = activeSheet.getCell(cellPos.row, cellPos.col);
- const value = selectedRow[ATT_ID];
-
- // 값 설정
- cell.value(value ?? null);
-
- // 🎨 스타일 및 편집 권한 설정
- cell.locked(!isEditable);
- const style = createCellStyle(isEditable);
- activeSheet.setStyle(cellPos.row, cellPos.col, style);
-
- // 📋 LIST 타입 드롭다운 설정
- if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
- setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1);
- }
-
- } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
- // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨
- console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`);
-
- // 🚀 행 확장 (필요시)
- ensureRowCapacity(activeSheet, cellPos.row + tableData.length);
-
- // 📋 각 행마다 개별 매핑 생성
- tableData.forEach((rowData, index) => {
- const targetRow = cellPos.row + index;
- const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`;
- const cellEditable = isFieldEditable(ATT_ID, rowData);
-
- // 개별 매핑 추가
- mappings.push({
- attId: ATT_ID,
- cellAddress: targetCellAddress, // 각 행마다 다른 주소
- isEditable: cellEditable,
- dataRowIndex: index // 원본 데이터 인덱스
- });
-
- console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`);
- });
-
- // 📋 LIST 타입 드롭다운 설정 (조건부)
- if (columnConfig?.type === "LIST" && columnConfig.options) {
- // 편집 가능한 행이 하나라도 있으면 드롭다운 설정
- const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData));
- if (hasEditableRows) {
- setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length);
- }
- }
-
- // 🎨 개별 셀 데이터 및 스타일 설정
- tableData.forEach((rowData, index) => {
- const targetRow = cellPos.row + index;
- const cell = activeSheet.getCell(targetRow, cellPos.col);
- const value = rowData[ATT_ID];
-
- // 값 설정
- cell.value(value ?? null);
- console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`);
-
- // 편집 권한 및 스타일 설정
- const cellEditable = isFieldEditable(ATT_ID, rowData);
- cell.locked(!cellEditable);
- const style = createCellStyle(cellEditable);
- activeSheet.setStyle(targetRow, cellPos.col, style);
- });
- }
-
- console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`);
- }
- }
- });
- }
- });
-
- // 💾 매핑 정보 저장 및 이벤트 설정
- setCellMappings(mappings);
- setupSheetProtectionAndEvents(activeSheet, mappings);
- }
-
- } finally {
- // 렌더링 재개
- spread.resumePaint();
- }
-
- } catch (error) {
- console.error('❌ Error initializing spread:', error);
- toast.error('Failed to load template');
- if (spread?.resumePaint) {
- spread.resumePaint();
- }
- }
- }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]);
-
- // 변경사항 저장 함수
- const handleSaveChanges = React.useCallback(async () => {
- if (!currentSpread || !hasChanges) {
- toast.info("No changes to save");
- return;
- }
-
- // 저장 전 데이터 검증
- const errors = validateAllData();
- if (errors.length > 0) {
- toast.error(`Cannot save: ${errors.length} validation errors found. Please fix them first.`);
- return;
- }
-
- try {
- setIsPending(true);
-
- const activeSheet = currentSpread.getActiveSheet();
-
- if (templateType === 'SPREAD_ITEM' && selectedRow) {
- // 단일 행 저장
- const dataToSave = { ...selectedRow };
-
- 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;
- }
- }
- });
-
- dataToSave.TAG_NO = selectedRow.TAG_NO;
-
- const { success, message } = await updateFormDataInDB(
- formCode,
- contractItemId,
- dataToSave
- );
-
- if (!success) {
- toast.error(message);
- return;
- }
-
- toast.success("Changes saved successfully!");
- onUpdateSuccess?.(dataToSave);
-
- } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
- // 복수 행 저장
- const updatedRows: GenericData[] = [];
- let saveCount = 0;
-
- for (let i = 0; i < tableData.length; i++) {
- const originalRow = tableData[i];
- const dataToSave = { ...originalRow };
- let hasRowChanges = false;
-
- // 각 매핑에 대해 해당 행의 값 확인
- cellMappings.forEach(mapping => {
- if (mapping.dataRowIndex === i && mapping.isEditable) {
- const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
- const isColumnEditable = columnConfig?.shi !== true;
- const isRowEditable = originalRow.shi !== true;
-
- if (isColumnEditable && isRowEditable) {
- const cellPos = parseCellAddress(mapping.cellAddress);
- if (cellPos) {
- const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
-
- // 값이 변경되었는지 확인
- if (cellValue !== originalRow[mapping.attId]) {
- dataToSave[mapping.attId] = cellValue;
- hasRowChanges = true;
- }
- }
- }
- }
- });
-
- // 변경사항이 있는 행만 저장
- if (hasRowChanges) {
- dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록
-
- const { success } = await updateFormDataInDB(
- formCode,
- contractItemId,
- dataToSave
- );
-
- if (success) {
- updatedRows.push(dataToSave);
- saveCount++;
- }
- } else {
- updatedRows.push(originalRow); // 변경사항이 없으면 원본 유지
- }
- }
-
- if (saveCount > 0) {
- toast.success(`${saveCount} rows saved successfully!`);
- onUpdateSuccess?.(updatedRows);
- } else {
- toast.info("No changes to save");
- }
- }
-
- setHasChanges(false);
- setValidationErrors([]);
-
- } catch (error) {
- console.error("Error saving changes:", error);
- toast.error("An unexpected error occurred while saving");
- } finally {
- setIsPending(false);
- }
- }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings, columnsJSON, validateAllData]);
-
- if (!isOpen) return null;
-
- // 데이터 유효성 검사
- const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0;
- const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length;
-
- 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>
- <div className="space-y-3">
- {/* 템플릿 선택 */}
- {availableTemplates.length > 1 && (
- <div className="flex items-center gap-4">
- <span className="text-sm font-medium">Template:</span>
- <Select value={selectedTemplateId} onValueChange={handleTemplateChange}>
- <SelectTrigger className="w-64">
- <SelectValue placeholder="Select a template" />
- </SelectTrigger>
- <SelectContent>
- {availableTemplates.map(template => (
- <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}>
- {template.NAME} ({template.TMPL_TYPE})
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
- )}
-
- {/* 템플릿 정보 */}
- {selectedTemplate && (
- <div className="flex items-center gap-4 text-sm">
- <span className="font-medium text-blue-600">
- Template Type: {selectedTemplate.TMPL_TYPE === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : 'Item View (SPREAD_ITEM)'}
- </span>
- {templateType === 'SPREAD_ITEM' && selectedRow && (
- <span>• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span>
- )}
- {templateType === 'SPREAD_LIST' && (
- <span>• {dataCount} rows</span>
- )}
- {hasChanges && (
- <span className="text-orange-600 font-medium">
- • Unsaved changes
- </span>
- )}
- {validationErrors.length > 0 && (
- <span className="text-red-600 font-medium flex items-center">
- <AlertTriangle className="w-4 h-4 mr-1" />
- {validationErrors.length} validation errors
- </span>
- )}
- </div>
- )}
-
- {/* 범례 */}
- <div className="flex items-center gap-4 text-xs">
- <span className="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-muted-foreground">
- <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span>
- Read-only fields
- </span>
- <span className="text-muted-foreground">
- <span className="inline-block w-3 h-3 bg-red-100 border border-red-300 mr-1"></span>
- Validation errors
- </span>
- {cellMappings.length > 0 && (
- <span className="text-blue-600">
- {editableFieldsCount} of {cellMappings.length} fields editable
- </span>
- )}
- </div>
- </div>
- </DialogDescription>
- </DialogHeader>
-
- {/* SpreadSheets 컴포넌트 영역 */}
- <div className="flex-1 overflow-hidden">
- {selectedTemplate && isClient && isDataValid ? (
- <SpreadSheets
- key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${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...
- </>
- ) : !selectedTemplate ? (
- "No template available"
- ) : !isDataValid ? (
- `No ${templateType === 'SPREAD_ITEM' ? 'selected row' : 'data'} available`
- ) : (
- "Template not ready"
- )}
- </div>
- )}
- </div>
-
- <DialogFooter className="flex-shrink-0">
- <div className="flex items-center gap-2">
- <Button variant="outline" onClick={onClose}>
- Close
- </Button>
-
- {hasChanges && (
- <Button
- variant="default"
- onClick={handleSaveChanges}
- disabled={isPending || validationErrors.length > 0}
- >
- {isPending ? (
- <>
- <Loader className="mr-2 h-4 w-4 animate-spin" />
- Saving...
- </>
- ) : (
- <>
- <Save className="mr-2 h-4 w-4" />
- Save Changes
- </>
- )}
- </Button>
- )}
-
- {validationErrors.length > 0 && (
- <Button
- variant="outline"
- onClick={validateAllData}
- className="text-red-600 border-red-300 hover:bg-red-50"
- >
- <AlertTriangle className="mr-2 h-4 w-4" />
- Check Errors ({validationErrors.length})
- </Button>
- )}
- </div>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-} \ No newline at end of file