562 lines
14 KiB
TypeScript
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;
|
|
}
|