22
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
|
||||
import React, { 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[];
|
||||
@@ -12,60 +10,6 @@ interface ResponsiveGridRendererProps {
|
||||
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;
|
||||
@@ -78,132 +22,10 @@ function getComponentTypeId(component: ComponentData): string {
|
||||
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[];
|
||||
rowMinY?: number;
|
||||
rowMaxBottom?: number;
|
||||
}
|
||||
|
||||
function FullWidthOverlayRow({
|
||||
main,
|
||||
overlayComps,
|
||||
canvasWidth,
|
||||
renderComponent,
|
||||
}: {
|
||||
main: ComponentData;
|
||||
overlayComps: ComponentData[];
|
||||
canvasWidth: number;
|
||||
renderComponent: (component: ComponentData) => React.ReactNode;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(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)
|
||||
);
|
||||
const yOffset = rawYOffset + (maxBtnH / 2) * (1 - scale);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"relative flex w-full flex-col",
|
||||
compFlexGrow ? "min-h-0 flex-1" : "flex-shrink-0"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-component-id={main.id}
|
||||
data-component-type={getComponentTypeId(main)}
|
||||
className="min-h-0 min-w-0"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: compFlexGrow ? "100%" : "auto",
|
||||
minHeight: compFlexGrow ? "300px" : undefined,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{renderComponent(main)}
|
||||
</div>
|
||||
|
||||
{overlayComps.length > 0 && containerW > 0 && (
|
||||
<div
|
||||
className="pointer-events-none absolute left-0 z-10"
|
||||
style={{
|
||||
top: `${yOffset}px`,
|
||||
width: `${canvasWidth}px`,
|
||||
height: `${maxBtnH}px`,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "top left",
|
||||
}}
|
||||
>
|
||||
{overlayComps.map((comp) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
data-component-id={comp.id}
|
||||
data-component-type={getComponentTypeId(comp)}
|
||||
className="pointer-events-auto absolute"
|
||||
style={{
|
||||
left: `${comp.position.x}px`,
|
||||
top: `${comp.position.y - minButtonY}px`,
|
||||
width: `${comp.size?.width || 90}px`,
|
||||
height: `${comp.size?.height || 40}px`,
|
||||
}}
|
||||
>
|
||||
{renderComponent(comp)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링.
|
||||
* 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대.
|
||||
*/
|
||||
function ProportionalRenderer({
|
||||
components,
|
||||
canvasWidth,
|
||||
@@ -270,220 +92,13 @@ export function ResponsiveGridRenderer({
|
||||
canvasHeight,
|
||||
renderComponent,
|
||||
}: ResponsiveGridRendererProps) {
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
const topLevel = components.filter((c) => !c.parentId);
|
||||
const hasFullWidthComponent = topLevel.some((c) => isFullWidthComponent(c));
|
||||
|
||||
if (!isMobile && !hasFullWidthComponent) {
|
||||
return (
|
||||
<ProportionalRenderer
|
||||
components={components}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
renderComponent={renderComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const rows = groupComponentsIntoRows(topLevel);
|
||||
const processedRows: 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);
|
||||
}
|
||||
}
|
||||
|
||||
const allComps = [...fullWidthComps, ...normalComps];
|
||||
const rowMinY = allComps.length > 0 ? Math.min(...allComps.map(c => c.position.y)) : 0;
|
||||
const rowMaxBottom = allComps.length > 0 ? Math.max(...allComps.map(c => c.position.y + (c.size?.height || 40))) : 0;
|
||||
|
||||
if (fullWidthComps.length > 0 && normalComps.length > 0) {
|
||||
for (const fwComp of fullWidthComps) {
|
||||
processedRows.push({
|
||||
type: "fullwidth",
|
||||
mainComponent: fwComp,
|
||||
overlayComps: normalComps,
|
||||
normalComps: [],
|
||||
rowMinY,
|
||||
rowMaxBottom,
|
||||
});
|
||||
}
|
||||
} else if (fullWidthComps.length > 0) {
|
||||
for (const fwComp of fullWidthComps) {
|
||||
processedRows.push({
|
||||
type: "fullwidth",
|
||||
mainComponent: fwComp,
|
||||
overlayComps: [],
|
||||
normalComps: [],
|
||||
rowMinY,
|
||||
rowMaxBottom,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
processedRows.push({
|
||||
type: "normal",
|
||||
overlayComps: [],
|
||||
normalComps,
|
||||
rowMinY,
|
||||
rowMaxBottom,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-screen-runtime="true"
|
||||
className="bg-background flex h-full w-full flex-col overflow-x-hidden"
|
||||
style={{ minHeight: "200px" }}
|
||||
>
|
||||
{processedRows.map((processedRow, rowIndex) => {
|
||||
const rowMarginTop = (() => {
|
||||
if (rowIndex === 0) return 0;
|
||||
const prevRow = processedRows[rowIndex - 1];
|
||||
const prevBottom = prevRow.rowMaxBottom ?? 0;
|
||||
const currTop = processedRow.rowMinY ?? 0;
|
||||
const designGap = currTop - prevBottom;
|
||||
if (designGap <= 0) return 0;
|
||||
return Math.min(Math.max(Math.round(designGap * 0.5), 4), 48);
|
||||
})();
|
||||
|
||||
if (processedRow.type === "fullwidth" && processedRow.mainComponent) {
|
||||
return (
|
||||
<div key={`row-${rowIndex}`} style={{ marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }}>
|
||||
<FullWidthOverlayRow
|
||||
main={processedRow.mainComponent}
|
||||
overlayComps={processedRow.overlayComps}
|
||||
canvasWidth={canvasWidth}
|
||||
renderComponent={renderComponent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { normalComps } = processedRow;
|
||||
const allButtons = normalComps.every((c) => isButtonComponent(c));
|
||||
|
||||
// 데스크톱에서 버튼만 있는 행: 디자이너의 x, width를 비율로 적용
|
||||
if (allButtons && normalComps.length > 0 && !isMobile) {
|
||||
const rowHeight = Math.max(...normalComps.map(c => c.size?.height || 40));
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`row-${rowIndex}`}
|
||||
className="relative w-full flex-shrink-0"
|
||||
style={{
|
||||
height: `${rowHeight}px`,
|
||||
marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined,
|
||||
}}
|
||||
>
|
||||
{normalComps.map((component) => {
|
||||
const typeId = getComponentTypeId(component);
|
||||
const leftPct = (component.position.x / canvasWidth) * 100;
|
||||
const widthPct = ((component.size?.width || 90) / canvasWidth) * 100;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
data-component-id={component.id}
|
||||
data-component-type={typeId}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${leftPct}%`,
|
||||
width: `${widthPct}%`,
|
||||
height: `${component.size?.height || 40}px`,
|
||||
}}
|
||||
>
|
||||
{renderComponent(component)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const gap = isMobile ? 8 : getRowGap(normalComps, canvasWidth);
|
||||
|
||||
const hasFlexHeightComp = normalComps.some((c) => {
|
||||
const h = c.size?.height || 0;
|
||||
return h / canvasHeight >= 0.8;
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`row-${rowIndex}`}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap overflow-hidden",
|
||||
hasFlexHeightComp ? "min-h-0 flex-1" : "flex-shrink-0"
|
||||
)}
|
||||
style={{ gap: `${gap}px`, marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }}
|
||||
>
|
||||
{normalComps.map((component) => {
|
||||
const typeId = getComponentTypeId(component);
|
||||
const isButton = isButtonComponent(component);
|
||||
const isFullWidth = isMobile && !isButton;
|
||||
|
||||
if (isButton) {
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
data-component-id={component.id}
|
||||
data-component-type={typeId}
|
||||
className="flex-shrink-0"
|
||||
style={{
|
||||
height: component.size?.height
|
||||
? `${component.size.height}px`
|
||||
: "40px",
|
||||
}}
|
||||
>
|
||||
{renderComponent(component)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percentWidth = isFullWidth
|
||||
? 100
|
||||
: getPercentageWidth(component.size?.width || 100, canvasWidth);
|
||||
const flexBasis = isFullWidth
|
||||
? "100%"
|
||||
: `calc(${percentWidth}% - ${gap}px)`;
|
||||
|
||||
const heightPct = (component.size?.height || 0) / canvasHeight;
|
||||
const useFlexHeight = heightPct >= 0.8;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
data-component-id={component.id}
|
||||
data-component-type={typeId}
|
||||
className={cn("min-w-0 overflow-hidden", useFlexHeight && "min-h-0 flex-1")}
|
||||
style={{
|
||||
width: isFullWidth ? "100%" : undefined,
|
||||
flexBasis: useFlexHeight ? undefined : flexBasis,
|
||||
flexGrow: percentWidth,
|
||||
flexShrink: 1,
|
||||
minWidth: isMobile ? "100%" : undefined,
|
||||
minHeight: useFlexHeight ? "300px" : (component.size?.height
|
||||
? `${component.size.height}px`
|
||||
: undefined),
|
||||
height: useFlexHeight ? "100%" : "auto",
|
||||
}}
|
||||
>
|
||||
{renderComponent(component)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ProportionalRenderer
|
||||
components={components}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
renderComponent={renderComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user