ERP-node/frontend/lib/registry/AutoRegisteringComponentRen...

463 lines
11 KiB
TypeScript
Raw Normal View History

2025-09-11 18:38:28 +09:00
"use client";
import React from "react";
import { ComponentDefinition, ComponentRendererProps, ComponentConfig } from "@/types/component";
import { ComponentRegistry } from "./ComponentRegistry";
/**
*
*
* AutoRegisteringLayoutRenderer와
*/
export class AutoRegisteringComponentRenderer {
protected props: ComponentRendererProps;
/**
*
*
*/
static componentDefinition: ComponentDefinition;
constructor(props: ComponentRendererProps) {
this.props = props;
}
/**
*
*
*/
render(): React.ReactElement {
throw new Error(`${this.constructor.name}: render() 메서드를 구현해야 합니다. 이는 추상 메서드입니다.`);
}
/**
*
* ,
*/
protected getComponentStyle(): React.CSSProperties {
const { component, isDesignMode = false } = this.props;
const baseStyle: React.CSSProperties = {
position: "absolute",
left: `${component.position?.x || 0}px`,
top: `${component.position?.y || 0}px`,
width: `${component.size?.width || 200}px`,
height: `${component.size?.height || 36}px`,
zIndex: component.position?.z || 1,
...component.style,
};
// 디자인 모드에서 추가 스타일
if (isDesignMode) {
baseStyle.border = "1px dashed #cbd5e1";
baseStyle.borderColor = this.props.isSelected ? "#3b82f6" : "#cbd5e1";
}
return baseStyle;
}
/**
* Props
* HTML
*/
protected getWebTypeProps(): Record<string, any> {
const { component } = this.props;
const baseProps = {
id: component.id,
name: component.id,
value: component.value || "",
disabled: component.readonly || false,
required: component.required || false,
placeholder: component.placeholder || "",
};
switch (component.webType) {
case "text":
return {
...baseProps,
type: "text",
maxLength: component.maxLength,
minLength: component.minLength,
};
case "number":
return {
...baseProps,
type: "number",
min: component.min,
max: component.max,
step: component.step || 1,
};
case "email":
return {
...baseProps,
type: "email",
};
case "password":
return {
...baseProps,
type: "password",
};
case "date":
return {
...baseProps,
type: "date",
min: component.minDate,
max: component.maxDate,
};
case "datetime":
return {
...baseProps,
type: "datetime-local",
min: component.minDate,
max: component.maxDate,
};
case "time":
return {
...baseProps,
type: "time",
};
case "url":
return {
...baseProps,
type: "url",
};
case "tel":
return {
...baseProps,
type: "tel",
};
case "search":
return {
...baseProps,
type: "search",
};
case "textarea":
return {
...baseProps,
rows: component.rows || 3,
cols: component.cols,
wrap: component.wrap || "soft",
};
case "select":
case "dropdown":
return {
...baseProps,
multiple: component.multiple || false,
};
case "checkbox":
return {
...baseProps,
type: "checkbox",
checked: component.checked || false,
};
case "radio":
return {
...baseProps,
type: "radio",
checked: component.checked || false,
};
case "button":
return {
...baseProps,
type: component.buttonType || "button",
};
case "file":
return {
...baseProps,
type: "file",
accept: component.accept,
multiple: component.multiple || false,
};
case "range":
return {
...baseProps,
type: "range",
min: component.min || 0,
max: component.max || 100,
step: component.step || 1,
};
case "color":
return {
...baseProps,
type: "color",
};
default:
return baseProps;
}
}
/**
*
*
*/
protected getLabelStyle(): React.CSSProperties {
const { component } = this.props;
return {
position: "absolute",
top: "-25px",
left: "0px",
fontSize: component.style?.labelFontSize || "14px",
color: component.style?.labelColor || "#374151",
marginBottom: component.style?.labelMarginBottom || "4px",
fontWeight: "500",
};
}
/**
*
*/
protected getLabelInfo(): { text: string; isRequired: boolean } | null {
const { component } = this.props;
if (!component.label && !component.style?.labelText) {
return null;
}
return {
text: component.style?.labelText || component.label,
isRequired: component.required || false,
};
}
/**
*
*/
protected getErrorStyle(): React.CSSProperties {
return {
position: "absolute",
top: "100%",
left: "0px",
fontSize: "12px",
color: "#ef4444",
marginTop: "4px",
};
}
/**
*
*/
protected getHelperTextStyle(): React.CSSProperties {
return {
position: "absolute",
top: "100%",
left: "0px",
fontSize: "12px",
color: "#6b7280",
marginTop: "4px",
};
}
/**
*
*
*/
protected getEventHandlers() {
const { onClick, onDragStart, onDragEnd } = this.props;
return {
onClick: (e: React.MouseEvent) => {
e.stopPropagation();
onClick?.();
},
onDragStart: (e: React.DragEvent) => {
onDragStart?.(e);
},
onDragEnd: (e: React.DragEvent) => {
onDragEnd?.(e);
},
};
}
/**
*
*/
protected getConfig<T = ComponentConfig>(): T {
const { component } = this.props;
const definition = ComponentRegistry.getComponent(component.componentType);
return {
...definition?.defaultConfig,
...component.config,
} as T;
}
/**
*
*/
protected updateComponent(updates: Partial<any>): void {
this.props.onUpdate?.(updates);
}
/**
*
*/
protected handleValueChange(value: any): void {
this.updateComponent({ value });
}
/**
*
*/
private static registeredComponents = new Set<string>();
/**
*
*
*/
static registerSelf(): void {
const definition = this.componentDefinition;
if (!definition) {
console.error(`${this.name}: componentDefinition이 정의되지 않았습니다.`);
return;
}
if (this.registeredComponents.has(definition.id)) {
console.warn(`⚠️ ${definition.id} 컴포넌트가 이미 등록되어 있습니다.`);
return;
}
try {
// 레지스트리에 등록
ComponentRegistry.registerComponent(definition);
this.registeredComponents.add(definition.id);
console.log(`✅ 컴포넌트 자동 등록 완료: ${definition.id} (${definition.name})`);
// 개발 모드에서 추가 정보 출력
if (process.env.NODE_ENV === "development") {
console.log(`📦 ${definition.id}:`, {
name: definition.name,
category: definition.category,
webType: definition.webType,
tags: definition.tags?.join(", ") || "none",
});
}
} catch (error) {
console.error(`${definition.id} 컴포넌트 등록 실패:`, error);
}
}
/**
* ( Hot Reload용)
*/
static unregisterSelf(): void {
const definition = this.componentDefinition;
if (definition && this.registeredComponents.has(definition.id)) {
ComponentRegistry.unregisterComponent(definition.id);
this.registeredComponents.delete(definition.id);
console.log(`🗑️ 컴포넌트 자동 해제: ${definition.id}`);
}
}
/**
* Hot Reload
*/
static enableHotReload(): void {
if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {
// HMR (Hot Module Replacement) 감지
if ((module as any).hot) {
(module as any).hot.dispose(() => {
this.unregisterSelf();
});
(module as any).hot.accept(() => {
this.registerSelf();
});
}
}
}
/**
*
*/
static validateDefinition(): {
isValid: boolean;
errors: string[];
warnings: string[];
} {
const definition = this.componentDefinition;
if (!definition) {
return {
isValid: false,
errors: ["componentDefinition이 정의되지 않았습니다"],
warnings: [],
};
}
2025-09-12 14:24:25 +09:00
// 기본적인 검증만 수행
const errors: string[] = [];
const warnings: string[] = [];
if (!definition.id) errors.push("id가 필요합니다");
if (!definition.name) errors.push("name이 필요합니다");
if (!definition.category) errors.push("category가 필요합니다");
return {
isValid: errors.length === 0,
errors,
warnings,
};
2025-09-11 18:38:28 +09:00
}
/**
*
*/
static getDebugInfo(): object {
const definition = this.componentDefinition;
return {
className: this.name,
definition: definition || null,
isRegistered: definition ? ComponentRegistry.hasComponent(definition.id) : false,
validation: this.validateDefinition(),
};
}
}
// 클래스가 정의되는 즉시 자동 등록 활성화
// 하위 클래스에서 이 클래스를 상속받으면 자동으로 등록됩니다
if (typeof window !== "undefined") {
// 브라우저 환경에서만 실행
setTimeout(() => {
// 모든 모듈이 로드된 후 등록 실행
const subclasses = Object.getOwnPropertyNames(window)
.map((name) => (window as any)[name])
.filter(
(obj) =>
typeof obj === "function" &&
obj.prototype instanceof AutoRegisteringComponentRenderer &&
obj.componentDefinition,
);
subclasses.forEach((cls) => {
try {
cls.registerSelf();
} catch (error) {
console.error(`컴포넌트 자동 등록 실패: ${cls.name}`, error);
}
});
}, 0);
}