2026-03-07 06:53:06 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-03-11 11:53:29 +09:00
|
|
|
import React, { useRef, useState, useEffect } from "react";
|
2026-03-07 06:53:06 +09:00
|
|
|
import { ComponentData } from "@/types/screen";
|
|
|
|
|
|
|
|
|
|
interface ResponsiveGridRendererProps {
|
|
|
|
|
components: ComponentData[];
|
|
|
|
|
canvasWidth: number;
|
|
|
|
|
canvasHeight: number;
|
|
|
|
|
renderComponent: (component: ComponentData) => React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getComponentTypeId(component: ComponentData): string {
|
2026-03-09 13:33:01 +09:00
|
|
|
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 || "";
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 17:15:12 +09:00
|
|
|
/**
|
|
|
|
|
* 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링.
|
|
|
|
|
* 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대.
|
|
|
|
|
*/
|
2026-03-12 13:27:01 +09:00
|
|
|
function ProportionalRenderer({
|
|
|
|
|
components,
|
|
|
|
|
canvasWidth,
|
|
|
|
|
canvasHeight,
|
|
|
|
|
renderComponent,
|
|
|
|
|
}: ResponsiveGridRendererProps) {
|
|
|
|
|
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 topLevel = components.filter((c) => !c.parentId);
|
|
|
|
|
const ratio = containerW > 0 ? containerW / canvasWidth : 1;
|
|
|
|
|
|
|
|
|
|
const maxBottom = topLevel.reduce((max, c) => {
|
|
|
|
|
const bottom = c.position.y + (c.size?.height || 40);
|
|
|
|
|
return Math.max(max, bottom);
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
data-screen-runtime="true"
|
|
|
|
|
className="bg-background relative w-full overflow-x-hidden"
|
|
|
|
|
style={{ minHeight: containerW > 0 ? `${maxBottom * ratio}px` : "200px" }}
|
|
|
|
|
>
|
|
|
|
|
{containerW > 0 &&
|
|
|
|
|
topLevel.map((component) => {
|
|
|
|
|
const typeId = getComponentTypeId(component);
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={component.id}
|
|
|
|
|
data-component-id={component.id}
|
|
|
|
|
data-component-type={typeId}
|
|
|
|
|
style={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
left: `${(component.position.x / canvasWidth) * 100}%`,
|
|
|
|
|
top: `${component.position.y * ratio}px`,
|
|
|
|
|
width: `${((component.size?.width || 100) / canvasWidth) * 100}%`,
|
|
|
|
|
height: `${(component.size?.height || 40) * ratio}px`,
|
|
|
|
|
zIndex: component.position.z || 1,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{renderComponent(component)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 06:53:06 +09:00
|
|
|
export function ResponsiveGridRenderer({
|
|
|
|
|
components,
|
|
|
|
|
canvasWidth,
|
|
|
|
|
canvasHeight,
|
|
|
|
|
renderComponent,
|
|
|
|
|
}: ResponsiveGridRendererProps) {
|
|
|
|
|
return (
|
2026-03-16 17:15:12 +09:00
|
|
|
<ProportionalRenderer
|
|
|
|
|
components={components}
|
|
|
|
|
canvasWidth={canvasWidth}
|
|
|
|
|
canvasHeight={canvasHeight}
|
|
|
|
|
renderComponent={renderComponent}
|
|
|
|
|
/>
|
2026-03-07 06:53:06 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default ResponsiveGridRenderer;
|