/** * 반응형 레이아웃 엔진 * * 절대 위치로 배치된 컴포넌트들을 반응형 그리드로 변환 */ import React, { useMemo } from "react"; import { ComponentData } from "@/types/screen-management"; import { Breakpoint, BREAKPOINTS } from "@/types/responsive"; import { generateSmartDefaults, ensureResponsiveConfig } from "@/lib/utils/responsiveDefaults"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; export interface ResponsiveLayoutEngineProps { components: ComponentData[]; breakpoint: Breakpoint; containerWidth: number; screenWidth?: number; preserveYPosition?: boolean; // true: Y좌표 유지 (하이브리드), false: 16px 간격 (반응형) formData?: Record; onFormDataChange?: (fieldName: string, value: unknown) => void; screenInfo?: { id: number; tableName?: string }; } /** * 반응형 레이아웃 엔진 * * 변환 로직: * 1. Y 위치 기준으로 행(row)으로 그룹화 * 2. 각 행 내에서 X 위치 기준으로 정렬 * 3. 반응형 설정 적용 (order, gridColumns, hide) * 4. CSS Grid로 렌더링 */ export const ResponsiveLayoutEngine: React.FC = ({ components, breakpoint, containerWidth, screenWidth = 1920, preserveYPosition = false, // 기본값: 반응형 모드 (16px 간격) formData, onFormDataChange, screenInfo, }) => { // 1단계: 컴포넌트들을 Y 위치 기준으로 행(row)으로 그룹화 const rows = useMemo(() => { const sortedComponents = [...components].sort((a, b) => a.position.y - b.position.y); const rows: ComponentData[][] = []; let currentRow: ComponentData[] = []; let currentRowY = 0; const ROW_THRESHOLD = 150; // 같은 행으로 간주할 Y 오차 범위 (px) - 여유있게 설정 sortedComponents.forEach((comp) => { if (currentRow.length === 0) { currentRow.push(comp); currentRowY = comp.position.y; } else if (Math.abs(comp.position.y - currentRowY) < ROW_THRESHOLD) { currentRow.push(comp); } else { rows.push(currentRow); currentRow = [comp]; currentRowY = comp.position.y; } }); if (currentRow.length > 0) { rows.push(currentRow); } return rows; }, [components]); // 2단계: 각 행 내에서 X 위치 기준으로 정렬 const sortedRows = useMemo(() => { return rows.map((row) => [...row].sort((a, b) => a.position.x - b.position.x)); }, [rows]); // 3단계: 반응형 설정 적용 const responsiveComponents = useMemo(() => { const result = sortedRows.flatMap((row, rowIndex) => row.map((comp, compIndex) => { // 컴포넌트에 gridColumns가 이미 설정되어 있으면 그 값 사용 if ((comp as any).gridColumns !== undefined) { return { ...comp, responsiveDisplay: { gridColumns: (comp as any).gridColumns, order: compIndex + 1, hide: false, }, }; } // 반응형 설정이 없으면 자동 생성 const compWithConfig = ensureResponsiveConfig(comp, screenWidth); // 현재 브레이크포인트의 설정 가져오기 (같은 행의 컴포넌트 개수 전달) const config = compWithConfig.responsiveConfig!.useSmartDefaults ? generateSmartDefaults(comp, screenWidth, row.length)[breakpoint] : compWithConfig.responsiveConfig!.responsive?.[breakpoint]; const finalConfig = config || generateSmartDefaults(comp, screenWidth, row.length)[breakpoint]; return { ...compWithConfig, responsiveDisplay: finalConfig, }; }), ); return result; }, [sortedRows, breakpoint, screenWidth]); // 4단계: 필터링 및 정렬 const visibleComponents = useMemo(() => { return responsiveComponents .filter((comp) => !comp.responsiveDisplay?.hide) .sort((a, b) => (a.responsiveDisplay?.order || 0) - (b.responsiveDisplay?.order || 0)); }, [responsiveComponents]); const gridColumns = BREAKPOINTS[breakpoint].columns; // 각 행의 Y 위치를 추적 const rowsWithYPosition = useMemo(() => { return sortedRows.map((row) => ({ components: row, yPosition: Math.min(...row.map((c) => c.position.y)), // 행의 최소 Y 위치 })); }, [sortedRows]); return (
{rowsWithYPosition.map((row, rowIndex) => { const rowComponents = visibleComponents.filter((vc) => row.components.some((rc) => rc.id === vc.id)); // Y 좌표 계산: preserveYPosition에 따라 다르게 처리 let marginTop: string; if (preserveYPosition) { // 하이브리드 모드: 원래 Y 좌표 간격 유지 if (rowIndex === 0) { marginTop = `${row.yPosition}px`; } else { const prevRowY = rowsWithYPosition[rowIndex - 1].yPosition; const actualGap = row.yPosition - prevRowY; marginTop = `${actualGap}px`; } } else { // 반응형 모드: 16px 고정 간격 marginTop = rowIndex === 0 ? `${row.yPosition}px` : "16px"; } return (
{rowComponents.map((comp) => (
))}
); })}
); };