/** * V2 레이아웃 변환 유틸리티 * * 기존 LayoutData ↔ V2 LayoutData 변환 */ import { ComponentV2, LayoutV2, getComponentUrl, getComponentTypeFromUrl, getDefaultsByUrl, mergeComponentConfig, extractCustomConfig, } from "@/lib/schemas/componentConfig"; // 기존 ComponentData 타입 (간략화) interface LegacyComponentData { id: string; componentType?: string; widgetType?: string; type?: string; position?: { x: number; y: number }; size?: { width: number; height: number }; componentConfig?: Record; [key: string]: any; } interface LegacyLayoutData { components: LegacyComponentData[]; layers?: any[]; // 레이어 시스템 gridSettings?: any; screenResolution?: any; metadata?: any; } // ============================================ // 중첩 컴포넌트 기본값 적용 헬퍼 함수 (재귀적) // ============================================ function applyDefaultsToNestedComponents(components: any[]): any[] { if (!Array.isArray(components)) return components; return components.map((nestedComp: any) => { if (!nestedComp) return nestedComp; // 중첩 컴포넌트의 타입 확인 (componentType 또는 url에서 추출) let nestedComponentType = nestedComp.componentType; if (!nestedComponentType && nestedComp.url) { nestedComponentType = getComponentTypeFromUrl(nestedComp.url); } // 결과 객체 초기화 (원본 복사) const result = { ...nestedComp }; // 🆕 탭 위젯인 경우 재귀적으로 탭 내부 컴포넌트도 처리 if (nestedComponentType === "v2-tabs-widget") { const config = result.componentConfig || {}; if (config.tabs && Array.isArray(config.tabs)) { result.componentConfig = { ...config, tabs: config.tabs.map((tab: any) => { if (tab?.components && Array.isArray(tab.components)) { return { ...tab, components: applyDefaultsToNestedComponents(tab.components), }; } return tab; }), }; } } // 🆕 분할 패널인 경우 재귀적으로 내부 컴포넌트도 처리 if (nestedComponentType === "v2-split-panel-layout") { const config = result.componentConfig || {}; result.componentConfig = { ...config, leftPanel: config.leftPanel ? { ...config.leftPanel, components: applyDefaultsToNestedComponents(config.leftPanel.components || []), } : config.leftPanel, rightPanel: config.rightPanel ? { ...config.rightPanel, components: applyDefaultsToNestedComponents(config.rightPanel.components || []), } : config.rightPanel, }; } // 컴포넌트 타입이 없으면 그대로 반환 if (!nestedComponentType) { return result; } // 중첩 컴포넌트의 기본값 가져오기 const nestedDefaults = getDefaultsByUrl(`registry://${nestedComponentType}`); // componentConfig가 있으면 기본값과 병합 if (result.componentConfig && Object.keys(nestedDefaults).length > 0) { const mergedNestedConfig = mergeComponentConfig(nestedDefaults, result.componentConfig); return { ...result, componentConfig: mergedNestedConfig, }; } return result; }); } // ============================================ // 분할 패널 내부 컴포넌트 기본값 적용 // ============================================ function applyDefaultsToSplitPanelComponents(mergedConfig: Record): Record { const result = { ...mergedConfig }; // leftPanel.components 처리 if (result.leftPanel?.components) { result.leftPanel = { ...result.leftPanel, components: applyDefaultsToNestedComponents(result.leftPanel.components), }; } // rightPanel.components 처리 if (result.rightPanel?.components) { result.rightPanel = { ...result.rightPanel, components: applyDefaultsToNestedComponents(result.rightPanel.components), }; } return result; } // ============================================ // V2 → Legacy 변환 (로드 시) // ============================================ export function convertV2ToLegacy(v2Layout: LayoutV2 | null): LegacyLayoutData | null { if (!v2Layout) { return null; } // V2 컴포넌트를 Legacy 컴포넌트로 변환하는 함수 (레이어 내 컴포넌트에도 재사용) const convertV2Component = (comp: ComponentV2, layerId?: string): LegacyComponentData => { const componentType = getComponentTypeFromUrl(comp.url); const defaults = getDefaultsByUrl(comp.url); let mergedConfig = mergeComponentConfig(defaults, comp.overrides); // 분할 패널인 경우 내부 컴포넌트에도 기본값 적용 if (componentType === "v2-split-panel-layout") { mergedConfig = applyDefaultsToSplitPanelComponents(mergedConfig); } // 탭 위젯인 경우 탭 내부 컴포넌트에도 기본값 적용 if (componentType === "v2-tabs-widget" && mergedConfig.tabs) { mergedConfig = { ...mergedConfig, tabs: mergedConfig.tabs.map((tab: any) => { if (tab?.components) { return { ...tab, components: applyDefaultsToNestedComponents(tab.components), }; } return tab; }), }; } const overrides = comp.overrides || {}; return { id: comp.id, componentType: componentType, widgetType: componentType, type: "component", position: comp.position, size: comp.size, componentConfig: mergedConfig, // 상위 레벨 속성 복원 tableName: overrides.tableName, columnName: overrides.columnName, label: overrides.label || mergedConfig.label || "", required: overrides.required, readonly: overrides.readonly, hidden: overrides.hidden, codeCategory: overrides.codeCategory, inputType: overrides.inputType, webType: overrides.webType, autoFill: overrides.autoFill, style: overrides.style || {}, webTypeConfig: overrides.webTypeConfig || {}, parentId: null, gridColumns: 12, gridRowIndex: 0, // 🆕 레이어 ID 복원 ...(layerId ? { layerId } : {}), }; }; // 🆕 레이어 구조가 있는 경우 (v2.1) const v2Layers = (v2Layout as any).layers; if (v2Layers && Array.isArray(v2Layers) && v2Layers.length > 0) { // 모든 레이어의 컴포넌트를 평탄화하여 layout.components에 저장 (layerId 포함) const allComponents: LegacyComponentData[] = []; const legacyLayers = v2Layers.map((layer: any) => { const layerComponents = (layer.components || []).map((comp: any) => convertV2Component(comp, layer.id) ); allComponents.push(...layerComponents); return { ...layer, // layer.components는 legacy 변환 시 빈 배열로 (layout.components + layerId 방식 사용) components: [], }; }); return { components: allComponents, layers: legacyLayers, gridSettings: v2Layout.gridSettings || { enabled: true, size: 20, color: "#d1d5db", opacity: 0.5, snapToGrid: true, columns: 12, gap: 16, padding: 16, }, screenResolution: v2Layout.screenResolution || { width: 1920, height: 1080 }, }; } // 레이어 없는 기존 방식 if (!v2Layout.components) return null; const components: LegacyComponentData[] = v2Layout.components.map((comp) => convertV2Component(comp)); return { components, gridSettings: v2Layout.gridSettings || { enabled: true, size: 20, color: "#d1d5db", opacity: 0.5, snapToGrid: true, columns: 12, gap: 16, padding: 16, }, screenResolution: v2Layout.screenResolution || { width: 1920, height: 1080 }, }; } // ============================================ // Legacy → V2 변환 (저장 시) // ============================================ export function convertLegacyToV2(legacyLayout: LegacyLayoutData): LayoutV2 { // 컴포넌트 변환 함수 (레이어 내 컴포넌트 변환에도 재사용) const convertComponent = (comp: LegacyComponentData, index: number): ComponentV2 => { // 컴포넌트 타입 결정 const componentType = comp.componentType || comp.widgetType || comp.type || "unknown"; const url = getComponentUrl(componentType); // 기본값 가져오기 const defaults = getDefaultsByUrl(url); // 🆕 컴포넌트 상위 레벨 속성들도 포함 (tableName, columnName 등) const topLevelProps: Record = {}; if (comp.tableName) topLevelProps.tableName = comp.tableName; if (comp.columnName) topLevelProps.columnName = comp.columnName; // 🔧 label은 빈 문자열도 저장 (라벨 삭제 지원) if (comp.label !== undefined) topLevelProps.label = comp.label; if (comp.required !== undefined) topLevelProps.required = comp.required; if (comp.readonly !== undefined) topLevelProps.readonly = comp.readonly; if (comp.hidden !== undefined) topLevelProps.hidden = comp.hidden; // 🆕 숨김 설정 저장 if (comp.codeCategory) topLevelProps.codeCategory = comp.codeCategory; if (comp.inputType) topLevelProps.inputType = comp.inputType; if (comp.webType) topLevelProps.webType = comp.webType; // 🆕 autoFill 설정 저장 (자동 입력 기능) if (comp.autoFill) topLevelProps.autoFill = comp.autoFill; // 🆕 style 설정 저장 (라벨 텍스트, 라벨 스타일 등) if (comp.style && Object.keys(comp.style).length > 0) topLevelProps.style = comp.style; // 🔧 webTypeConfig 저장 (버튼 제어기능, 플로우 가시성 등) if (comp.webTypeConfig && Object.keys(comp.webTypeConfig).length > 0) { topLevelProps.webTypeConfig = comp.webTypeConfig; // 🔍 디버그: webTypeConfig 저장 확인 if (comp.webTypeConfig.dataflowConfig || comp.webTypeConfig.enableDataflowControl) { console.log("💾 webTypeConfig 저장:", { componentId: comp.id, enableDataflowControl: comp.webTypeConfig.enableDataflowControl, dataflowConfig: comp.webTypeConfig.dataflowConfig, }); } } // 현재 설정에서 차이값만 추출 const fullConfig = comp.componentConfig || {}; const configOverrides = extractCustomConfig(fullConfig, defaults); // 🔧 디버그: style 저장 확인 (주석 처리) // if (comp.style?.labelDisplay !== undefined || configOverrides.style?.labelDisplay !== undefined) { console.log("💾 저장 시 style 변환:", { componentId: comp.id, "comp.style": comp.style, "configOverrides.style": configOverrides.style, "topLevelProps.style": topLevelProps.style }); } // 상위 레벨 속성과 componentConfig 병합 // 🔧 style은 양쪽을 병합하되 comp.style(topLevelProps.style)을 우선시 const mergedStyle = { ...(configOverrides.style || {}), ...(topLevelProps.style || {}), }; // 🔧 webTypeConfig도 병합 (topLevelProps가 우선, dataflowConfig 등 보존) const mergedWebTypeConfig = { ...(configOverrides.webTypeConfig || {}), ...(topLevelProps.webTypeConfig || {}), }; const overrides = { ...topLevelProps, ...configOverrides, // 🆕 병합된 style 사용 (comp.style 값이 최종 우선) ...(Object.keys(mergedStyle).length > 0 ? { style: mergedStyle } : {}), // 🆕 병합된 webTypeConfig 사용 (comp.webTypeConfig가 최종 우선) ...(Object.keys(mergedWebTypeConfig).length > 0 ? { webTypeConfig: mergedWebTypeConfig } : {}), }; return { id: comp.id, url: url, position: comp.position || { x: 0, y: 0 }, size: comp.size || { width: 100, height: 100 }, displayOrder: index, overrides: overrides, }; }; // 🆕 레이어 정보 변환 (layers가 있으면 레이어 구조로 저장) const legacyLayers = (legacyLayout as any).layers; if (legacyLayers && Array.isArray(legacyLayers) && legacyLayers.length > 0) { const v2Layers = legacyLayers.map((layer: any) => ({ ...layer, // 레이어 내 컴포넌트를 V2 형식으로 변환 components: (layer.components || []).map((comp: any, idx: number) => convertComponent(comp, idx)), })); return { version: "2.1", layers: v2Layers, components: [], // 레이어 구조 사용 시 상위 components는 빈 배열 gridSettings: legacyLayout.gridSettings, screenResolution: legacyLayout.screenResolution, metadata: legacyLayout.metadata, }; } // 레이어 없으면 기존 방식 (컴포넌트만) const components = legacyLayout.components.map((comp, index) => convertComponent(comp, index)); return { version: "2.0", components, gridSettings: legacyLayout.gridSettings, screenResolution: legacyLayout.screenResolution, metadata: legacyLayout.metadata, }; } // ============================================ // V2 레이아웃 유효성 검사 // ============================================ export function isValidV2Layout(data: any): data is LayoutV2 { if (!data || typeof data !== "object") return false; // v2.0: components 기반, v2.1: layers 기반 const isV2 = data.version === "2.0" && Array.isArray(data.components); const isV21 = data.version === "2.1" && Array.isArray(data.layers); return isV2 || isV21; } // ============================================ // 기존 레이아웃인지 확인 // ============================================ export function isLegacyLayout(data: any): boolean { return data && typeof data === "object" && Array.isArray(data.components) && data.version !== "2.0"; }