"use client"; import React, { useMemo, useRef, useState, useEffect } from "react"; import { ComponentData } from "@/types/screen"; import { useResponsive } from "@/lib/hooks/useResponsive"; import { cn } from "@/lib/utils"; 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-panel-layout", "screen-split-panel", "v2-split-line", "flow-widget", "v2-tab-container", "tab-container", "tabs-widget", "v2-tabs-widget", ]); const FLEX_GROW_TYPES = new Set([ "table-list", "v2-table-list", "split-panel-layout", "split-panel-layout2", "v2-split-panel-layout", "screen-split-panel", "v2-tab-container", "tab-container", "tabs-widget", "v2-tabs-widget", ]); 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)); } function getComponentTypeId(component: ComponentData): string { const direct = (component as any).componentType || (component as any).widgetType; if (direct) return direct; const url = (component as any).url; if (url && typeof url === "string") { const parts = url.split("/"); return parts[parts.length - 1]; } return component.type || ""; } function isButtonComponent(component: ComponentData): boolean { return getComponentTypeId(component).includes("button"); } function isFullWidthComponent(component: ComponentData): boolean { return FULL_WIDTH_TYPES.has(getComponentTypeId(component)); } function shouldFlexGrow(component: ComponentData): boolean { return FLEX_GROW_TYPES.has(getComponentTypeId(component)); } function getPercentageWidth(componentWidth: number, canvasWidth: number): number { const pct = (componentWidth / canvasWidth) * 100; return pct >= 95 ? 100 : pct; } function getRowGap(row: ComponentData[], canvasWidth: number): number { if (row.length < 2) return 0; const totalW = row.reduce((s, c) => s + (c.size?.width || 100), 0); const gap = canvasWidth - totalW; const cnt = row.length - 1; if (gap <= 0 || cnt <= 0) return 8; return Math.min(Math.max(Math.round(gap / cnt), 4), 24); } interface ProcessedRow { type: "normal" | "fullwidth"; mainComponent?: ComponentData; overlayComps: ComponentData[]; normalComps: ComponentData[]; } /** * 풀위드스 컴포넌트 + 오버레이 버튼. * 원본 좌표 그대로 배치 → transform: scale 한 방으로 축소. * 디자이너 미리보기와 동일한 원리. */ function FullWidthOverlayRow({ main, overlayComps, canvasWidth, renderComponent, }: { main: ComponentData; overlayComps: ComponentData[]; canvasWidth: number; renderComponent: (component: ComponentData) => React.ReactNode; }) { const containerRef = useRef(null); const [containerW, setContainerW] = useState(0); useEffect(() => { const el = containerRef.current; if (!el) return; const ro = new ResizeObserver((entries) => { const w = entries[0]?.contentRect.width; if (w && w > 0) setContainerW(w); }); ro.observe(el); return () => ro.disconnect(); }, []); const compFlexGrow = shouldFlexGrow(main); const mainY = main.position.y; const scale = containerW > 0 ? containerW / canvasWidth : 1; const minButtonY = Math.min(...overlayComps.map((c) => c.position.y)); const rawYOffset = minButtonY - mainY; const maxBtnH = Math.max( ...overlayComps.map((c) => c.size?.height || 40) ); // 버튼 중심 보정: 스케일 축소 시 버튼이 작아지므로 중심 위치가 위로 올라감 // 디자이너와 동일한 중심-대-중심 정렬을 유지하기 위해 Y 오프셋 보정 const yOffset = rawYOffset + (maxBtnH / 2) * (1 - scale); return (
{renderComponent(main)}
{overlayComps.length > 0 && containerW > 0 && (
{overlayComps.map((comp) => (
{renderComponent(comp)}
))}
)}
); } 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: ProcessedRow[] = []; 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 (fullWidthComps.length > 0 && normalComps.length > 0) { for (const fwComp of fullWidthComps) { result.push({ type: "fullwidth", mainComponent: fwComp, overlayComps: normalComps, normalComps: [], }); } } else if (fullWidthComps.length > 0) { for (const fwComp of fullWidthComps) { result.push({ type: "fullwidth", mainComponent: fwComp, overlayComps: [], normalComps: [], }); } } else { result.push({ type: "normal", overlayComps: [], normalComps, }); } } return result; }, [components]); return (
{processedRows.map((processedRow, rowIndex) => { if (processedRow.type === "fullwidth" && processedRow.mainComponent) { return ( ); } const { normalComps } = processedRow; const allButtons = normalComps.every((c) => isButtonComponent(c)); const gap = isMobile ? 8 : allButtons ? 8 : getRowGap(normalComps, canvasWidth); return (
{normalComps.map((component) => { const typeId = getComponentTypeId(component); const isButton = isButtonComponent(component); const isFullWidth = isMobile && !isButton; if (isButton) { return (
{renderComponent(component)}
); } const percentWidth = isFullWidth ? 100 : getPercentageWidth(component.size?.width || 100, canvasWidth); const flexBasis = isFullWidth ? "100%" : `calc(${percentWidth}% - ${gap}px)`; const compFlexGrow = shouldFlexGrow(component); return (
{renderComponent(component)}
); })}
); })}
); } export default ResponsiveGridRenderer;