"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 { 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 { const { component } = this.props; const definition = ComponentRegistry.getComponent(component.componentType); return { ...definition?.defaultConfig, ...component.config, } as T; } /** * 업데이트 헬퍼 */ protected updateComponent(updates: Partial): void { this.props.onUpdate?.(updates); } /** * 값 변경 헬퍼 */ protected handleValueChange(value: any): void { this.updateComponent({ value }); } /** * 자동 등록 상태 추적 */ private static registeredComponents = new Set(); /** * 클래스가 정의될 때 자동으로 레지스트리에 등록 * 레이아웃 시스템과 동일한 방식 */ 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: [], }; } // 기본적인 검증만 수행 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, }; } /** * 개발자 도구용 디버그 정보 */ 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); }