"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[] { if (!this.props.allComponents) { return []; } // 1. 레이아웃 내의 존에 직접 배치된 컴포넌트들 (zoneId 기준) const zoneComponents = this.props.allComponents.filter( (comp) => comp.parentId === this.props.layout.id && comp.zoneId === zoneId, ); // 2. 기존 방식 호환성 (parentId가 존 ID인 경우) const legacyComponents = this.props.allComponents.filter((comp) => comp.parentId === zoneId); // 중복 제거하여 반환 const allComponents = [...zoneComponents, ...legacyComponents]; const uniqueComponents = allComponents.filter( (component, index, array) => array.findIndex((c) => c.id === component.id) === index, ); return uniqueComponents; } /** * 레이아웃 존을 렌더링합니다. */ 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}
{/* 드롭존 */}
{ if (isDesignMode) { e.preventDefault(); e.stopPropagation(); } }} onDrop={(e) => { if (isDesignMode) { e.preventDefault(); e.stopPropagation(); this.handleZoneDrop(e, zone.id); } }} > {zoneChildren.length > 0 ? ( zoneChildren.map((child) => ( )) ) : (
{isDesignMode ? "컴포넌트를 드래그하여 추가하세요" : ""}
)}
); } /** * 존에 드롭된 컴포넌트를 처리합니다. */ private handleZoneDrop = (e: React.DragEvent, zoneId: string) => { try { const dragData = e.dataTransfer.getData("application/json"); if (!dragData) return; const { type, table, column, component } = JSON.parse(dragData); // 드롭 위치 계산 (존 내부 상대 좌표) const dropZone = e.currentTarget as HTMLElement; const rect = dropZone.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; let newComponent: any; if (type === "table") { // 테이블 컨테이너 생성 newComponent = { id: `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: "container", label: table.tableLabel || table.tableName, tableName: table.tableName, position: { x, y, z: 1 }, size: { width: 300, height: 200 }, parentId: this.props.layout.id, // 레이아웃을 부모로 설정 zoneId: zoneId, // 존 ID 설정 }; } else if (type === "column") { // 컬럼 위젯 생성 newComponent = { id: `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: "widget", label: column.columnLabel || column.columnName, tableName: table.tableName, columnName: column.columnName, widgetType: column.widgetType, dataType: column.dataType, required: column.required, position: { x, y, z: 1 }, size: { width: 200, height: 40 }, parentId: this.props.layout.id, // 레이아웃을 부모로 설정 zoneId: zoneId, // 존 ID 설정 }; } else if (type === "component" && component) { // 컴포넌트 생성 newComponent = { id: `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: component.componentType || "widget", label: component.name, widgetType: component.webType, position: { x, y, z: 1 }, size: component.defaultSize || { width: 200, height: 40 }, parentId: this.props.layout.id, // 레이아웃을 부모로 설정 zoneId: zoneId, // 존 ID 설정 componentConfig: component.componentConfig || {}, webTypeConfig: this.getDefaultWebTypeConfig(component.webType), style: { labelDisplay: true, labelFontSize: "14px", labelColor: "#374151", labelFontWeight: "500", labelMarginBottom: "4px", }, }; } if (newComponent && this.props.onUpdateLayout) { // 레이아웃에 컴포넌트 추가 const updatedComponents = [...(this.props.layout.components || []), newComponent]; this.props.onUpdateLayout({ ...this.props.layout, components: updatedComponents, }); console.log(`컴포넌트가 존 ${zoneId}에 추가되었습니다:`, newComponent); } } catch (error) { console.error("존 드롭 처리 중 오류:", error); } }; /** * 웹타입별 기본 설정을 반환합니다. */ private getDefaultWebTypeConfig = (webType: string) => { const configs: Record = { text: { maxLength: 255 }, number: { min: 0, max: 999999 }, date: { format: "YYYY-MM-DD" }, select: { options: [] }, // 추가 웹타입 설정... }; return configs[webType] || {}; }; /** * 존의 스타일을 계산합니다. */ 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: 특정 레이아웃만 리로드하는 로직 구현 }; } }