596 lines
19 KiB
TypeScript
596 lines
19 KiB
TypeScript
/**
|
|
* 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<string, any>;
|
|
position: { x: number; y: number; width: number; height: number };
|
|
size?: { width: number; height: number };
|
|
label?: string;
|
|
columnName?: string;
|
|
widgetType?: string;
|
|
style?: Record<string, any>;
|
|
[key: string]: any;
|
|
}
|
|
|
|
// ========================================
|
|
// webType → V2 componentType 매핑
|
|
// ========================================
|
|
|
|
const FIELD_WEBTYPE_TO_V2: Record<string, { componentType: string; extraConfig?: Record<string, any> }> = {
|
|
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<string, any> = {
|
|
...(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<string, string> = {
|
|
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<string, any> = {
|
|
...(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<string, any> = {
|
|
...(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<string, string> = {
|
|
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<string, any> = {
|
|
...(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<string, string> = {
|
|
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<string, any> = {
|
|
...(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<string, any> = {
|
|
...(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<string, any> = {
|
|
...(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<string, any>
|
|
): 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-");
|
|
}
|