ERP-node/frontend/lib/meta-components/auto-generate/generateComponents.ts

562 lines
14 KiB
TypeScript

import { AutoGenerateConfig, AutoGenerateColumn } from "./AutoGenerateModal";
// ComponentData 타입 (ScreenDesigner에서 사용하는 것과 동일)
export interface ComponentData {
id: string;
componentType: string;
position: { x: number; y: number; z: number };
size: { width: number; height: number };
layerId: number;
componentConfig: Record<string, any>;
style?: Record<string, any>;
label?: string;
tableName?: string;
columnName?: string;
}
// 컴포넌트 ID 생성
function generateComponentId(): string {
return `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// 시스템 컬럼 목록
const SYSTEM_COLUMNS = [
"created_by",
"updated_by",
"created_date",
"updated_date",
"created_at",
"updated_at",
"writer",
"company_code",
];
// 시스템 컬럼 판별
function isSystemColumn(columnName: string): boolean {
return SYSTEM_COLUMNS.includes(columnName.toLowerCase());
}
// WebType별 기본 높이
function getFieldHeight(webType: string): number {
const heightMap: Record<string, number> = {
textarea: 120,
file: 240,
image: 240,
};
return heightMap[webType] || 40;
}
/**
* 템플릿별 메타 컴포넌트 자동 생성
*/
export function generateComponents(
tableName: string,
tableLabel: string,
config: AutoGenerateConfig,
columns: AutoGenerateColumn[],
startX: number,
startY: number,
): ComponentData[] {
const components: ComponentData[] = [];
// 선택된 컬럼만 필터링
const selectedCols = columns.filter((col) =>
config.selectedColumns.includes(col.columnName),
);
switch (config.templateType) {
case "list":
return generateListTemplate(
tableName,
tableLabel,
config,
selectedCols,
startX,
startY,
);
case "form":
return generateFormTemplate(
tableName,
tableLabel,
config,
selectedCols,
startX,
startY,
);
case "master-detail":
return generateMasterDetailTemplate(
tableName,
tableLabel,
config,
selectedCols,
startX,
startY,
);
case "card":
return generateCardTemplate(
tableName,
tableLabel,
config,
selectedCols,
startX,
startY,
);
default:
return components;
}
}
/**
* 목록형 템플릿
* Search + Action (등록/삭제) + DataView (table) + Modal (form)
*/
function generateListTemplate(
tableName: string,
tableLabel: string,
config: AutoGenerateConfig,
columns: AutoGenerateColumn[],
startX: number,
startY: number,
): ComponentData[] {
const components: ComponentData[] = [];
let currentY = startY;
// ID를 미리 생성 (컴포넌트 간 참조를 위해)
const dataViewId = generateComponentId();
const createModalId = generateComponentId();
// 1. Search 컴포넌트 (조건: includeSearch)
if (config.includeSearch) {
components.push({
id: generateComponentId(),
componentType: "meta-search",
position: { x: startX, y: currentY, z: 1 },
size: { width: 800, height: 60 },
layerId: 1,
componentConfig: {
mode: "combined",
tableName,
fields: columns.slice(0, 3).map((col) => ({
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
})),
},
style: { labelDisplay: true },
label: "검색",
});
currentY += 70;
}
// 2. Action 그룹 (등록/삭제 버튼) (조건: includeCrud)
if (config.includeCrud) {
components.push({
id: generateComponentId(),
componentType: "meta-action",
position: { x: startX, y: currentY, z: 1 },
size: { width: 200, height: 40 },
layerId: 1,
componentConfig: {
label: "등록",
variant: "default",
steps: [
{
type: "open_modal",
targetId: createModalId,
},
],
},
style: { labelDisplay: false },
label: "등록 버튼",
});
components.push({
id: generateComponentId(),
componentType: "meta-action",
position: { x: startX + 110, y: currentY, z: 1 },
size: { width: 200, height: 40 },
layerId: 1,
componentConfig: {
label: "삭제",
variant: "destructive",
steps: [
{
type: "api_call",
method: "DELETE",
url: `/api/screen-data/${tableName}/{id}`,
},
{
type: "refresh_component",
targetId: dataViewId,
},
],
},
style: { labelDisplay: false },
label: "삭제 버튼",
});
currentY += 50;
}
// 3. DataView 컴포넌트 (테이블 뷰)
components.push({
id: dataViewId,
componentType: "meta-dataview",
position: { x: startX, y: currentY, z: 1 },
size: { width: 900, height: 400 },
layerId: 1,
componentConfig: {
viewMode: "table",
tableName,
columns: columns.map((col) => ({
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
})),
pagination: true,
selectable: true,
},
style: { labelDisplay: true },
label: tableLabel,
});
currentY += 410;
// 4. Modal 컴포넌트 (등록/수정 폼) (조건: includeModal)
if (config.includeModal) {
const formFields = columns.map((col) => ({
id: generateComponentId(),
componentType: "meta-field",
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
required: col.required,
}));
components.push({
id: createModalId,
componentType: "meta-modal",
position: { x: startX, y: currentY, z: 1 },
size: { width: 600, height: 400 },
layerId: 1,
componentConfig: {
trigger: "button",
title: `${tableLabel} 등록`,
formFields,
submitAction: {
type: "api_call",
method: "POST",
url: `/api/screen-data/${tableName}`,
},
successAction: {
type: "refresh_component",
targetId: dataViewId, // 위에서 생성한 DataView ID 참조
},
},
style: { labelDisplay: false },
label: "등록 모달",
});
}
return components;
}
/**
* 폼형 템플릿
* Field 컴포넌트들 (2열 그리드) + Action (저장/취소)
*/
function generateFormTemplate(
tableName: string,
tableLabel: string,
config: AutoGenerateConfig,
columns: AutoGenerateColumn[],
startX: number,
startY: number,
): ComponentData[] {
const components: ComponentData[] = [];
let currentY = startY;
let currentX = startX;
const columnWidth = 350;
const gapX = 20;
const gapY = 10;
// Field 컴포넌트 2열로 배치
columns.forEach((col, index) => {
const fieldHeight = getFieldHeight(col.webType);
const isLeftColumn = index % 2 === 0;
if (!isLeftColumn) {
currentX = startX + columnWidth + gapX;
} else {
currentX = startX;
if (index > 0) {
currentY += Math.max(
getFieldHeight(columns[index - 2]?.webType || "text"),
getFieldHeight(columns[index - 1]?.webType || "text"),
) + gapY;
}
}
components.push({
id: generateComponentId(),
componentType: "meta-field",
position: { x: currentX, y: currentY, z: 1 },
size: { width: columnWidth, height: fieldHeight },
layerId: 1,
componentConfig: {
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
required: col.required,
tableName,
},
style: { labelDisplay: true },
label: col.label || col.columnName,
tableName,
columnName: col.columnName,
});
});
// 마지막 행 높이 계산
const lastRowHeight =
columns.length % 2 === 0
? Math.max(
getFieldHeight(columns[columns.length - 2]?.webType || "text"),
getFieldHeight(columns[columns.length - 1]?.webType || "text"),
)
: getFieldHeight(columns[columns.length - 1]?.webType || "text");
currentY += lastRowHeight + 20;
// Action 버튼들 (조건: includeCrud)
if (config.includeCrud) {
components.push({
id: generateComponentId(),
componentType: "meta-action",
position: { x: startX, y: currentY, z: 1 },
size: { width: 100, height: 40 },
layerId: 1,
componentConfig: {
label: "저장",
variant: "default",
steps: [
{
type: "api_call",
method: "POST",
url: `/api/screen-data/${tableName}`,
},
],
},
style: { labelDisplay: false },
label: "저장 버튼",
});
components.push({
id: generateComponentId(),
componentType: "meta-action",
position: { x: startX + 110, y: currentY, z: 1 },
size: { width: 100, height: 40 },
layerId: 1,
componentConfig: {
label: "취소",
variant: "outline",
steps: [
{
type: "reset_form",
},
],
},
style: { labelDisplay: false },
label: "취소 버튼",
});
}
return components;
}
/**
* 마스터-디테일 템플릿
* 좌측: Search + DataView (table)
* 우측: Field 컴포넌트들 + Action (저장)
*/
function generateMasterDetailTemplate(
tableName: string,
tableLabel: string,
config: AutoGenerateConfig,
columns: AutoGenerateColumn[],
startX: number,
startY: number,
): ComponentData[] {
const components: ComponentData[] = [];
const masterWidth = 450;
const detailX = startX + masterWidth + 30;
let masterY = startY;
let detailY = startY;
// === 좌측 (Master) ===
// 1. Search
if (config.includeSearch) {
components.push({
id: generateComponentId(),
componentType: "meta-search",
position: { x: startX, y: masterY, z: 1 },
size: { width: masterWidth, height: 60 },
layerId: 1,
componentConfig: {
mode: "simple",
tableName,
fields: columns.slice(0, 2).map((col) => ({
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
})),
},
style: { labelDisplay: true },
label: "검색",
});
masterY += 70;
}
// 2. DataView (Master)
const masterDataViewId = generateComponentId();
components.push({
id: masterDataViewId,
componentType: "meta-dataview",
position: { x: startX, y: masterY, z: 1 },
size: { width: masterWidth, height: 500 },
layerId: 1,
componentConfig: {
viewMode: "table",
tableName,
columns: columns.slice(0, 4).map((col) => ({
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
})),
pagination: true,
selectable: "single",
},
style: { labelDisplay: true },
label: tableLabel,
});
// === 우측 (Detail) ===
// Field 컴포넌트들 (단일 열)
columns.forEach((col, index) => {
const fieldHeight = getFieldHeight(col.webType);
components.push({
id: generateComponentId(),
componentType: "meta-field",
position: { x: detailX, y: detailY, z: 1 },
size: { width: 400, height: fieldHeight },
layerId: 1,
componentConfig: {
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
required: col.required,
tableName,
},
style: { labelDisplay: true },
label: col.label || col.columnName,
tableName,
columnName: col.columnName,
});
detailY += fieldHeight + 10;
});
// Action 버튼 (저장)
components.push({
id: generateComponentId(),
componentType: "meta-action",
position: { x: detailX, y: detailY, z: 1 },
size: { width: 100, height: 40 },
layerId: 1,
componentConfig: {
label: "저장",
variant: "default",
steps: [
{
type: "api_call",
method: "PUT",
url: `/api/screen-data/${tableName}/{id}`,
},
{
type: "refresh_component",
targetId: masterDataViewId, // 동적 ID 참조
},
],
},
style: { labelDisplay: false },
label: "저장 버튼",
});
return components;
}
/**
* 카드형 템플릿
* Search + DataView (card)
*/
function generateCardTemplate(
tableName: string,
tableLabel: string,
config: AutoGenerateConfig,
columns: AutoGenerateColumn[],
startX: number,
startY: number,
): ComponentData[] {
const components: ComponentData[] = [];
let currentY = startY;
// 1. Search (간단)
if (config.includeSearch) {
components.push({
id: generateComponentId(),
componentType: "meta-search",
position: { x: startX, y: currentY, z: 1 },
size: { width: 800, height: 60 },
layerId: 1,
componentConfig: {
mode: "simple",
tableName,
fields: columns.slice(0, 2).map((col) => ({
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
})),
},
style: { labelDisplay: true },
label: "검색",
});
currentY += 70;
}
// 2. DataView (카드 뷰)
components.push({
id: generateComponentId(),
componentType: "meta-dataview",
position: { x: startX, y: currentY, z: 1 },
size: { width: 900, height: 500 },
layerId: 1,
componentConfig: {
viewMode: "card",
tableName,
columns: columns.map((col) => ({
columnName: col.columnName,
label: col.label || col.columnName,
webType: col.webType,
})),
pagination: true,
},
style: { labelDisplay: true },
label: tableLabel,
});
return components;
}