118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import React, { useRef, useState, useEffect } from "react";
|
|
import { ComponentData } from "@/types/screen";
|
|
|
|
interface ResponsiveGridRendererProps {
|
|
components: ComponentData[];
|
|
canvasWidth: number;
|
|
canvasHeight: number;
|
|
renderComponent: (component: ComponentData) => React.ReactNode;
|
|
}
|
|
|
|
function getComponentTypeId(component: ComponentData): string {
|
|
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 || "";
|
|
}
|
|
|
|
/**
|
|
* CSS transform scale 기반 렌더링.
|
|
* 디자이너와 동일하게 캔버스 해상도(px)로 레이아웃 후 CSS scale로 축소/확대.
|
|
* 텍스트, 패딩, 버튼 등 모든 요소가 균일하게 스케일링되어 WYSIWYG 보장.
|
|
*/
|
|
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 scale = 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 w-full overflow-hidden"
|
|
style={{ height: containerW > 0 ? `${maxBottom * scale}px` : "200px" }}
|
|
>
|
|
{containerW > 0 && (
|
|
<div
|
|
style={{
|
|
width: `${canvasWidth}px`,
|
|
height: `${maxBottom}px`,
|
|
transform: `scale(${scale})`,
|
|
transformOrigin: "top left",
|
|
position: "relative",
|
|
}}
|
|
>
|
|
{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}px`,
|
|
top: `${component.position.y}px`,
|
|
width: `${component.size?.width || 100}px`,
|
|
height: `${component.size?.height || 40}px`,
|
|
zIndex: component.position.z || 1,
|
|
}}
|
|
>
|
|
{renderComponent(component)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ResponsiveGridRenderer({
|
|
components,
|
|
canvasWidth,
|
|
canvasHeight,
|
|
renderComponent,
|
|
}: ResponsiveGridRendererProps) {
|
|
return (
|
|
<ProportionalRenderer
|
|
components={components}
|
|
canvasWidth={canvasWidth}
|
|
canvasHeight={canvasHeight}
|
|
renderComponent={renderComponent}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default ResponsiveGridRenderer;
|