"use client"; import React, { useMemo } from "react"; import { ComponentData } from "@/types/screen"; import { useResponsive } from "@/lib/hooks/useResponsive"; interface ResponsiveGridRendererProps { components: ComponentData[]; canvasWidth: number; canvasHeight: number; renderComponent: (component: ComponentData) => React.ReactNode; } // 전체 행을 차지해야 하는 컴포넌트 타입 const FULL_WIDTH_TYPES = new Set([ "table-list", "v2-table-list", "table-search-widget", "v2-table-search-widget", "conditional-container", "split-panel-layout", "split-panel-layout2", "v2-split-line", "flow-widget", "v2-tab-container", "tab-container", ]); // 높이를 auto로 처리해야 하는 컴포넌트 타입 const AUTO_HEIGHT_TYPES = new Set([ "table-list", "v2-table-list", "table-search-widget", "v2-table-search-widget", "conditional-container", "flow-widget", "v2-tab-container", "tab-container", "split-panel-layout", "split-panel-layout2", ]); /** * Y좌표 기준으로 컴포넌트를 행(row) 단위로 그룹핑 * - Y값 차이가 threshold 이내면 같은 행으로 판정 * - 같은 행 안에서는 X좌표 순으로 정렬 */ function groupComponentsIntoRows( components: ComponentData[], threshold: number = 30 ): ComponentData[][] { if (components.length === 0) return []; const sorted = [...components].sort((a, b) => a.position.y - b.position.y); const rows: ComponentData[][] = []; let currentRow: ComponentData[] = []; let currentRowY = -Infinity; for (const comp of sorted) { if (comp.position.y - currentRowY > threshold) { if (currentRow.length > 0) rows.push(currentRow); currentRow = [comp]; currentRowY = comp.position.y; } else { currentRow.push(comp); } } if (currentRow.length > 0) rows.push(currentRow); return rows.map((row) => row.sort((a, b) => a.position.x - b.position.x) ); } /** * 컴포넌트의 유형 식별 (componentType, componentId, widgetType 등) */ function getComponentTypeId(component: ComponentData): string { return ( (component as any).componentType || (component as any).componentId || (component as any).widgetType || component.type || "" ); } /** * 전체 행을 차지해야 하는 컴포넌트인지 판정 */ function isFullWidthComponent(component: ComponentData): boolean { const typeId = getComponentTypeId(component); return FULL_WIDTH_TYPES.has(typeId); } /** * 높이를 auto로 처리해야 하는 컴포넌트인지 판정 */ function shouldAutoHeight(component: ComponentData): boolean { const typeId = getComponentTypeId(component); return AUTO_HEIGHT_TYPES.has(typeId); } /** * 컴포넌트 너비를 캔버스 대비 비율(%)로 변환 */ function getPercentageWidth( componentWidth: number, canvasWidth: number ): number { const percentage = (componentWidth / canvasWidth) * 100; if (percentage >= 95) return 100; return percentage; } /** * 행 내 컴포넌트 사이의 수평 갭(px)을 비율 기반으로 추정 */ function getRowGap(row: ComponentData[], canvasWidth: number): number { if (row.length < 2) return 0; const totalComponentWidth = row.reduce( (sum, c) => sum + (c.size?.width || 100), 0 ); const totalGap = canvasWidth - totalComponentWidth; const gapCount = row.length - 1; if (totalGap <= 0 || gapCount <= 0) return 8; const gapPx = totalGap / gapCount; return Math.min(Math.max(Math.round(gapPx), 4), 24); } export function ResponsiveGridRenderer({ components, canvasWidth, canvasHeight, renderComponent, }: ResponsiveGridRendererProps) { const { isMobile } = useResponsive(); // 전체 행을 차지하는 컴포넌트는 별도 행으로 분리 const processedRows = useMemo(() => { const topLevel = components.filter((c) => !c.parentId); const rows = groupComponentsIntoRows(topLevel); const result: ComponentData[][] = []; for (const row of rows) { // 전체 너비 컴포넌트는 독립 행으로 분리 const fullWidthComps: ComponentData[] = []; const normalComps: ComponentData[] = []; for (const comp of row) { if (isFullWidthComponent(comp)) { fullWidthComps.push(comp); } else { normalComps.push(comp); } } // 일반 컴포넌트 행 먼저 if (normalComps.length > 0) { result.push(normalComps); } // 전체 너비 컴포넌트는 각각 독립 행 for (const comp of fullWidthComps) { result.push([comp]); } } return result; }, [components]); return (