ERP-node/frontend/lib/schemas/componentConfig.ts

309 lines
8.6 KiB
TypeScript
Raw Normal View History

/**
*
*
*
* - 기본값: defaultConfig에서
* - 커스텀: DB custom_config에서
* - = + ( )
*/
import { z } from "zod";
// ============================================
// 공통 스키마 (모든 구조 허용)
// ============================================
export const customConfigSchema = z.record(z.any());
export type CustomConfig = z.infer<typeof customConfigSchema>;
// ============================================
// 깊은 병합 함수
// ============================================
export function deepMerge<T extends Record<string, any>>(
target: T,
source: Record<string, any>
): T {
const result = { ...target };
for (const key of Object.keys(source)) {
const sourceValue = source[key];
const targetValue = result[key as keyof T];
// 둘 다 객체이고 배열이 아니면 깊은 병합
if (
isPlainObject(sourceValue) &&
isPlainObject(targetValue)
) {
result[key as keyof T] = deepMerge(targetValue, sourceValue);
} else if (sourceValue !== undefined) {
// source 값이 있으면 덮어쓰기
result[key as keyof T] = sourceValue;
}
}
return result;
}
function isPlainObject(value: unknown): value is Record<string, any> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
// ============================================
// 설정 병합 함수 (렌더링 시 사용)
// ============================================
export function mergeComponentConfig(
defaultConfig: Record<string, any>,
customConfig: Record<string, any> | null | undefined
): Record<string, any> {
if (!customConfig || Object.keys(customConfig).length === 0) {
return { ...defaultConfig };
}
return deepMerge(defaultConfig, customConfig);
}
// ============================================
// 커스텀 설정 추출 함수 (저장 시 사용)
// ============================================
export function extractCustomConfig(
fullConfig: Record<string, any>,
defaultConfig: Record<string, any>
): Record<string, any> {
const customConfig: Record<string, any> = {};
for (const key of Object.keys(fullConfig)) {
const fullValue = fullConfig[key];
const defaultValue = defaultConfig[key];
// 기본값과 다른 경우만 커스텀으로 추출
if (!isDeepEqual(fullValue, defaultValue)) {
customConfig[key] = fullValue;
}
}
return customConfig;
}
// ============================================
// 깊은 비교 함수
// ============================================
export function isDeepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (a == null || b == null) return a === b;
if (typeof a !== typeof b) return false;
if (typeof a !== "object") return a === b;
if (Array.isArray(a) !== Array.isArray(b)) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!isDeepEqual(a[i], b[i])) return false;
}
return true;
}
const objA = a as Record<string, unknown>;
const objB = b as Record<string, unknown>;
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!keysB.includes(key)) return false;
if (!isDeepEqual(objA[key], objB[key])) return false;
}
return true;
}
// ============================================
// 컴포넌트 URL 생성 함수
// ============================================
export function getComponentUrl(componentType: string): string {
return `@/lib/registry/components/${componentType}`;
}
// ============================================
// 컴포넌트 타입 추출 함수 (URL에서)
// ============================================
export function getComponentTypeFromUrl(componentUrl: string): string {
// "@/lib/registry/components/split-panel-layout" → "split-panel-layout"
const parts = componentUrl.split("/");
return parts[parts.length - 1];
}
// ============================================
// V2 레이아웃 스키마
// ============================================
export const componentV2Schema = z.object({
id: z.string(),
url: z.string(),
position: z.object({
x: z.number().default(0),
y: z.number().default(0),
}),
size: z.object({
width: z.number().default(100),
height: z.number().default(100),
}),
displayOrder: z.number().default(0),
overrides: z.record(z.any()).default({}),
});
export const layoutV2Schema = z.object({
version: z.string().default("2.0"),
components: z.array(componentV2Schema).default([]),
updatedAt: z.string().optional(),
});
export type ComponentV2 = z.infer<typeof componentV2Schema>;
export type LayoutV2 = z.infer<typeof layoutV2Schema>;
// ============================================
// 컴포넌트별 기본값 레지스트리
// ============================================
const componentDefaultsRegistry: Record<string, Record<string, any>> = {
"table-list": {
pagination: true,
pageSize: 20,
selectable: true,
showHeader: true,
},
"button-primary": {
label: "버튼",
variant: "default",
size: "default",
},
"text-input": {
placeholder: "",
multiline: false,
},
"select-basic": {
placeholder: "선택하세요",
options: [],
},
"date-input": {
format: "YYYY-MM-DD",
},
"split-panel-layout": {
splitRatio: 50,
direction: "horizontal",
resizable: true,
},
"tabs-widget": {
tabs: [],
defaultTab: 0,
},
"card-display": {
title: "",
bordered: true,
},
"flow-widget": {
flowId: null,
},
"category-management": {
categoryType: "",
},
"pivot-table": {
rows: [],
columns: [],
values: [],
},
"unified-grid": {
columns: [],
},
"checkbox-basic": {
label: "",
defaultChecked: false,
},
"radio-basic": {
options: [],
},
"file-upload": {
accept: "*",
multiple: false,
},
"repeat-container": {
children: [],
},
};
// ============================================
// 컴포넌트 기본값 조회
// ============================================
export function getComponentDefaults(componentType: string): Record<string, any> {
return componentDefaultsRegistry[componentType] || {};
}
// ============================================
// URL에서 기본값 조회
// ============================================
export function getDefaultsByUrl(url: string): Record<string, any> {
const componentType = getComponentTypeFromUrl(url);
return getComponentDefaults(componentType);
}
// ============================================
// V2 컴포넌트 로드 (기본값 + overrides 병합)
// ============================================
export function loadComponentV2(component: ComponentV2): ComponentV2 & { config: Record<string, any> } {
const defaults = getDefaultsByUrl(component.url);
const config = mergeComponentConfig(defaults, component.overrides);
return {
...component,
config,
};
}
// ============================================
// V2 컴포넌트 저장 (차이값 추출)
// ============================================
export function saveComponentV2(
component: ComponentV2 & { config?: Record<string, any> }
): ComponentV2 {
const defaults = getDefaultsByUrl(component.url);
const overrides = component.config
? extractCustomConfig(component.config, defaults)
: component.overrides;
return {
id: component.id,
url: component.url,
position: component.position,
size: component.size,
displayOrder: component.displayOrder,
overrides,
};
}
// ============================================
// V2 레이아웃 로드 (전체 컴포넌트 기본값 병합)
// ============================================
export function loadLayoutV2(layoutData: any): LayoutV2 & { components: Array<ComponentV2 & { config: Record<string, any> }> } {
const parsed = layoutV2Schema.parse(layoutData || { version: "2.0", components: [] });
return {
...parsed,
components: parsed.components.map(loadComponentV2),
};
}
// ============================================
// V2 레이아웃 저장 (전체 컴포넌트 차이값 추출)
// ============================================
export function saveLayoutV2(
components: Array<ComponentV2 & { config?: Record<string, any> }>
): LayoutV2 {
return {
version: "2.0",
components: components.map(saveComponentV2),
updatedAt: new Date().toISOString(),
};
}