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-18 00:05:40 +09:00
|
|
|
* 디자이너 절대좌표를 캔버스 대비 비율(%)로 변환하여 렌더링.
|
|
|
|
|
* 가로: 컨테이너 너비 대비 % → 반응형 스케일
|
|
|
|
|
* 세로: 컨테이너 높이 대비 % → 뷰포트에 맞게 자동 조절
|
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);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
data-screen-runtime="true"
|
2026-03-18 00:05:40 +09:00
|
|
|
className="bg-background relative h-full w-full overflow-hidden"
|
2026-03-12 13:27:01 +09:00
|
|
|
>
|
2026-03-17 22:49:42 +09:00
|
|
|
{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}%`,
|
2026-03-18 00:05:40 +09:00
|
|
|
top: `${(component.position.y / canvasHeight) * 100}%`,
|
2026-03-17 22:49:42 +09:00
|
|
|
width: `${((component.size?.width || 100) / canvasWidth) * 100}%`,
|
2026-03-18 00:05:40 +09:00
|
|
|
height: `${((component.size?.height || 40) / canvasHeight) * 100}%`,
|
2026-03-17 22:49:42 +09:00
|
|
|
zIndex: component.position.z || 1,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{renderComponent(component)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-03-12 13:27:01 +09:00
|
|
|
</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;
|