"use client"; import React from "react"; import { ComponentData, LayoutComponent } from "@/types/screen"; import { LayoutZone } from "@/types/layout"; import { DynamicComponentRenderer } from "../DynamicComponentRenderer"; export interface LayoutRendererProps { layout: LayoutComponent; allComponents: ComponentData[]; isDesignMode?: boolean; isSelected?: boolean; onClick?: (e: React.MouseEvent) => void; onZoneClick?: (zoneId: string, e: React.MouseEvent) => void; onComponentDrop?: (zoneId: string, component: ComponentData, e: React.DragEvent) => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: (e: React.DragEvent) => void; onUpdateLayout?: (updatedLayout: LayoutComponent) => void; // 레이아웃 업데이트 콜백 children?: React.ReactNode; className?: string; style?: React.CSSProperties; // 추가된 props들 (레이아웃에서 사용되지 않지만 필터링 시 필요) formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; isInteractive?: boolean; screenId?: number; tableName?: string; onRefresh?: () => void; onClose?: () => void; mode?: "view" | "edit"; isInModal?: boolean; originalData?: Record; [key: string]: any; // 기타 props 허용 } export abstract class BaseLayoutRenderer extends React.Component { abstract render(): React.ReactElement; /** * 레이아웃 존을 렌더링합니다. */ protected 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 ? "center" : "flex-start", color: "#64748b", fontSize: "12px", transition: "all 0.2s ease", padding: "8px", position: "relative", }; return (
{ e.stopPropagation(); onZoneClick?.(zone.id, e); }} onMouseEnter={(e) => { const element = e.currentTarget; element.style.borderColor = "#3b82f6"; element.style.backgroundColor = "rgba(59, 130, 246, 0.02)"; element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)"; }} onMouseLeave={(e) => { const element = e.currentTarget; element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; element.style.boxShadow = "none"; }} onDrop={this.handleDrop(zone.id)} onDragOver={this.handleDragOver} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} > {/* 구역 라벨 - 항상 표시 */}
{zone.name || zone.id}
{zoneChildren.length === 0 && isDesignMode ? (
{zone.name}에 컴포넌트를 드롭하세요
) : zoneChildren.length === 0 ? (
빈 구역
) : ( zoneChildren.map((child) => ( )) )}
); } /** * 존 스타일을 계산합니다. */ protected getZoneStyle(zone: LayoutZone): React.CSSProperties { const style: React.CSSProperties = {}; // 크기 설정 if (typeof zone.size.width === "number") { style.width = `${zone.size.width}px`; } else { style.width = zone.size.width; } if (typeof zone.size.height === "number") { style.height = `${zone.size.height}px`; } else { style.height = zone.size.height; } // 최소/최대 크기 if (zone.size.minWidth) style.minWidth = `${zone.size.minWidth}px`; if (zone.size.minHeight) style.minHeight = `${zone.size.minHeight}px`; if (zone.size.maxWidth) style.maxWidth = `${zone.size.maxWidth}px`; if (zone.size.maxHeight) style.maxHeight = `${zone.size.maxHeight}px`; // 커스텀 스타일 적용 if (zone.style) { Object.assign(style, this.convertComponentStyleToCSS(zone.style)); } return style; } /** * ComponentStyle을 CSS 스타일로 변환합니다. */ protected convertComponentStyleToCSS(componentStyle: any): React.CSSProperties { const cssStyle: React.CSSProperties = {}; // 여백 if (componentStyle.margin) cssStyle.margin = componentStyle.margin; if (componentStyle.padding) cssStyle.padding = componentStyle.padding; // 테두리 if (componentStyle.borderWidth) cssStyle.borderWidth = `${componentStyle.borderWidth}px`; if (componentStyle.borderColor) cssStyle.borderColor = componentStyle.borderColor; if (componentStyle.borderStyle) cssStyle.borderStyle = componentStyle.borderStyle; if (componentStyle.borderRadius) cssStyle.borderRadius = `${componentStyle.borderRadius}px`; // 배경 if (componentStyle.backgroundColor) cssStyle.backgroundColor = componentStyle.backgroundColor; // 텍스트 if (componentStyle.color) cssStyle.color = componentStyle.color; if (componentStyle.fontSize) cssStyle.fontSize = `${componentStyle.fontSize}px`; if (componentStyle.fontWeight) cssStyle.fontWeight = componentStyle.fontWeight; if (componentStyle.textAlign) cssStyle.textAlign = componentStyle.textAlign; return cssStyle; } /** * 레이아웃 컨테이너 스타일을 계산합니다. */ protected 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; } /** * 존별 자식 컴포넌트들을 분류합니다. */ protected getZoneChildren(zoneId: string): ComponentData[] { return this.props.allComponents.filter( (component) => component.parentId === this.props.layout.id && (component as any).zoneId === zoneId, ); } /** * 드래그 드롭 핸들러 */ private handleDrop = (zoneId: string) => (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // 드롭존 하이라이트 제거 this.removeDragHighlight(e.currentTarget as HTMLElement); try { const componentData = e.dataTransfer.getData("application/json"); if (componentData) { const component = JSON.parse(componentData); this.props.onComponentDrop?.(zoneId, component, e); } } catch (error) { console.error("드롭 데이터 파싱 오류:", error); } }; private handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // 드롭존 하이라이트 추가 this.addDragHighlight(e.currentTarget as HTMLElement); }; private handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }; private handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // 실제로 존을 벗어났는지 확인 const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { this.removeDragHighlight(e.currentTarget as HTMLElement); } }; /** * 드래그 하이라이트 추가 */ private addDragHighlight(element: HTMLElement) { element.classList.add("drag-over"); element.style.borderColor = "#3b82f6"; element.style.backgroundColor = "rgba(59, 130, 246, 0.05)"; } /** * 드래그 하이라이트 제거 */ private removeDragHighlight(element: HTMLElement) { element.classList.remove("drag-over"); element.style.borderColor = ""; element.style.backgroundColor = ""; } }