- 새로운 V2Media 컴포넌트를 추가하여 파일, 이미지, 비디오, 오디오 등 다양한 미디어 타입을 지원합니다. - V2Media의 설정 스키마와 기본 속성을 정의하고, 관련 설정 패널을 통합하였습니다. - 기존 컴포넌트 목록에 V2Media를 포함시켜 통합 미디어 기능을 강화하였습니다. - componentConfig 스키마에서 v2-repeater를 제거하여 불필요한 항목을 정리하였습니다.
975 lines
29 KiB
TypeScript
975 lines
29 KiB
TypeScript
/**
|
|
* V2/V2 컴포넌트 설정 스키마 및 병합 유틸리티
|
|
*
|
|
* V2 컴포넌트와 V2 컴포넌트의 overrides 스키마 및 기본값을 관리
|
|
* - 저장: component_url + overrides (차이값만)
|
|
* - 로드: 코드 기본값 + overrides 병합 (Zod)
|
|
*/
|
|
import { z } from "zod";
|
|
|
|
// ============================================
|
|
// 공통 스키마 (모든 구조 허용)
|
|
// ============================================
|
|
export const customConfigSchema = z.record(z.string(), 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/v2-table-list" → "v2-table-list"
|
|
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.string(), z.any()).default({}),
|
|
});
|
|
|
|
export const layoutV2Schema = z.object({
|
|
version: z.string().default("2.0"),
|
|
components: z.array(componentV2Schema).default([]),
|
|
updatedAt: z.string().optional(),
|
|
screenResolution: z
|
|
.object({
|
|
width: z.number().default(1920),
|
|
height: z.number().default(1080),
|
|
})
|
|
.optional(),
|
|
gridSettings: z.any().optional(),
|
|
});
|
|
|
|
export type ComponentV2 = z.infer<typeof componentV2Schema>;
|
|
export type LayoutV2 = z.infer<typeof layoutV2Schema>;
|
|
|
|
// ============================================
|
|
// V2 컴포넌트 overrides 스키마 정의
|
|
// ============================================
|
|
|
|
// v2-table-list
|
|
const v2TableListOverridesSchema = z
|
|
.object({
|
|
displayMode: z.enum(["table", "card"]).default("table"),
|
|
showHeader: z.boolean().default(true),
|
|
showFooter: z.boolean().default(true),
|
|
height: z.string().default("auto"),
|
|
checkbox: z
|
|
.object({
|
|
enabled: z.boolean().default(true),
|
|
multiple: z.boolean().default(true),
|
|
position: z.string().default("left"),
|
|
selectAll: z.boolean().default(true),
|
|
})
|
|
.default({ enabled: true, multiple: true, position: "left", selectAll: true }),
|
|
columns: z.array(z.any()).default([]),
|
|
autoWidth: z.boolean().default(true),
|
|
stickyHeader: z.boolean().default(false),
|
|
pagination: z
|
|
.object({
|
|
enabled: z.boolean().default(true),
|
|
pageSize: z.number().default(20),
|
|
showSizeSelector: z.boolean().default(true),
|
|
showPageInfo: z.boolean().default(true),
|
|
})
|
|
.default({ enabled: true, pageSize: 20, showSizeSelector: true, showPageInfo: true }),
|
|
autoLoad: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-button-primary
|
|
const v2ButtonPrimaryOverridesSchema = z
|
|
.object({
|
|
text: z.string().default("저장"),
|
|
actionType: z.string().default("button"),
|
|
variant: z.string().default("primary"),
|
|
action: z
|
|
.object({
|
|
type: z.string().default("save"),
|
|
successMessage: z.string().optional(),
|
|
errorMessage: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-text-display
|
|
const v2TextDisplayOverridesSchema = z
|
|
.object({
|
|
text: z.string().default("텍스트를 입력하세요"),
|
|
fontSize: z.string().default("14px"),
|
|
fontWeight: z.string().default("normal"),
|
|
color: z.string().default("#212121"),
|
|
textAlign: z.string().default("left"),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-split-panel-layout
|
|
const v2SplitPanelLayoutOverridesSchema = z
|
|
.object({
|
|
leftPanel: z
|
|
.object({
|
|
title: z.string().default("마스터"),
|
|
showSearch: z.boolean().default(false),
|
|
showAdd: z.boolean().default(false),
|
|
})
|
|
.default({ title: "마스터", showSearch: false, showAdd: false }),
|
|
rightPanel: z
|
|
.object({
|
|
title: z.string().default("디테일"),
|
|
showSearch: z.boolean().default(false),
|
|
showAdd: z.boolean().default(false),
|
|
})
|
|
.default({ title: "디테일", showSearch: false, showAdd: false }),
|
|
splitRatio: z.number().default(30),
|
|
resizable: z.boolean().default(true),
|
|
autoLoad: z.boolean().default(true),
|
|
syncSelection: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-section-card
|
|
const v2SectionCardOverridesSchema = z
|
|
.object({
|
|
title: z.string().default("섹션 제목"),
|
|
description: z.string().default(""),
|
|
showHeader: z.boolean().default(true),
|
|
padding: z.string().default("md"),
|
|
backgroundColor: z.string().default("default"),
|
|
borderStyle: z.string().default("solid"),
|
|
collapsible: z.boolean().default(false),
|
|
defaultOpen: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-section-paper
|
|
const v2SectionPaperOverridesSchema = z
|
|
.object({
|
|
backgroundColor: z.string().default("default"),
|
|
padding: z.string().default("md"),
|
|
roundedCorners: z.string().default("md"),
|
|
shadow: z.string().default("none"),
|
|
showBorder: z.boolean().default(false),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-divider-line
|
|
const v2DividerLineOverridesSchema = z
|
|
.object({
|
|
placeholder: z.string().default("텍스트를 입력하세요"),
|
|
maxLength: z.number().default(255),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-repeat-container
|
|
const v2RepeatContainerOverridesSchema = z
|
|
.object({
|
|
dataSourceType: z.string().default("manual"),
|
|
layout: z.string().default("vertical"),
|
|
gridColumns: z.number().default(2),
|
|
gap: z.string().default("16px"),
|
|
showBorder: z.boolean().default(true),
|
|
showShadow: z.boolean().default(false),
|
|
emptyMessage: z.string().default("데이터가 없습니다"),
|
|
usePaging: z.boolean().default(false),
|
|
pageSize: z.number().default(10),
|
|
clickable: z.boolean().default(false),
|
|
selectionMode: z.string().default("single"),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-rack-structure
|
|
const v2RackStructureOverridesSchema = z
|
|
.object({
|
|
showPreview: z.boolean().default(true),
|
|
showTemplate: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-numbering-rule
|
|
const v2NumberingRuleOverridesSchema = z
|
|
.object({
|
|
showPreview: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-category-manager
|
|
const v2CategoryManagerOverridesSchema = z
|
|
.object({
|
|
viewMode: z.string().default("tree"),
|
|
maxDepth: z.number().default(3),
|
|
showActions: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-pivot-grid
|
|
const v2PivotGridOverridesSchema = z
|
|
.object({
|
|
fields: z.array(z.any()).default([]),
|
|
dataSource: z.any().optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-location-swap-selector
|
|
const v2LocationSwapSelectorOverridesSchema = z
|
|
.object({
|
|
dataSource: z
|
|
.object({
|
|
type: z.string().default("static"),
|
|
tableName: z.string().default(""),
|
|
valueField: z.string().default("location_code"),
|
|
labelField: z.string().default("location_name"),
|
|
})
|
|
.default({ type: "static", tableName: "", valueField: "location_code", labelField: "location_name" }),
|
|
departureField: z.string().default("departure"),
|
|
destinationField: z.string().default("destination"),
|
|
departureLabel: z.string().default("출발지"),
|
|
destinationLabel: z.string().default("도착지"),
|
|
showSwapButton: z.boolean().default(true),
|
|
variant: z.string().default("card"),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-aggregation-widget
|
|
const v2AggregationWidgetOverridesSchema = z
|
|
.object({
|
|
dataSourceType: z.string().default("table"),
|
|
items: z.array(z.any()).default([]),
|
|
filters: z.array(z.any()).default([]),
|
|
filterLogic: z.string().default("AND"),
|
|
layout: z.string().default("horizontal"),
|
|
showLabels: z.boolean().default(true),
|
|
showIcons: z.boolean().default(true),
|
|
gap: z.string().default("16px"),
|
|
autoRefresh: z.boolean().default(false),
|
|
refreshOnFormChange: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-card-display
|
|
const v2CardDisplayOverridesSchema = z
|
|
.object({
|
|
cardsPerRow: z.number().default(3),
|
|
cardSpacing: z.number().default(16),
|
|
cardStyle: z
|
|
.object({
|
|
showTitle: z.boolean().default(true),
|
|
showSubtitle: z.boolean().default(true),
|
|
showDescription: z.boolean().default(true),
|
|
showImage: z.boolean().default(false),
|
|
showActions: z.boolean().default(true),
|
|
})
|
|
.default({ showTitle: true, showSubtitle: true, showDescription: true, showImage: false, showActions: true }),
|
|
columnMapping: z.record(z.string(), z.any()).default({}),
|
|
dataSource: z.string().default("table"),
|
|
staticData: z.array(z.any()).default([]),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-table-search-widget
|
|
const v2TableSearchWidgetOverridesSchema = z
|
|
.object({
|
|
title: z.string().default("테이블 검색"),
|
|
autoSelectFirstTable: z.boolean().default(true),
|
|
showTableSelector: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-tabs-widget
|
|
const v2TabsWidgetOverridesSchema = z
|
|
.object({
|
|
tabs: z
|
|
.array(
|
|
z.object({
|
|
id: z.string(),
|
|
label: z.string(),
|
|
order: z.number().default(0),
|
|
disabled: z.boolean().default(false),
|
|
components: z.array(z.any()).default([]),
|
|
}),
|
|
)
|
|
.default([
|
|
{ id: "tab-1", label: "탭 1", order: 0, disabled: false, components: [] },
|
|
{ id: "tab-2", label: "탭 2", order: 1, disabled: false, components: [] },
|
|
]),
|
|
defaultTab: z.string().default("tab-1"),
|
|
orientation: z.enum(["horizontal", "vertical"]).default("horizontal"),
|
|
variant: z.string().default("default"),
|
|
allowCloseable: z.boolean().default(false),
|
|
persistSelection: z.boolean().default(false),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-repeater
|
|
const v2V2RepeaterOverridesSchema = z
|
|
.object({
|
|
renderMode: z.enum(["inline", "modal", "button", "mixed"]).default("inline"),
|
|
dataSource: z
|
|
.object({
|
|
tableName: z.string().default(""),
|
|
foreignKey: z.string().default(""),
|
|
referenceKey: z.string().default(""),
|
|
})
|
|
.default({ tableName: "", foreignKey: "", referenceKey: "" }),
|
|
columns: z.array(z.any()).default([]),
|
|
modal: z.object({ size: z.string().default("md") }).default({ size: "md" }),
|
|
button: z
|
|
.object({
|
|
sourceType: z.string().default("manual"),
|
|
manualButtons: z.array(z.any()).default([]),
|
|
layout: z.string().default("horizontal"),
|
|
style: z.string().default("outline"),
|
|
})
|
|
.default({ sourceType: "manual", manualButtons: [], layout: "horizontal", style: "outline" }),
|
|
features: z
|
|
.object({
|
|
showAddButton: z.boolean().default(true),
|
|
showDeleteButton: z.boolean().default(true),
|
|
inlineEdit: z.boolean().default(false),
|
|
dragSort: z.boolean().default(false),
|
|
showRowNumber: z.boolean().default(false),
|
|
selectable: z.boolean().default(false),
|
|
multiSelect: z.boolean().default(false),
|
|
})
|
|
.default({
|
|
showAddButton: true,
|
|
showDeleteButton: true,
|
|
inlineEdit: false,
|
|
dragSort: false,
|
|
showRowNumber: false,
|
|
selectable: false,
|
|
multiSelect: false,
|
|
}),
|
|
})
|
|
.passthrough();
|
|
|
|
// ============================================
|
|
// V2 컴포넌트 overrides 스키마 정의
|
|
// ============================================
|
|
|
|
// v2-input
|
|
const v2InputOverridesSchema = z
|
|
.object({
|
|
inputType: z.string().default("text"),
|
|
format: z.string().default("none"),
|
|
placeholder: z.string().default(""),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-select
|
|
const v2SelectOverridesSchema = z
|
|
.object({
|
|
mode: z.string().default("dropdown"),
|
|
source: z.string().default("static"),
|
|
options: z.array(z.any()).default([]),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-date
|
|
const v2DateOverridesSchema = z
|
|
.object({
|
|
dateType: z.string().default("date"),
|
|
format: z.string().default("YYYY-MM-DD"),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-list
|
|
const v2ListOverridesSchema = z
|
|
.object({
|
|
viewMode: z.string().default("table"),
|
|
source: z.string().default("static"),
|
|
columns: z.array(z.any()).default([]),
|
|
pagination: z.boolean().default(true),
|
|
sortable: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-layout
|
|
const v2LayoutOverridesSchema = z
|
|
.object({
|
|
layoutType: z.string().default("grid"),
|
|
columns: z.number().default(2),
|
|
gap: z.string().default("16"),
|
|
use12Column: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-group
|
|
const v2GroupOverridesSchema = z
|
|
.object({
|
|
groupType: z.string().default("section"),
|
|
title: z.string().default(""),
|
|
collapsible: z.boolean().default(false),
|
|
defaultOpen: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-media
|
|
const v2MediaOverridesSchema = z
|
|
.object({
|
|
mediaType: z.string().default("image"),
|
|
multiple: z.boolean().default(false),
|
|
preview: z.boolean().default(true),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-biz
|
|
const v2BizOverridesSchema = z
|
|
.object({
|
|
bizType: z.string().default("flow"),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-hierarchy
|
|
const v2HierarchyOverridesSchema = z
|
|
.object({
|
|
hierarchyType: z.string().default("tree"),
|
|
viewMode: z.string().default("tree"),
|
|
dataSource: z.string().default("static"),
|
|
})
|
|
.passthrough();
|
|
|
|
// v2-repeater
|
|
const v2RepeaterOverridesSchema = z
|
|
.object({
|
|
renderMode: z.enum(["inline", "modal"]).default("inline"),
|
|
mainTableName: z.string().optional(),
|
|
useCustomTable: z.boolean().default(false),
|
|
foreignKeyColumn: z.string().optional(),
|
|
foreignKeySourceColumn: z.string().optional(),
|
|
dataSource: z
|
|
.object({
|
|
tableName: z.string().optional(),
|
|
sourceTable: z.string().optional(),
|
|
foreignKey: z.string().optional(),
|
|
referenceKey: z.string().optional(),
|
|
displayColumn: z.string().optional(),
|
|
})
|
|
.default({}),
|
|
columns: z.array(z.any()).default([]),
|
|
columnMappings: z.array(z.any()).default([]),
|
|
calculationRules: z.array(z.any()).default([]),
|
|
modal: z
|
|
.object({
|
|
size: z.enum(["sm", "md", "lg", "xl", "full"]).default("lg"),
|
|
title: z.string().optional(),
|
|
buttonText: z.string().optional(),
|
|
sourceDisplayColumns: z.array(z.any()).default([]),
|
|
searchFields: z.array(z.string()).default([]),
|
|
})
|
|
.default({ size: "lg", sourceDisplayColumns: [], searchFields: [] }),
|
|
features: z
|
|
.object({
|
|
showAddButton: z.boolean().default(true),
|
|
showDeleteButton: z.boolean().default(true),
|
|
inlineEdit: z.boolean().default(true),
|
|
dragSort: z.boolean().default(false),
|
|
showRowNumber: z.boolean().default(false),
|
|
selectable: z.boolean().default(false),
|
|
multiSelect: z.boolean().default(true),
|
|
})
|
|
.default({
|
|
showAddButton: true,
|
|
showDeleteButton: true,
|
|
inlineEdit: true,
|
|
dragSort: false,
|
|
showRowNumber: false,
|
|
selectable: false,
|
|
multiSelect: true,
|
|
}),
|
|
style: z
|
|
.object({
|
|
maxHeight: z.string().optional(),
|
|
minHeight: z.string().optional(),
|
|
borderless: z.boolean().default(false),
|
|
compact: z.boolean().default(false),
|
|
})
|
|
.optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
// ============================================
|
|
// 컴포넌트별 overrides 스키마 레지스트리
|
|
// ============================================
|
|
const componentOverridesSchemaRegistry: Record<string, z.ZodType<Record<string, any>>> = {
|
|
// V2 컴포넌트 (16개)
|
|
"v2-table-list": v2TableListOverridesSchema,
|
|
"v2-button-primary": v2ButtonPrimaryOverridesSchema,
|
|
"v2-text-display": v2TextDisplayOverridesSchema,
|
|
"v2-split-panel-layout": v2SplitPanelLayoutOverridesSchema,
|
|
"v2-section-card": v2SectionCardOverridesSchema,
|
|
"v2-section-paper": v2SectionPaperOverridesSchema,
|
|
"v2-divider-line": v2DividerLineOverridesSchema,
|
|
"v2-repeat-container": v2RepeatContainerOverridesSchema,
|
|
"v2-rack-structure": v2RackStructureOverridesSchema,
|
|
"v2-numbering-rule": v2NumberingRuleOverridesSchema,
|
|
"v2-category-manager": v2CategoryManagerOverridesSchema,
|
|
"v2-pivot-grid": v2PivotGridOverridesSchema,
|
|
"v2-location-swap-selector": v2LocationSwapSelectorOverridesSchema,
|
|
"v2-aggregation-widget": v2AggregationWidgetOverridesSchema,
|
|
"v2-card-display": v2CardDisplayOverridesSchema,
|
|
"v2-table-search-widget": v2TableSearchWidgetOverridesSchema,
|
|
"v2-tabs-widget": v2TabsWidgetOverridesSchema,
|
|
"v2-repeater": v2V2RepeaterOverridesSchema,
|
|
|
|
// V2 컴포넌트 (9개)
|
|
"v2-input": v2InputOverridesSchema,
|
|
"v2-select": v2SelectOverridesSchema,
|
|
"v2-date": v2DateOverridesSchema,
|
|
"v2-list": v2ListOverridesSchema,
|
|
"v2-layout": v2LayoutOverridesSchema,
|
|
"v2-group": v2GroupOverridesSchema,
|
|
"v2-media": v2MediaOverridesSchema,
|
|
"v2-biz": v2BizOverridesSchema,
|
|
"v2-hierarchy": v2HierarchyOverridesSchema,
|
|
};
|
|
|
|
// ============================================
|
|
// 컴포넌트별 기본값 레지스트리 (fallback용)
|
|
// ============================================
|
|
const componentDefaultsRegistry: Record<string, Record<string, any>> = {
|
|
// V2 컴포넌트
|
|
"v2-table-list": {
|
|
displayMode: "table",
|
|
showHeader: true,
|
|
showFooter: true,
|
|
height: "auto",
|
|
checkbox: { enabled: true, multiple: true, position: "left", selectAll: true },
|
|
columns: [],
|
|
autoWidth: true,
|
|
stickyHeader: false,
|
|
pagination: { enabled: true, pageSize: 20, showSizeSelector: true, showPageInfo: true },
|
|
autoLoad: true,
|
|
},
|
|
"v2-button-primary": {
|
|
text: "저장",
|
|
actionType: "button",
|
|
variant: "primary",
|
|
action: { type: "save", successMessage: "저장되었습니다.", errorMessage: "저장 중 오류가 발생했습니다." },
|
|
},
|
|
"v2-text-display": {
|
|
text: "텍스트를 입력하세요",
|
|
fontSize: "14px",
|
|
fontWeight: "normal",
|
|
color: "#212121",
|
|
textAlign: "left",
|
|
},
|
|
"v2-split-panel-layout": {
|
|
leftPanel: { title: "마스터", showSearch: false, showAdd: false },
|
|
rightPanel: { title: "디테일", showSearch: false, showAdd: false },
|
|
splitRatio: 30,
|
|
resizable: true,
|
|
autoLoad: true,
|
|
syncSelection: true,
|
|
},
|
|
"v2-section-card": {
|
|
title: "섹션 제목",
|
|
description: "",
|
|
showHeader: true,
|
|
padding: "md",
|
|
backgroundColor: "default",
|
|
borderStyle: "solid",
|
|
collapsible: false,
|
|
defaultOpen: true,
|
|
},
|
|
"v2-section-paper": {
|
|
backgroundColor: "default",
|
|
padding: "md",
|
|
roundedCorners: "md",
|
|
shadow: "none",
|
|
showBorder: false,
|
|
},
|
|
"v2-divider-line": {
|
|
placeholder: "텍스트를 입력하세요",
|
|
maxLength: 255,
|
|
},
|
|
"v2-repeat-container": {
|
|
dataSourceType: "manual",
|
|
layout: "vertical",
|
|
gridColumns: 2,
|
|
gap: "16px",
|
|
showBorder: true,
|
|
showShadow: false,
|
|
emptyMessage: "데이터가 없습니다",
|
|
usePaging: false,
|
|
pageSize: 10,
|
|
clickable: false,
|
|
selectionMode: "single",
|
|
},
|
|
"v2-rack-structure": {
|
|
showPreview: true,
|
|
showTemplate: true,
|
|
},
|
|
"v2-numbering-rule": {
|
|
showPreview: true,
|
|
},
|
|
"v2-category-manager": {
|
|
viewMode: "tree",
|
|
maxDepth: 3,
|
|
showActions: true,
|
|
},
|
|
"v2-pivot-grid": {
|
|
fields: [],
|
|
},
|
|
"v2-location-swap-selector": {
|
|
dataSource: { type: "static", tableName: "", valueField: "location_code", labelField: "location_name" },
|
|
departureField: "departure",
|
|
destinationField: "destination",
|
|
departureLabel: "출발지",
|
|
destinationLabel: "도착지",
|
|
showSwapButton: true,
|
|
variant: "card",
|
|
},
|
|
"v2-aggregation-widget": {
|
|
dataSourceType: "table",
|
|
items: [],
|
|
filters: [],
|
|
filterLogic: "AND",
|
|
layout: "horizontal",
|
|
showLabels: true,
|
|
showIcons: true,
|
|
gap: "16px",
|
|
autoRefresh: false,
|
|
refreshOnFormChange: true,
|
|
},
|
|
"v2-card-display": {
|
|
cardsPerRow: 3,
|
|
cardSpacing: 16,
|
|
cardStyle: { showTitle: true, showSubtitle: true, showDescription: true, showImage: false, showActions: true },
|
|
columnMapping: {},
|
|
dataSource: "table",
|
|
staticData: [],
|
|
},
|
|
"v2-table-search-widget": {
|
|
title: "테이블 검색",
|
|
autoSelectFirstTable: true,
|
|
showTableSelector: true,
|
|
},
|
|
"v2-tabs-widget": {
|
|
tabs: [
|
|
{ id: "tab-1", label: "탭 1", order: 0, disabled: false, components: [] },
|
|
{ id: "tab-2", label: "탭 2", order: 1, disabled: false, components: [] },
|
|
],
|
|
defaultTab: "tab-1",
|
|
orientation: "horizontal",
|
|
variant: "default",
|
|
allowCloseable: false,
|
|
persistSelection: false,
|
|
},
|
|
"v2-repeater": {
|
|
renderMode: "inline",
|
|
dataSource: { tableName: "", foreignKey: "", referenceKey: "" },
|
|
columns: [],
|
|
modal: { size: "md" },
|
|
button: { sourceType: "manual", manualButtons: [], layout: "horizontal", style: "outline" },
|
|
features: {
|
|
showAddButton: true,
|
|
showDeleteButton: true,
|
|
inlineEdit: false,
|
|
dragSort: false,
|
|
showRowNumber: false,
|
|
selectable: false,
|
|
multiSelect: false,
|
|
},
|
|
},
|
|
|
|
// V2 컴포넌트
|
|
"v2-input": {
|
|
inputType: "text",
|
|
format: "none",
|
|
placeholder: "",
|
|
},
|
|
"v2-select": {
|
|
mode: "dropdown",
|
|
source: "static",
|
|
options: [],
|
|
},
|
|
"v2-date": {
|
|
dateType: "date",
|
|
format: "YYYY-MM-DD",
|
|
},
|
|
"v2-list": {
|
|
viewMode: "table",
|
|
source: "static",
|
|
columns: [],
|
|
pagination: true,
|
|
sortable: true,
|
|
},
|
|
"v2-layout": {
|
|
layoutType: "grid",
|
|
columns: 2,
|
|
gap: "16",
|
|
use12Column: true,
|
|
},
|
|
"v2-group": {
|
|
groupType: "section",
|
|
title: "",
|
|
collapsible: false,
|
|
defaultOpen: true,
|
|
},
|
|
"v2-media": {
|
|
mediaType: "image",
|
|
multiple: false,
|
|
preview: true,
|
|
},
|
|
"v2-biz": {
|
|
bizType: "flow",
|
|
},
|
|
"v2-hierarchy": {
|
|
hierarchyType: "tree",
|
|
viewMode: "tree",
|
|
dataSource: "static",
|
|
},
|
|
"v2-repeater": {
|
|
renderMode: "inline",
|
|
useCustomTable: false,
|
|
dataSource: {},
|
|
columns: [],
|
|
columnMappings: [],
|
|
calculationRules: [],
|
|
modal: {
|
|
size: "lg",
|
|
sourceDisplayColumns: [],
|
|
searchFields: [],
|
|
},
|
|
features: {
|
|
showAddButton: true,
|
|
showDeleteButton: true,
|
|
inlineEdit: true,
|
|
dragSort: false,
|
|
showRowNumber: false,
|
|
selectable: false,
|
|
multiSelect: true,
|
|
},
|
|
},
|
|
};
|
|
|
|
// ============================================
|
|
// 컴포넌트 기본값 조회
|
|
// ============================================
|
|
export function getComponentDefaults(componentType: string): Record<string, any> {
|
|
const schema = componentOverridesSchemaRegistry[componentType];
|
|
if (schema) {
|
|
return schema.parse({});
|
|
}
|
|
|
|
return componentDefaultsRegistry[componentType] || {};
|
|
}
|
|
|
|
// ============================================
|
|
// URL에서 기본값 조회
|
|
// ============================================
|
|
export function getDefaultsByUrl(url: string): Record<string, any> {
|
|
const componentType = getComponentTypeFromUrl(url);
|
|
return getComponentDefaults(componentType);
|
|
}
|
|
|
|
// ============================================
|
|
// overrides 스키마 파싱 (유효성 검사)
|
|
// ============================================
|
|
export function parseOverridesByUrl(
|
|
url: string,
|
|
overrides: Record<string, any> | null | undefined,
|
|
options?: { applyDefaults?: boolean },
|
|
): Record<string, any> {
|
|
const componentType = getComponentTypeFromUrl(url);
|
|
const schema = componentOverridesSchemaRegistry[componentType];
|
|
const applyDefaults = options?.applyDefaults ?? false;
|
|
|
|
if (!schema) {
|
|
return overrides || {};
|
|
}
|
|
|
|
const parsed = schema.safeParse(overrides || {});
|
|
if (!parsed.success) {
|
|
console.warn("V2 overrides 스키마 검증 실패", {
|
|
componentType,
|
|
errors: parsed.error.issues,
|
|
});
|
|
return overrides || {};
|
|
}
|
|
|
|
return applyDefaults ? parsed.data : overrides || {};
|
|
}
|
|
|
|
// ============================================
|
|
// V2 컴포넌트 로드 (기본값 + overrides 병합)
|
|
// ============================================
|
|
export function loadComponentV2(component: ComponentV2): ComponentV2 & { config: Record<string, any> } {
|
|
const defaults = getDefaultsByUrl(component.url);
|
|
const overrides = parseOverridesByUrl(component.url, component.overrides);
|
|
const config = mergeComponentConfig(defaults, overrides);
|
|
|
|
return {
|
|
...component,
|
|
config,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// V2 컴포넌트 저장 (차이값 추출)
|
|
// ============================================
|
|
export function saveComponentV2(component: ComponentV2 & { config?: Record<string, any> }): ComponentV2 {
|
|
const defaults = getDefaultsByUrl(component.url);
|
|
const normalizedConfig = component.config
|
|
? parseOverridesByUrl(component.url, component.config, { applyDefaults: true })
|
|
: undefined;
|
|
const normalizedOverrides = normalizedConfig
|
|
? extractCustomConfig(normalizedConfig, defaults)
|
|
: parseOverridesByUrl(component.url, component.overrides);
|
|
|
|
return {
|
|
id: component.id,
|
|
url: component.url,
|
|
position: component.position,
|
|
size: component.size,
|
|
displayOrder: component.displayOrder,
|
|
overrides: normalizedOverrides,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// 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),
|
|
};
|
|
}
|