ERP-node/frontend/components/screen/ResponsiveLayoutEngine.tsx

144 lines
4.7 KiB
TypeScript

/**
* 반응형 레이아웃 엔진
*
* 절대 위치로 배치된 컴포넌트들을 반응형 그리드로 변환
*/
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";
interface ResponsiveLayoutEngineProps {
components: ComponentData[];
breakpoint: Breakpoint;
containerWidth: number;
screenWidth?: number;
}
/**
* 반응형 레이아웃 엔진
*
* 변환 로직:
* 1. Y 위치 기준으로 행(row)으로 그룹화
* 2. 각 행 내에서 X 위치 기준으로 정렬
* 3. 반응형 설정 적용 (order, gridColumns, hide)
* 4. CSS Grid로 렌더링
*/
export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
components,
breakpoint,
containerWidth,
screenWidth = 1920,
}) => {
// 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) => {
// 반응형 설정이 없으면 자동 생성
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 (
<div className="responsive-container w-full" style={{ position: "relative" }}>
{rowsWithYPosition.map((row, rowIndex) => {
const rowComponents = visibleComponents.filter((vc) => row.components.some((rc) => rc.id === vc.id));
return (
<div
key={`row-${rowIndex}`}
className="responsive-grid w-full"
style={{
display: "grid",
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
gap: "16px",
padding: "0 16px",
marginTop: rowIndex === 0 ? `${row.yPosition}px` : "16px",
}}
>
{rowComponents.map((comp) => (
<div
key={comp.id}
className="responsive-grid-item"
style={{
gridColumn: `span ${comp.responsiveDisplay?.gridColumns || gridColumns}`,
}}
>
<DynamicComponentRenderer component={comp} isPreview={true} />
</div>
))}
</div>
);
})}
</div>
);
};