/** * V3 메타 컴포넌트 → V2 ComponentData 변환기 * * V3 메타 컴포넌트는 "설정 추상화 레이어"이며, 실제 렌더링은 * 기존 V2 DynamicComponentRenderer에 위임한다. * 이 변환기가 메타 설정을 V2가 이해하는 ComponentData로 변환한다. */ import type { FieldComponentConfig, DataViewComponentConfig, ActionComponentConfig, LayoutComponentConfig, DisplayComponentConfig, SearchComponentConfig, ModalComponentConfig, } from "../api/metaComponent"; interface MetaComponentInput { id: string; type: string; config: any; position?: { x: number; y: number; width: number; height: number }; } interface V2ComponentOutput { id: string; type: "component" | "widget"; componentType: string; componentConfig: Record; position: { x: number; y: number; width: number; height: number }; size?: { width: number; height: number }; label?: string; columnName?: string; widgetType?: string; style?: Record; [key: string]: any; } // ======================================== // webType → V2 componentType 매핑 // ======================================== const FIELD_WEBTYPE_TO_V2: Record }> = { text: { componentType: "v2-input", extraConfig: { inputType: "text" } }, number: { componentType: "v2-input", extraConfig: { inputType: "number" } }, email: { componentType: "v2-input", extraConfig: { inputType: "email" } }, tel: { componentType: "v2-input", extraConfig: { inputType: "tel" } }, url: { componentType: "v2-input", extraConfig: { inputType: "url" } }, password: { componentType: "v2-input", extraConfig: { inputType: "password" } }, textarea: { componentType: "v2-input", extraConfig: { inputType: "textarea" } }, date: { componentType: "v2-date", extraConfig: { includeTime: false } }, datetime: { componentType: "v2-date", extraConfig: { includeTime: true } }, select: { componentType: "v2-select" }, entity: { componentType: "entity-search-input" }, checkbox: { componentType: "v2-select", extraConfig: { mode: "checkbox" } }, radio: { componentType: "v2-select", extraConfig: { mode: "radio" } }, toggle: { componentType: "v2-select", extraConfig: { mode: "toggle" } }, file: { componentType: "v2-file-upload" }, slider: { componentType: "v2-input", extraConfig: { inputType: "slider" } }, numbering: { componentType: "v2-numbering-rule" }, code: { componentType: "v2-input", extraConfig: { inputType: "text" } }, color: { componentType: "v2-input", extraConfig: { inputType: "color" } }, }; // ======================================== // 변환 함수들 // ======================================== function transformField(meta: MetaComponentInput): V2ComponentOutput { const config = meta.config as FieldComponentConfig; const webType = config.webType || "text"; const mapping = FIELD_WEBTYPE_TO_V2[webType] || FIELD_WEBTYPE_TO_V2["text"]; // 원본 V2 config가 보존되어 있으면 그것을 기반으로 사용 (정보 손실 방지) const originalConfig = (config as any)._originalConfig; const originalType = (config as any)._originalType; // 원본이 있으면 원본 컴포넌트 타입을 그대로 사용 const resolvedComponentType = originalType || mapping.componentType; const v2Config: Record = { ...(originalConfig || {}), ...(mapping.extraConfig || {}), label: config.label, placeholder: config.placeholder, required: config.required, readonly: config.readonly, disabled: config.disabled, defaultValue: config.defaultValue, columnName: config.binding, }; // 엔티티 조인 설정 if (config.join && webType === "entity") { v2Config.tableName = config.join.targetTable; v2Config.joinColumn = config.join.targetColumn; v2Config.displayColumn = config.join.displayColumn; v2Config.additionalColumns = config.join.additionalColumns; v2Config.searchable = config.join.searchable ?? true; if (config.join.filterBy) { v2Config.filterBy = config.join.filterBy; } } // 셀렉트 옵션 설정 if (config.options) { v2Config.source = config.options.source; if (config.options.source === "code_table") { v2Config.codeCategory = config.options.codeCategory; } else if (config.options.source === "static") { v2Config.staticOptions = config.options.staticList; } else if (config.options.source === "api") { v2Config.apiEndpoint = config.options.apiEndpoint; } } // 검증 규칙 if (config.validation) { v2Config.validation = config.validation; } // 테이블명 (화면 레벨 테이블) if (config.tableName) { v2Config.tableName = v2Config.tableName || config.tableName; } const position = meta.position || { x: 0, y: 0, width: 400, height: 40 }; return { id: meta.id, type: "component", componentType: resolvedComponentType, componentConfig: v2Config, position, size: { width: position.width, height: position.height }, label: config.label || config.binding || "필드", columnName: config.binding, widgetType: webType, style: { labelDisplay: !!config.label, labelText: config.label, labelPosition: config.display?.labelPosition || "top", }, }; } function transformDataView(meta: MetaComponentInput): V2ComponentOutput { const config = meta.config as DataViewComponentConfig; const viewMode = config.viewMode || "table"; const originalConfig = (config as any)._originalConfig; const originalType = (config as any)._originalType; const componentTypeMap: Record = { table: "v2-table-list", card: "v2-card-display", tree: "v2-bom-tree", pivot: "v2-pivot-grid", timeline: "v2-timeline-scheduler", list: "v2-table-list", kanban: "v2-table-list", calendar: "v2-table-list", }; const resolvedComponentType = originalType || componentTypeMap[viewMode] || "v2-table-list"; const v2Config: Record = { ...(originalConfig || {}), tableName: config.tableName, columns: config.columns, defaultSort: config.defaultSort, pageSize: config.pageSize || 20, }; // CRUD 액션 설정 if (config.actions) { v2Config.allowCreate = config.actions.create; v2Config.allowRead = config.actions.read; v2Config.allowUpdate = config.actions.update; v2Config.allowDelete = config.actions.delete; } // 테이블 뷰 상세 설정 if (config.tableConfig) { v2Config.showRowNumber = config.tableConfig.showRowNumber; v2Config.showCheckbox = config.tableConfig.showCheckbox; v2Config.stickyHeader = config.tableConfig.stickyHeader; v2Config.groupBy = config.tableConfig.groupBy; v2Config.summaryColumns = config.tableConfig.summaryColumns; v2Config.editableColumns = config.tableConfig.editableColumns; v2Config.frozenColumns = config.tableConfig.frozenColumns; } // 카드 뷰 설정 if (config.cardConfig) { v2Config.titleColumn = config.cardConfig.titleColumn; v2Config.descriptionColumn = config.cardConfig.descriptionColumn; v2Config.imageColumn = config.cardConfig.imageColumn; v2Config.columnsPerRow = config.cardConfig.columnsPerRow; v2Config.cardStyle = config.cardConfig.cardStyle; } // 검색 설정 if (config.searchable) { v2Config.searchable = true; v2Config.filterColumns = config.filterColumns; } // 마스터-디테일 연결 if (config.dataSource?.masterField) { v2Config.masterField = config.dataSource.masterField; v2Config.detailForeignKey = config.dataSource.detailForeignKey; } const position = meta.position || { x: 0, y: 0, width: 800, height: 400 }; return { id: meta.id, type: "component", componentType: resolvedComponentType, componentConfig: v2Config, position, size: { width: position.width, height: position.height }, label: config.tableName || "데이터 뷰", }; } function transformAction(meta: MetaComponentInput): V2ComponentOutput { const config = meta.config as ActionComponentConfig; const originalConfig = (config as any)._originalConfig; const originalType = (config as any)._originalType; const v2Config: Record = { ...(originalConfig || {}), label: config.label || "버튼", variant: config.buttonType || "default", icon: config.icon, }; // 액션 스텝들을 V2 버튼 설정으로 변환 if (config.steps && config.steps.length > 0) { const firstStep = config.steps[0]; switch (firstStep.type) { case "save": v2Config.actionType = "save"; v2Config.targetTable = firstStep.target; break; case "delete": v2Config.actionType = "delete"; v2Config.targetTable = firstStep.target; break; case "refresh": v2Config.actionType = "refresh"; break; case "navigate": v2Config.actionType = "navigate"; v2Config.targetScreenId = (firstStep as any).screenId; break; case "openModal": v2Config.actionType = "modal"; v2Config.modalId = (firstStep as any).modalId; break; case "api": v2Config.actionType = "api"; v2Config.method = (firstStep as any).method; v2Config.endpoint = (firstStep as any).endpoint; v2Config.body = (firstStep as any).body; break; case "flowMove": v2Config.actionType = "flow"; v2Config.flowDefinitionId = (firstStep as any).flowId; v2Config.targetStepId = (firstStep as any).stepId; break; case "export": v2Config.actionType = "export"; v2Config.exportFormat = (firstStep as any).format; break; } // 멀티 스텝인 경우 전체 스텝도 보존 if (config.steps.length > 1) { v2Config.actionSteps = config.steps; } } // 실행 조건 if (config.enableCondition) { v2Config.enableCondition = config.enableCondition; } // 확인 다이얼로그 if (config.confirmDialog) { v2Config.confirmDialog = config.confirmDialog; } const position = meta.position || { x: 0, y: 0, width: 120, height: 40 }; return { id: meta.id, type: "component", componentType: originalType || "v2-button-primary", componentConfig: v2Config, position, size: { width: position.width, height: position.height }, label: config.label || "버튼", }; } function transformLayout(meta: MetaComponentInput): V2ComponentOutput { const config = meta.config as LayoutComponentConfig; const mode = config.mode || "columns"; const originalConfig = (config as any)._originalConfig; const originalType = (config as any)._originalType; const componentTypeMap: Record = { columns: "v2-split-panel-layout", rows: "v2-repeat-container", tabs: "v2-tabs-widget", accordion: "v2-section-card", card: "v2-section-card", conditional: "conditional-container", }; const resolvedComponentType = originalType || componentTypeMap[mode] || "v2-section-card"; const v2Config: Record = { ...(originalConfig || {}), gap: config.gap, padding: config.padding, bordered: config.bordered, title: config.title, }; // 영역 설정 if (config.areas) { v2Config.areas = config.areas; v2Config.panels = config.areas; } // 탭 설정 if (config.tabs) { v2Config.tabs = config.tabs; } // 조건부 설정 if (config.conditions) { v2Config.conditions = config.conditions; } const position = meta.position || { x: 0, y: 0, width: 800, height: 600 }; return { id: meta.id, type: "component", componentType: resolvedComponentType, componentConfig: v2Config, position, size: { width: position.width, height: position.height }, label: config.title || `레이아웃 (${mode})`, }; } function transformDisplay(meta: MetaComponentInput): V2ComponentOutput { const config = meta.config as DisplayComponentConfig; const displayType = config.displayType || "text"; const originalConfig = (config as any)._originalConfig; const originalType = (config as any)._originalType; const componentTypeMap: Record = { text: "v2-text-display", heading: "v2-text-display", divider: "v2-divider-line", badge: "v2-text-display", alert: "v2-text-display", stat: "v2-text-display", chart: "v2-text-display", image: "v2-media", progress: "v2-text-display", spacer: "v2-divider-line", html: "v2-text-display", }; // 원본이 있으면 원본 컴포넌트 타입을 그대로 사용 const resolvedComponentType = originalType || componentTypeMap[displayType] || "v2-text-display"; const v2Config: Record = { ...(originalConfig || {}), displayType, }; if (config.text) { v2Config.text = config.text.content; v2Config.size = config.text.size; v2Config.weight = config.text.weight; v2Config.align = config.text.align; v2Config.color = config.text.color; } if (config.dataBinding) { v2Config.dataBinding = config.dataBinding; } const position = meta.position || { x: 0, y: 0, width: 400, height: 40 }; return { id: meta.id, type: "component", componentType: resolvedComponentType, componentConfig: v2Config, position, size: { width: position.width, height: position.height }, label: config.text?.content || `표시 (${displayType})`, }; } function transformSearch(meta: MetaComponentInput): V2ComponentOutput { const config = meta.config as SearchComponentConfig; const originalConfig = (config as any)._originalConfig; const originalType = (config as any)._originalType; // 원본이 있으면 원본 컴포넌트 타입을 그대로 사용 const resolvedComponentType = originalType || "v2-table-search-widget"; const v2Config: Record = { ...(originalConfig || {}), mode: config.mode || "simple", targetDataView: config.targetDataView, fields: config.fields, quickFilters: config.quickFilters, }; const position = meta.position || { x: 0, y: 0, width: 800, height: 60 }; return { id: meta.id, type: "component", componentType: resolvedComponentType, componentConfig: v2Config, position, size: { width: position.width, height: position.height }, label: "검색", }; } function transformModal(meta: MetaComponentInput): V2ComponentOutput { const config = meta.config as ModalComponentConfig; const originalConfig = (config as any)._originalConfig; const originalType = (config as any)._originalType; // 원본이 있으면 원본 컴포넌트 타입을 그대로 사용 const resolvedComponentType = originalType || "v2-button-primary"; const v2Config: Record = { ...(originalConfig || {}), trigger: config.trigger || "button", size: config.size || "md", triggerLabel: config.triggerLabel, }; if (config.content) { v2Config.contentType = config.content.type; if (config.content.formConfig) { v2Config.formConfig = config.content.formConfig; } if (config.content.screenId) { v2Config.screenId = config.content.screenId; } if (config.content.passData) { v2Config.passData = config.content.passData; } } if (config.onClose) { v2Config.onCloseActions = config.onClose; } const position = meta.position || { x: 0, y: 0, width: 120, height: 40 }; return { id: meta.id, type: "component", componentType: resolvedComponentType, componentConfig: { ...v2Config, actionType: "modal", label: config.triggerLabel || "모달 열기", variant: "outline", }, position, size: { width: position.width, height: position.height }, label: config.triggerLabel || "모달", }; } // ======================================== // 메인 변환 함수 // ======================================== /** * V3 메타 컴포넌트를 V2 ComponentData로 변환 * 이 결과물을 DynamicComponentRenderer에 전달하면 기존 V2 파이프라인으로 렌더링된다. */ export function metaToV2(meta: MetaComponentInput): V2ComponentOutput { switch (meta.type) { case "meta-field": return transformField(meta); case "meta-dataview": return transformDataView(meta); case "meta-action": return transformAction(meta); case "meta-layout": return transformLayout(meta); case "meta-display": return transformDisplay(meta); case "meta-search": return transformSearch(meta); case "meta-modal": return transformModal(meta); default: return { id: meta.id, type: "component", componentType: "v2-text-display", componentConfig: { text: `알 수 없는 메타 타입: ${meta.type}` }, position: meta.position || { x: 0, y: 0, width: 200, height: 40 }, label: `Unknown: ${meta.type}`, }; } } /** * V3 메타 컴포넌트를 V2로 변환하되, 원본 V2 컴포넌트의 속성을 보존 * (position, size, style, layerId 등 레이아웃 관련 속성 유지) */ export function metaToV2WithOriginal( meta: MetaComponentInput, originalV2?: Record ): V2ComponentOutput { const transformed = metaToV2(meta); if (!originalV2) return transformed; return { ...originalV2, ...transformed, // 원본의 레이아웃 속성 보존 position: meta.position || originalV2.position || transformed.position, size: originalV2.size || transformed.size, style: originalV2.style || transformed.style, layerId: originalV2.layerId, gridColumnSpan: originalV2.gridColumnSpan, gridColumnStart: originalV2.gridColumnStart, gridRowIndex: originalV2.gridRowIndex, zoneId: originalV2.zoneId, parentId: originalV2.parentId, className: originalV2.className, responsiveConfig: originalV2.responsiveConfig, conditional: originalV2.conditional, autoFill: originalV2.autoFill, }; } /** * 레이아웃의 모든 메타 컴포넌트를 V2로 일괄 변환 */ export function transformAllMetaToV2( components: any[], originalComponents?: any[] ): any[] { return components.map((comp) => { const compType = comp.componentType || comp.type; if (typeof compType === "string" && compType.startsWith("meta-")) { const metaInput: MetaComponentInput = { id: comp.id, type: compType, config: comp.componentConfig || comp.config || {}, position: comp.position, }; const original = originalComponents?.find((o: any) => o.id === comp.id); return metaToV2WithOriginal(metaInput, original); } return comp; }); } /** * 컴포넌트가 메타 컴포넌트인지 확인 */ export function isMetaComponent(component: any): boolean { const type = component?.componentType || component?.type; return typeof type === "string" && type.startsWith("meta-"); }