632 lines
17 KiB
TypeScript
632 lines
17 KiB
TypeScript
/**
|
|
* V2 레이아웃 → V3 메타 컴포넌트 자동 변환 유틸리티
|
|
* - 기존 70개 컴포넌트를 7개 메타 컴포넌트로 통합 변환
|
|
* - 변환 불가 컴포넌트는 그대로 유지 (skipped)
|
|
*/
|
|
|
|
import type {
|
|
LayoutData,
|
|
MetaComponent,
|
|
FieldComponentConfig,
|
|
DataViewComponentConfig,
|
|
ActionComponentConfig,
|
|
LayoutComponentConfig,
|
|
DisplayComponentConfig,
|
|
SearchComponentConfig,
|
|
ModalComponentConfig,
|
|
ActionStep,
|
|
} from "../api/metaComponent";
|
|
|
|
// ============================================
|
|
// 인터페이스 정의
|
|
// ============================================
|
|
|
|
interface MigrationResult {
|
|
success: boolean;
|
|
convertedCount: number;
|
|
skippedCount: number;
|
|
skippedComponents: string[]; // 변환 불가 컴포넌트 ID 목록
|
|
}
|
|
|
|
interface V2Component {
|
|
id: string;
|
|
componentType: string;
|
|
componentConfig?: any;
|
|
position?: { x: number; y: number; width: number; height: number };
|
|
layerId?: number;
|
|
style?: any;
|
|
[key: string]: any;
|
|
}
|
|
|
|
interface V2LayoutData {
|
|
version: string;
|
|
screenId: number;
|
|
layerId: number;
|
|
components: V2Component[];
|
|
layers?: any[];
|
|
metadata?: any;
|
|
}
|
|
|
|
// ============================================
|
|
// 변환 불가 컴포넌트 목록 (별도 유지)
|
|
// ============================================
|
|
|
|
const SKIP_COMPONENTS = new Set([
|
|
// 특수 시각화 컴포넌트
|
|
"rack-structure",
|
|
"v2-rack-structure",
|
|
"v2-location-swap-selector",
|
|
"location-swap-selector",
|
|
"map",
|
|
"mail-recipient-selector",
|
|
|
|
// 복합 특수 컴포넌트 (별도 처리 필요)
|
|
"v2-bom-item-editor",
|
|
// category-manager는 Field로 변환 가능하므로 제거
|
|
"v2-category-manager",
|
|
"customer-item-mapping",
|
|
"tax-invoice-list",
|
|
"v2-process-work-standard",
|
|
"v2-item-routing",
|
|
]);
|
|
|
|
// ============================================
|
|
// 컴포넌트 타입 판별 함수
|
|
// ============================================
|
|
|
|
const FIELD_TYPES = new Set([
|
|
"text-input",
|
|
"v2-input",
|
|
"number-input",
|
|
"date-input",
|
|
"v2-date",
|
|
"select-basic",
|
|
"v2-select",
|
|
"checkbox-basic",
|
|
"radio-basic",
|
|
"textarea-basic",
|
|
"file-upload",
|
|
"v2-file-upload",
|
|
"entity-search-input",
|
|
"autocomplete-search-input",
|
|
"slider-basic",
|
|
"toggle-switch",
|
|
"numbering-rule",
|
|
"v2-numbering-rule",
|
|
"image-widget",
|
|
"category-manager", // 카테고리 선택기 추가
|
|
]);
|
|
|
|
const DATAVIEW_TYPES = new Set([
|
|
"table-list",
|
|
"v2-table-list",
|
|
"v2-table-grouped",
|
|
"v2-repeater",
|
|
"simple-repeater-table",
|
|
// modal-repeater-table은 MODAL_TYPES로 이동
|
|
"pivot-grid",
|
|
"v2-pivot-grid",
|
|
"v2-bom-tree",
|
|
"aggregation-widget",
|
|
"v2-aggregation-widget",
|
|
"v2-timeline-scheduler",
|
|
]);
|
|
|
|
const ACTION_TYPES = new Set([
|
|
"button-primary",
|
|
"v2-button-primary",
|
|
"flow-widget",
|
|
"related-data-buttons",
|
|
]);
|
|
|
|
const LAYOUT_TYPES = new Set([
|
|
"split-panel-layout",
|
|
"split-panel-layout2", // 추가
|
|
"v2-split-panel-layout",
|
|
"screen-split-panel",
|
|
"tabs",
|
|
"tabs-widget", // 추가
|
|
"v2-tabs-widget",
|
|
"section-card",
|
|
"v2-section-card",
|
|
"section-paper",
|
|
"v2-section-paper",
|
|
"conditional-container",
|
|
"accordion-basic",
|
|
"repeat-container",
|
|
"v2-repeat-container",
|
|
]);
|
|
|
|
const DISPLAY_TYPES = new Set([
|
|
"text-display",
|
|
"v2-text-display",
|
|
"divider-line",
|
|
"v2-divider-line",
|
|
"v2-split-line",
|
|
"badge",
|
|
"alert",
|
|
"stats-card",
|
|
"card-display", // DataView에서 이동
|
|
"v2-card-display",
|
|
"chart",
|
|
"image-display",
|
|
"progress-bar",
|
|
"v2-media",
|
|
]);
|
|
|
|
const SEARCH_TYPES = new Set([
|
|
"table-search-widget",
|
|
"v2-table-search-widget",
|
|
"search-panel",
|
|
]);
|
|
|
|
const MODAL_TYPES = new Set([
|
|
"modal-repeater-table",
|
|
"universal-form-modal",
|
|
"repeat-screen-modal",
|
|
"v2-modal",
|
|
]);
|
|
|
|
// ============================================
|
|
// 변환 함수들
|
|
// ============================================
|
|
|
|
/**
|
|
* Field로 변환
|
|
*/
|
|
function convertToField(v2Comp: V2Component): MetaComponent {
|
|
const config = v2Comp.componentConfig || {};
|
|
const properties = v2Comp.properties || {};
|
|
|
|
// widgetType → webType 매핑 (V2 properties에서 추출)
|
|
const widgetTypeToWebType: Record<string, string> = {
|
|
text: "text",
|
|
direct: "text",
|
|
entity: "entity",
|
|
category: "category",
|
|
date: "date",
|
|
number: "number",
|
|
image: "file",
|
|
file: "file",
|
|
button: "text",
|
|
checkbox: "checkbox",
|
|
radio: "radio",
|
|
toggle: "toggle",
|
|
textarea: "textarea",
|
|
};
|
|
|
|
// webType 결정 (우선순위: properties.widgetType → config.widgetType → componentType에서 추론)
|
|
let webType = "text";
|
|
const propsWidgetType = properties.widgetType || config.widgetType;
|
|
if (propsWidgetType && widgetTypeToWebType[propsWidgetType]) {
|
|
webType = widgetTypeToWebType[propsWidgetType];
|
|
} else {
|
|
// componentType에서 추론
|
|
switch (v2Comp.componentType) {
|
|
case "number-input":
|
|
webType = "number";
|
|
break;
|
|
case "date-input":
|
|
case "v2-date":
|
|
webType = config.includeTime ? "datetime" : "date";
|
|
break;
|
|
case "select-basic":
|
|
case "v2-select":
|
|
webType = "select";
|
|
break;
|
|
case "checkbox-basic":
|
|
webType = "checkbox";
|
|
break;
|
|
case "radio-basic":
|
|
webType = "radio";
|
|
break;
|
|
case "textarea-basic":
|
|
webType = "textarea";
|
|
break;
|
|
case "file-upload":
|
|
case "v2-file-upload":
|
|
webType = "file";
|
|
break;
|
|
case "entity-search-input":
|
|
case "autocomplete-search-input":
|
|
webType = "entity";
|
|
break;
|
|
case "slider-basic":
|
|
webType = "slider";
|
|
break;
|
|
case "toggle-switch":
|
|
webType = "toggle";
|
|
break;
|
|
case "numbering-rule":
|
|
case "v2-numbering-rule":
|
|
webType = "numbering";
|
|
break;
|
|
case "image-widget":
|
|
webType = "file";
|
|
break;
|
|
case "category-manager":
|
|
webType = "category";
|
|
break;
|
|
case "v2-input":
|
|
webType = config.inputType || "text";
|
|
break;
|
|
}
|
|
}
|
|
|
|
// columnName: 최상위 속성 → config 속성 순서로 탐색
|
|
const columnName = v2Comp.columnName || config.columnName || config.dataKey || "";
|
|
|
|
const fieldConfig: FieldComponentConfig = {
|
|
webType,
|
|
label: config.label || config.fieldLabel || v2Comp.label || "필드",
|
|
binding: columnName,
|
|
tableName: config.tableName || v2Comp.tableName,
|
|
placeholder: config.placeholder,
|
|
required: config.required ?? v2Comp.required ?? false,
|
|
defaultValue: config.defaultValue,
|
|
validation: config.validation,
|
|
join: config.joinConfig || config.join || v2Comp.joinConfig,
|
|
options: config.options,
|
|
};
|
|
|
|
// V2 확장 속성 추가 (타입 안전성을 위해 as any로 확장)
|
|
const extendedConfig: any = {
|
|
...fieldConfig,
|
|
autoFill: v2Comp.autoFill || config.autoFill || properties.autoFill,
|
|
fileConfig: v2Comp.fileConfig || config.fileConfig,
|
|
categoryGroupCode: config.categoryGroupCode || v2Comp.categoryGroupCode || properties.categoryGroupCode,
|
|
_originalConfig: config,
|
|
_originalType: v2Comp.componentType,
|
|
};
|
|
|
|
return {
|
|
id: v2Comp.id,
|
|
type: "meta-field",
|
|
position: v2Comp.position,
|
|
config: extendedConfig,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* DataView로 변환
|
|
*/
|
|
function convertToDataView(v2Comp: V2Component): MetaComponent {
|
|
const config = v2Comp.componentConfig || {};
|
|
|
|
// viewMode 결정
|
|
let viewMode: "table" | "card" | "list" | "tree" = "table";
|
|
if (v2Comp.componentType.includes("card")) {
|
|
viewMode = "card";
|
|
} else if (v2Comp.componentType.includes("tree")) {
|
|
viewMode = "tree";
|
|
}
|
|
|
|
const dataViewConfig: DataViewComponentConfig = {
|
|
viewMode,
|
|
tableName: config.tableName || config.dataSource || v2Comp.tableName || "",
|
|
columns: config.columns || config.visibleColumns || [],
|
|
defaultSort: config.defaultSort,
|
|
pageSize: config.pageSize || 20,
|
|
actions: {
|
|
create: config.allowCreate ?? true,
|
|
read: config.allowRead ?? true,
|
|
update: config.allowUpdate ?? true,
|
|
delete: config.allowDelete ?? true,
|
|
},
|
|
};
|
|
|
|
return {
|
|
id: v2Comp.id,
|
|
type: "meta-dataview",
|
|
position: v2Comp.position,
|
|
config: {
|
|
...dataViewConfig,
|
|
_originalConfig: config,
|
|
_originalType: v2Comp.componentType,
|
|
} as any,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Action으로 변환
|
|
*/
|
|
function convertToAction(v2Comp: V2Component): MetaComponent {
|
|
const config = v2Comp.componentConfig || {};
|
|
|
|
const steps: ActionStep[] = [];
|
|
|
|
// 기존 actionType에 따라 steps 구성
|
|
if (config.actionType === "save" || v2Comp.componentType.includes("save")) {
|
|
steps.push({
|
|
type: "save",
|
|
target: config.targetTable || config.tableName || "",
|
|
});
|
|
}
|
|
|
|
if (config.actionType === "delete") {
|
|
steps.push({
|
|
type: "delete",
|
|
target: config.targetTable || config.tableName || "",
|
|
});
|
|
}
|
|
|
|
// flow-widget인 경우
|
|
if (v2Comp.componentType === "flow-widget" && config.flowDefinitionId) {
|
|
steps.push({
|
|
type: "api",
|
|
method: "POST",
|
|
endpoint: `/api/flow/move/${config.flowDefinitionId}`,
|
|
body: { stepId: config.targetStepId },
|
|
});
|
|
}
|
|
|
|
// related-data-buttons인 경우
|
|
if (v2Comp.componentType === "related-data-buttons" && config.targetScreenId) {
|
|
steps.push({
|
|
type: "navigate",
|
|
screenId: config.targetScreenId,
|
|
});
|
|
}
|
|
|
|
// 기본 저장 액션이 없으면 추가
|
|
if (steps.length === 0) {
|
|
steps.push({
|
|
type: "save",
|
|
target: config.tableName || "",
|
|
});
|
|
}
|
|
|
|
const actionConfig: ActionComponentConfig = {
|
|
label: config.label || config.buttonLabel || "버튼",
|
|
buttonType: config.variant || "primary",
|
|
icon: config.icon,
|
|
steps,
|
|
confirmDialog: config.confirmDialog,
|
|
};
|
|
|
|
return {
|
|
id: v2Comp.id,
|
|
type: "meta-action",
|
|
position: v2Comp.position,
|
|
config: {
|
|
...actionConfig,
|
|
_originalConfig: config,
|
|
_originalType: v2Comp.componentType,
|
|
} as any,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Layout으로 변환
|
|
*/
|
|
function convertToLayout(v2Comp: V2Component): MetaComponent {
|
|
const config = v2Comp.componentConfig || {};
|
|
|
|
// mode 결정
|
|
let mode: "columns" | "rows" | "tabs" | "accordion" | "card" = "columns";
|
|
if (v2Comp.componentType.includes("tab")) {
|
|
mode = "tabs";
|
|
} else if (v2Comp.componentType.includes("accordion")) {
|
|
mode = "accordion";
|
|
} else if (v2Comp.componentType.includes("card") || v2Comp.componentType.includes("paper")) {
|
|
mode = "card";
|
|
} else if (v2Comp.componentType.includes("split-panel")) {
|
|
mode = "columns";
|
|
}
|
|
|
|
const layoutConfig: LayoutComponentConfig = {
|
|
mode,
|
|
areas: config.areas || config.panels,
|
|
tabs: config.tabs, // tabs-widget의 탭 정보 보존
|
|
gap: config.gap,
|
|
padding: config.padding,
|
|
bordered: config.bordered,
|
|
title: config.title || config.sectionTitle,
|
|
};
|
|
|
|
return {
|
|
id: v2Comp.id,
|
|
type: "meta-layout",
|
|
position: v2Comp.position,
|
|
config: {
|
|
...layoutConfig,
|
|
children: config.children, // 자식 컴포넌트 ID 배열
|
|
_originalConfig: config,
|
|
_originalType: v2Comp.componentType,
|
|
} as any,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Display로 변환
|
|
*/
|
|
function convertToDisplay(v2Comp: V2Component): MetaComponent {
|
|
const config = v2Comp.componentConfig || {};
|
|
|
|
// displayType 결정
|
|
let displayType: "text" | "heading" | "divider" | "badge" | "alert" | "stat" | "spacer" | "progress" = "text";
|
|
if (v2Comp.componentType.includes("divider") || v2Comp.componentType.includes("split-line")) {
|
|
displayType = "divider";
|
|
} else if (v2Comp.componentType === "badge") {
|
|
displayType = "badge";
|
|
} else if (v2Comp.componentType === "alert") {
|
|
displayType = "alert";
|
|
} else if (v2Comp.componentType.includes("stats") || v2Comp.componentType.includes("card-display")) {
|
|
displayType = "stat";
|
|
} else if (v2Comp.componentType.includes("progress")) {
|
|
displayType = "progress";
|
|
}
|
|
|
|
const displayConfig: DisplayComponentConfig = {
|
|
displayType,
|
|
text: config.text
|
|
? {
|
|
content: config.text,
|
|
size: config.size,
|
|
weight: config.weight,
|
|
align: config.align,
|
|
}
|
|
: undefined,
|
|
dataBinding: config.dataBinding || config.binding,
|
|
};
|
|
|
|
return {
|
|
id: v2Comp.id,
|
|
type: "meta-display",
|
|
position: v2Comp.position,
|
|
config: {
|
|
...displayConfig,
|
|
_originalConfig: config,
|
|
_originalType: v2Comp.componentType,
|
|
} as any,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Search로 변환 (table-search-widget 등)
|
|
*/
|
|
function convertToSearch(v2Comp: V2Component): MetaComponent {
|
|
const config = v2Comp.componentConfig || {};
|
|
|
|
// V2 검색 위젯에서 필드 추출
|
|
const searchFields = (config.searchFields || config.fields || []).map((f: any) => ({
|
|
columnName: f.columnName || f.field || f.key,
|
|
label: f.label || f.title || f.columnName,
|
|
searchType: f.type === "select" ? "select" : f.type === "date" ? "date_range" : "text",
|
|
options: f.options,
|
|
}));
|
|
|
|
return {
|
|
id: v2Comp.id,
|
|
type: "meta-search",
|
|
position: v2Comp.position,
|
|
config: {
|
|
targetDataView: config.targetTableId || config.linkedTableId || "",
|
|
mode: "simple",
|
|
fields: searchFields,
|
|
_originalConfig: config,
|
|
_originalType: v2Comp.componentType,
|
|
} as any, // SearchComponentConfig + 확장 속성
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Modal로 변환 (modal-repeater-table, universal-form-modal 등)
|
|
*/
|
|
function convertToModal(v2Comp: V2Component): MetaComponent {
|
|
const config = v2Comp.componentConfig || {};
|
|
|
|
return {
|
|
id: v2Comp.id,
|
|
type: "meta-modal",
|
|
position: v2Comp.position,
|
|
config: {
|
|
trigger: "button",
|
|
triggerLabel: config.triggerButton || config.buttonLabel || "모달 열기",
|
|
content: {
|
|
type: config.screenId ? "screen" : "form",
|
|
formConfig: config.tableName ? {
|
|
tableName: config.tableName,
|
|
mode: "edit",
|
|
columns: config.columns || [],
|
|
layout: "single",
|
|
} : undefined,
|
|
screenId: config.screenId,
|
|
},
|
|
size: config.size || "md",
|
|
_originalConfig: config,
|
|
_originalType: v2Comp.componentType,
|
|
} as any, // ModalComponentConfig + 확장 속성
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// 메인 마이그레이션 함수
|
|
// ============================================
|
|
|
|
/**
|
|
* V2 레이아웃을 V3 메타 컴포넌트 형태로 변환
|
|
* @param layoutData V2 레이아웃 데이터
|
|
* @returns 변환된 V3 레이아웃 + 변환 결과
|
|
*/
|
|
export function migrateTo3_0(layoutData: V2LayoutData): {
|
|
newLayoutData: LayoutData;
|
|
result: MigrationResult;
|
|
} {
|
|
const convertedComponents: MetaComponent[] = [];
|
|
const skippedComponents: string[] = [];
|
|
let convertedCount = 0;
|
|
let skippedCount = 0;
|
|
|
|
for (const comp of layoutData.components) {
|
|
const compType = comp.componentType;
|
|
|
|
// 변환 불가 컴포넌트는 그대로 유지
|
|
if (SKIP_COMPONENTS.has(compType)) {
|
|
convertedComponents.push(comp as any); // 그대로 추가 (V2 그대로)
|
|
skippedComponents.push(comp.id);
|
|
skippedCount++;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
// 컴포넌트 타입에 따라 변환
|
|
if (FIELD_TYPES.has(compType)) {
|
|
convertedComponents.push(convertToField(comp));
|
|
convertedCount++;
|
|
} else if (DATAVIEW_TYPES.has(compType)) {
|
|
convertedComponents.push(convertToDataView(comp));
|
|
convertedCount++;
|
|
} else if (ACTION_TYPES.has(compType)) {
|
|
convertedComponents.push(convertToAction(comp));
|
|
convertedCount++;
|
|
} else if (LAYOUT_TYPES.has(compType)) {
|
|
convertedComponents.push(convertToLayout(comp));
|
|
convertedCount++;
|
|
} else if (DISPLAY_TYPES.has(compType)) {
|
|
convertedComponents.push(convertToDisplay(comp));
|
|
convertedCount++;
|
|
} else if (SEARCH_TYPES.has(compType)) {
|
|
convertedComponents.push(convertToSearch(comp));
|
|
convertedCount++;
|
|
} else if (MODAL_TYPES.has(compType)) {
|
|
convertedComponents.push(convertToModal(comp));
|
|
convertedCount++;
|
|
} else {
|
|
// 알 수 없는 컴포넌트는 그대로 유지
|
|
convertedComponents.push(comp as any);
|
|
skippedComponents.push(comp.id);
|
|
skippedCount++;
|
|
}
|
|
} catch (error) {
|
|
// 변환 실패 시 원본 유지
|
|
console.error(`컴포넌트 변환 실패 (${comp.id}):`, error);
|
|
convertedComponents.push(comp as any);
|
|
skippedComponents.push(comp.id);
|
|
skippedCount++;
|
|
}
|
|
}
|
|
|
|
const newLayoutData: LayoutData = {
|
|
version: "3.0",
|
|
screenId: layoutData.screenId,
|
|
layerId: layoutData.layerId,
|
|
components: convertedComponents,
|
|
layers: layoutData.layers,
|
|
metadata: {
|
|
...layoutData.metadata,
|
|
lastModified: new Date().toISOString(),
|
|
description: `V2에서 V3로 자동 마이그레이션됨 (${convertedCount}개 변환, ${skippedCount}개 유지)`,
|
|
},
|
|
};
|
|
|
|
const result: MigrationResult = {
|
|
success: true,
|
|
convertedCount,
|
|
skippedCount,
|
|
skippedComponents,
|
|
};
|
|
|
|
return { newLayoutData, result };
|
|
}
|