/** * 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[]; 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); } // 결과 객체 초기화 (원본 복사) let 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 || !v2Layout.components) { return null; } const components: LegacyComponentData[] = v2Layout.components.map((comp) => { 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; }), }; } // 🆕 overrides에서 상위 레벨 속성들 추출 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, codeCategory: overrides.codeCategory, inputType: overrides.inputType, webType: overrides.webType, // 🆕 autoFill 설정 복원 (자동 입력 기능) autoFill: overrides.autoFill, // 기존 구조 호환을 위한 추가 필드 style: {}, parentId: null, gridColumns: 12, gridRowIndex: 0, }; }); 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 components: ComponentV2[] = legacyLayout.components.map((comp, index) => { // 컴포넌트 타입 결정 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; if (comp.label) topLevelProps.label = comp.label; if (comp.required !== undefined) topLevelProps.required = comp.required; if (comp.readonly !== undefined) topLevelProps.readonly = comp.readonly; 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; // 현재 설정에서 차이값만 추출 const fullConfig = comp.componentConfig || {}; const configOverrides = extractCustomConfig(fullConfig, defaults); // 상위 레벨 속성과 componentConfig 병합 const overrides = { ...topLevelProps, ...configOverrides }; return { id: comp.id, url: url, position: comp.position || { x: 0, y: 0 }, size: comp.size || { width: 100, height: 100 }, displayOrder: index, overrides: overrides, }; }); return { version: "2.0", components, }; } // ============================================ // V2 레이아웃 유효성 검사 // ============================================ export function isValidV2Layout(data: any): data is LayoutV2 { return data && typeof data === "object" && data.version === "2.0" && Array.isArray(data.components); } // ============================================ // 기존 레이아웃인지 확인 // ============================================ export function isLegacyLayout(data: any): boolean { return data && typeof data === "object" && Array.isArray(data.components) && data.version !== "2.0"; }