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

452 lines
11 KiB
TypeScript

"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: [],
};
}
// ComponentRegistry의 검증 로직 재사용
return ComponentRegistry["validateComponentDefinition"](definition);
}
/**
* 개발자 도구용 디버그 정보
*/
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);
}