"use client"; import { BaseLayoutRenderer, LayoutRendererProps } from "./BaseLayoutRenderer"; import { LayoutDefinition } from "@/types/layout"; import { LayoutRegistry } from "../LayoutRegistry"; import { ComponentData, LayoutComponent } from "@/types/screen"; import { LayoutZone } from "@/types/layout"; import React from "react"; import { DynamicComponentRenderer } from "../DynamicComponentRenderer"; /** * 자동 등록 기능을 제공하는 베이스 레이아웃 렌더러 * * 사용 방법: * 1. 이 클래스를 상속받습니다 * 2. static layoutDefinition을 정의합니다 * 3. 파일을 import하면 자동으로 등록됩니다 */ export class AutoRegisteringLayoutRenderer { protected props: LayoutRendererProps; constructor(props: LayoutRendererProps) { this.props = props; } /** * 레이아웃 정의 - 각 구현 클래스에서 반드시 정의해야 함 */ static readonly layoutDefinition: LayoutDefinition; /** * 렌더링 메서드 - 각 구현 클래스에서 오버라이드해야 함 */ render(): React.ReactElement { throw new Error("render() method must be implemented by subclass"); } /** * 레이아웃 컨테이너 스타일을 계산합니다. */ getLayoutContainerStyle(): React.CSSProperties { const { layout, style: propStyle } = this.props; const style: React.CSSProperties = { width: layout.size.width, height: layout.size.height, position: "relative", overflow: "hidden", ...propStyle, }; // 레이아웃 커스텀 스타일 적용 if (layout.style) { Object.assign(style, this.convertComponentStyleToCSS(layout.style)); } return style; } /** * 컴포넌트 스타일을 CSS 스타일로 변환합니다. */ protected convertComponentStyleToCSS(componentStyle: any): React.CSSProperties { const cssStyle: React.CSSProperties = {}; if (componentStyle.backgroundColor) { cssStyle.backgroundColor = componentStyle.backgroundColor; } if (componentStyle.borderColor) { cssStyle.borderColor = componentStyle.borderColor; } if (componentStyle.borderWidth) { cssStyle.borderWidth = `${componentStyle.borderWidth}px`; } if (componentStyle.borderStyle) { cssStyle.borderStyle = componentStyle.borderStyle; } if (componentStyle.borderRadius) { cssStyle.borderRadius = `${componentStyle.borderRadius}px`; } return cssStyle; } /** * 존별 자식 컴포넌트들을 분류합니다. */ getZoneChildren(zoneId: string): ComponentData[] { return this.props.allComponents.filter((comp) => comp.parentId === this.props.layout.id && comp.zoneId === zoneId); } /** * 레이아웃 존을 렌더링합니다. */ renderZone( zone: LayoutZone, zoneChildren: ComponentData[] = [], additionalProps: Record = {}, ): React.ReactElement { const { isDesignMode, onZoneClick, onComponentDrop } = this.props; // 존 스타일 계산 - 항상 구역 경계 표시 const zoneStyle: React.CSSProperties = { position: "relative", // 구역 경계 시각화 - 항상 표시 border: "1px solid #e2e8f0", borderRadius: "6px", backgroundColor: "rgba(248, 250, 252, 0.5)", transition: "all 0.2s ease", ...this.getZoneStyle(zone), ...additionalProps.style, }; // 디자인 모드일 때 더 강조된 스타일 if (isDesignMode) { zoneStyle.border = "2px dashed #cbd5e1"; zoneStyle.backgroundColor = "rgba(241, 245, 249, 0.8)"; } // 호버 효과를 위한 추가 스타일 const dropZoneStyle: React.CSSProperties = { minHeight: isDesignMode ? "60px" : "40px", borderRadius: "4px", display: "flex", flexDirection: "column", alignItems: zoneChildren.length === 0 ? "center" : "stretch", justifyContent: zoneChildren.length === 0 ? "flex-start" : "flex-start", color: "#64748b", fontSize: "12px", transition: "all 0.2s ease", padding: "8px", position: "relative", }; return (
{ e.stopPropagation(); onZoneClick?.(zone.id, e); }} onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; }} onDrop={(e) => { e.preventDefault(); const componentData = e.dataTransfer.getData("application/json"); if (componentData) { try { const component = JSON.parse(componentData); onComponentDrop?.(zone.id, component, e); } catch (error) { console.error("컴포넌트 드롭 데이터 파싱 오류:", error); } } }} onMouseEnter={(e) => { e.currentTarget.style.borderColor = "#3b82f6"; e.currentTarget.style.backgroundColor = "rgba(59, 130, 246, 0.05)"; e.currentTarget.style.boxShadow = "0 0 0 2px rgba(59, 130, 246, 0.1)"; }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; e.currentTarget.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; e.currentTarget.style.boxShadow = "none"; }} {...additionalProps} > {/* 존 라벨 */}
{zone.name || zone.id}
{/* 드롭존 */}
{zoneChildren.length > 0 ? ( zoneChildren.map((child) => ( )) ) : (
{isDesignMode ? "컴포넌트를 드래그하여 추가하세요" : ""}
)}
); } /** * 존의 스타일을 계산합니다. */ protected getZoneStyle(zone: LayoutZone): React.CSSProperties { const style: React.CSSProperties = {}; if (zone.size) { if (zone.size.width) { style.width = typeof zone.size.width === "number" ? `${zone.size.width}px` : zone.size.width; } if (zone.size.height) { style.height = typeof zone.size.height === "number" ? `${zone.size.height}px` : zone.size.height; } if (zone.size.minWidth) { style.minWidth = typeof zone.size.minWidth === "number" ? `${zone.size.minWidth}px` : zone.size.minWidth; } if (zone.size.minHeight) { style.minHeight = typeof zone.size.minHeight === "number" ? `${zone.size.minHeight}px` : zone.size.minHeight; } } return style; } /** * 자동 등록 상태 추적 */ private static registeredLayouts = new Set(); /** * 클래스가 정의될 때 자동으로 레지스트리에 등록 */ static registerSelf(): void { const definition = this.layoutDefinition; if (!definition) { console.error(`❌ ${this.name}: layoutDefinition이 정의되지 않았습니다.`); return; } if (this.registeredLayouts.has(definition.id)) { console.warn(`⚠️ ${definition.id} 레이아웃이 이미 등록되어 있습니다.`); return; } try { // 레지스트리에 등록 LayoutRegistry.registerLayout(definition); this.registeredLayouts.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, zones: definition.defaultZones?.length || 0, tags: definition.tags?.join(", ") || "none", }); } } catch (error) { console.error(`❌ ${definition.id} 레이아웃 등록 실패:`, error); } } /** * 레이아웃 등록 해제 (개발 모드에서 Hot Reload용) */ static unregisterSelf(): void { const definition = this.layoutDefinition; if (definition && this.registeredLayouts.has(definition.id)) { LayoutRegistry.unregisterLayout(definition.id); this.registeredLayouts.delete(definition.id); console.log(`🗑️ 등록 해제: ${definition.id}`); } } /** * Hot Reload 지원 (개발 모드) */ static reloadSelf(): void { if (process.env.NODE_ENV === "development") { this.unregisterSelf(); this.registerSelf(); console.log(`🔄 Hot Reload: ${this.layoutDefinition?.id}`); } } /** * 등록된 레이아웃 목록 조회 */ static getRegisteredLayouts(): string[] { return Array.from(this.registeredLayouts); } /** * 레이아웃 정의 유효성 검사 */ static validateDefinition(): { isValid: boolean; errors: string[]; warnings: string[] } { const definition = this.layoutDefinition; if (!definition) { return { isValid: false, errors: ["layoutDefinition이 정의되지 않았습니다."], warnings: [], }; } const errors: string[] = []; const warnings: string[] = []; // 필수 필드 검사 if (!definition.id) errors.push("ID가 필요합니다."); if (!definition.name) errors.push("이름이 필요합니다."); if (!definition.component) errors.push("컴포넌트가 필요합니다."); if (!definition.category) errors.push("카테고리가 필요합니다."); // 권장사항 검사 if (!definition.description || definition.description.length < 10) { warnings.push("설명은 10자 이상 권장됩니다."); } if (!definition.defaultZones || definition.defaultZones.length === 0) { warnings.push("기본 존 정의가 권장됩니다."); } return { isValid: errors.length === 0, errors, warnings, }; } } /** * 개발 모드에서 Hot Module Replacement 지원 */ if (process.env.NODE_ENV === "development" && typeof window !== "undefined") { // HMR API가 있는 경우 등록 if ((module as any).hot) { (module as any).hot.accept(); // 글로벌 Hot Reload 함수 등록 (window as any).__reloadLayout__ = (layoutId: string) => { const layouts = AutoRegisteringLayoutRenderer.getRegisteredLayouts(); console.log(`🔄 Available layouts for reload:`, layouts); // TODO: 특정 레이아웃만 리로드하는 로직 구현 }; } }