fix: 반응형 렌더링 근본 수정 - DesktopCanvasRenderer 제거, 런타임 고정 픽셀 제거
- ResponsiveGridRenderer: DesktopCanvasRenderer(transform:scale) 제거, 모든 화면 flex 퍼센트 레이아웃 강제 - ResponsiveGridRenderer: useMemo Hook 제거로 Hook 순서 에러 해결 - ResponsiveGridRenderer: 모든 비-버튼 컴포넌트 flexGrow:1 적용 (수주관리처럼 남는 공간 채움) - ResponsiveGridRenderer: 캔버스 높이 80% 이상 컴포넌트는 flex:1로 세로 공간도 채움 - DynamicComponentRenderer: 런타임 모드에서 size.width/height를 고정 픽셀 대신 100% 사용 - ResponsiveSplitPanel: 3단계 브레이크포인트 (데스크톱 1280+, 태블릿 768-1279, 모바일 <768) Made-with: Cursor
This commit is contained in:
parent
f3bbe4af7f
commit
e231052d9f
|
|
@ -6,36 +6,45 @@ import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface ResponsiveSplitPanelProps {
|
export interface ResponsiveSplitPanelProps {
|
||||||
/** 좌측 패널 콘텐츠 */
|
|
||||||
left: ReactNode;
|
left: ReactNode;
|
||||||
/** 우측 패널 콘텐츠 */
|
|
||||||
right: ReactNode;
|
right: ReactNode;
|
||||||
|
|
||||||
/** 좌측 패널 제목 (모바일 접기/펼치기 시 표시) */
|
|
||||||
leftTitle?: string;
|
leftTitle?: string;
|
||||||
/** 좌측 패널 기본 너비 (%, 기본: 25) */
|
|
||||||
leftWidth?: number;
|
leftWidth?: number;
|
||||||
/** 좌측 패널 최소 너비 (%, 기본: 10) */
|
|
||||||
minLeftWidth?: number;
|
minLeftWidth?: number;
|
||||||
/** 좌측 패널 최대 너비 (%, 기본: 50) */
|
|
||||||
maxLeftWidth?: number;
|
maxLeftWidth?: number;
|
||||||
|
|
||||||
/** 리사이저 표시 여부 (기본: true) */
|
|
||||||
showResizer?: boolean;
|
showResizer?: boolean;
|
||||||
/** 모바일에서 좌측 패널 기본 접힘 여부 (기본: true) */
|
|
||||||
collapsedOnMobile?: boolean;
|
collapsedOnMobile?: boolean;
|
||||||
|
|
||||||
/** 컨테이너 높이 (기본: "100%") */
|
|
||||||
height?: string;
|
height?: string;
|
||||||
/** 추가 className */
|
|
||||||
className?: string;
|
className?: string;
|
||||||
/** 좌측 패널 추가 className */
|
|
||||||
leftClassName?: string;
|
leftClassName?: string;
|
||||||
/** 우측 패널 추가 className */
|
|
||||||
rightClassName?: string;
|
rightClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 1024;
|
type ViewMode = "desktop" | "tablet" | "mobile";
|
||||||
|
|
||||||
|
const DESKTOP_BREAKPOINT = 1280;
|
||||||
|
const TABLET_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
function getViewMode(width: number): ViewMode {
|
||||||
|
if (width >= DESKTOP_BREAKPOINT) return "desktop";
|
||||||
|
if (width >= TABLET_BREAKPOINT) return "tablet";
|
||||||
|
return "mobile";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdaptiveLeftWidth(
|
||||||
|
baseWidth: number,
|
||||||
|
containerWidth: number,
|
||||||
|
minPx: number
|
||||||
|
): number {
|
||||||
|
const currentPx = (baseWidth / 100) * containerWidth;
|
||||||
|
if (currentPx < minPx) {
|
||||||
|
return Math.min((minPx / containerWidth) * 100, 50);
|
||||||
|
}
|
||||||
|
return baseWidth;
|
||||||
|
}
|
||||||
|
|
||||||
export function ResponsiveSplitPanel({
|
export function ResponsiveSplitPanel({
|
||||||
left,
|
left,
|
||||||
|
|
@ -51,28 +60,76 @@ export function ResponsiveSplitPanel({
|
||||||
leftClassName,
|
leftClassName,
|
||||||
rightClassName,
|
rightClassName,
|
||||||
}: ResponsiveSplitPanelProps) {
|
}: ResponsiveSplitPanelProps) {
|
||||||
const [leftWidth, setLeftWidth] = useState(initialLeftWidth);
|
const [userLeftWidth, setUserLeftWidth] = useState(initialLeftWidth);
|
||||||
const [isMobileView, setIsMobileView] = useState(false);
|
const [viewMode, setViewMode] = useState<ViewMode>("desktop");
|
||||||
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const isDraggingRef = useRef(false);
|
const isDraggingRef = useRef(false);
|
||||||
|
const prevViewModeRef = useRef<ViewMode>("desktop");
|
||||||
|
|
||||||
// 뷰포트 감지
|
// 컨테이너 크기 + 뷰 모드 실시간 감지
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const container = containerRef.current;
|
||||||
const mobile = window.innerWidth < MOBILE_BREAKPOINT;
|
if (!container) return;
|
||||||
setIsMobileView(mobile);
|
|
||||||
if (mobile && collapsedOnMobile) {
|
|
||||||
setLeftCollapsed(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkMobile();
|
const observer = new ResizeObserver((entries) => {
|
||||||
window.addEventListener("resize", checkMobile);
|
const entry = entries[0];
|
||||||
return () => window.removeEventListener("resize", checkMobile);
|
if (!entry) return;
|
||||||
|
const w = entry.contentRect.width;
|
||||||
|
setContainerWidth(w);
|
||||||
|
|
||||||
|
const newMode = getViewMode(window.innerWidth);
|
||||||
|
setViewMode((prev) => {
|
||||||
|
if (prev !== newMode) {
|
||||||
|
if (newMode === "mobile" && collapsedOnMobile) {
|
||||||
|
setLeftCollapsed(true);
|
||||||
|
} else if (newMode === "tablet") {
|
||||||
|
setLeftCollapsed(true);
|
||||||
|
} else if (newMode === "desktop" && prev !== "desktop") {
|
||||||
|
setLeftCollapsed(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newMode;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(container);
|
||||||
|
setContainerWidth(container.offsetWidth);
|
||||||
|
const initialMode = getViewMode(window.innerWidth);
|
||||||
|
setViewMode(initialMode);
|
||||||
|
if (initialMode === "mobile" && collapsedOnMobile) {
|
||||||
|
setLeftCollapsed(true);
|
||||||
|
} else if (initialMode === "tablet") {
|
||||||
|
setLeftCollapsed(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
}, [collapsedOnMobile]);
|
}, [collapsedOnMobile]);
|
||||||
|
|
||||||
// 데스크톱 리사이저
|
// window resize도 감지 (viewport 변화)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
const newMode = getViewMode(window.innerWidth);
|
||||||
|
setViewMode((prev) => {
|
||||||
|
if (prev !== newMode) {
|
||||||
|
if (newMode === "mobile" && collapsedOnMobile) {
|
||||||
|
setLeftCollapsed(true);
|
||||||
|
} else if (newMode === "tablet") {
|
||||||
|
setLeftCollapsed(true);
|
||||||
|
} else if (newMode === "desktop") {
|
||||||
|
setLeftCollapsed(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newMode;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
|
}, [collapsedOnMobile]);
|
||||||
|
|
||||||
|
// 리사이저
|
||||||
const handleMouseDown = useCallback(() => {
|
const handleMouseDown = useCallback(() => {
|
||||||
isDraggingRef.current = true;
|
isDraggingRef.current = true;
|
||||||
document.body.style.cursor = "col-resize";
|
document.body.style.cursor = "col-resize";
|
||||||
|
|
@ -85,7 +142,7 @@ export function ResponsiveSplitPanel({
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
const pct = ((e.clientX - rect.left) / rect.width) * 100;
|
const pct = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
if (pct >= minLeftWidth && pct <= maxLeftWidth) {
|
if (pct >= minLeftWidth && pct <= maxLeftWidth) {
|
||||||
setLeftWidth(pct);
|
setUserLeftWidth(pct);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[minLeftWidth, maxLeftWidth]
|
[minLeftWidth, maxLeftWidth]
|
||||||
|
|
@ -106,11 +163,17 @@ export function ResponsiveSplitPanel({
|
||||||
};
|
};
|
||||||
}, [handleMouseMove, handleMouseUp]);
|
}, [handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
// --- 모바일 레이아웃: 세로 스택 ---
|
// 좌측 패널 최소 px 보장 (200px 이하로 줄어들지 않게)
|
||||||
if (isMobileView) {
|
const MIN_LEFT_PX = 200;
|
||||||
|
const effectiveLeftWidth =
|
||||||
|
containerWidth > 0
|
||||||
|
? getAdaptiveLeftWidth(userLeftWidth, containerWidth, MIN_LEFT_PX)
|
||||||
|
: userLeftWidth;
|
||||||
|
|
||||||
|
// --- 모바일: 세로 스택 ---
|
||||||
|
if (viewMode === "mobile") {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-0", className)} style={{ height }}>
|
<div ref={containerRef} className={cn("flex flex-col gap-0", className)} style={{ height }}>
|
||||||
{/* 좌측 패널 토글 헤더 */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setLeftCollapsed(!leftCollapsed)}
|
onClick={() => setLeftCollapsed(!leftCollapsed)}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -127,19 +190,12 @@ export function ResponsiveSplitPanel({
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 좌측 패널 (접기/펼치기) */}
|
|
||||||
{!leftCollapsed && (
|
{!leftCollapsed && (
|
||||||
<div
|
<div className={cn("max-h-[40vh] overflow-y-auto border-b", leftClassName)}>
|
||||||
className={cn(
|
|
||||||
"max-h-[40vh] overflow-y-auto border-b",
|
|
||||||
leftClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{left}
|
{left}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 우측 패널 (항상 표시) */}
|
|
||||||
<div className={cn("min-h-0 flex-1 overflow-y-auto", rightClassName)}>
|
<div className={cn("min-h-0 flex-1 overflow-y-auto", rightClassName)}>
|
||||||
{right}
|
{right}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -147,39 +203,42 @@ export function ResponsiveSplitPanel({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 데스크톱 레이아웃: 좌우 분할 ---
|
// --- 태블릿 + 데스크톱: 좌우 분할 ---
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={cn("flex gap-0 overflow-hidden", className)}
|
className={cn("flex gap-0 overflow-hidden", className)}
|
||||||
style={{ height }}
|
style={{ height }}
|
||||||
>
|
>
|
||||||
{/* 좌측 패널 (접기 가능) */}
|
{/* 좌측 패널 */}
|
||||||
{!leftCollapsed ? (
|
{!leftCollapsed ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
style={{ width: `${leftWidth}%` }}
|
style={{ width: `${effectiveLeftWidth}%` }}
|
||||||
className={cn("flex h-full flex-col overflow-hidden pr-3", leftClassName)}
|
className={cn(
|
||||||
|
"flex h-full flex-col overflow-hidden border-r pr-1 transition-[width] duration-200",
|
||||||
|
leftClassName
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex-1 overflow-y-auto">{left}</div>
|
<div className="flex-1 overflow-y-auto">{left}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 리사이저 */}
|
{/* 리사이저 (데스크톱만) */}
|
||||||
{showResizer && (
|
{showResizer && viewMode === "desktop" && (
|
||||||
<div
|
<div
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
className="group hover:bg-accent/50 relative flex h-full w-3 shrink-0 cursor-col-resize items-center justify-center border-r transition-colors"
|
className="group hover:bg-accent/50 relative flex h-full w-2 shrink-0 cursor-col-resize items-center justify-center transition-colors"
|
||||||
>
|
>
|
||||||
<GripVertical className="text-muted-foreground group-hover:text-foreground h-4 w-4 transition-colors" />
|
<GripVertical className="text-muted-foreground group-hover:text-foreground h-4 w-4 transition-colors" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-8 shrink-0 items-start justify-center border-r pt-2">
|
<div className="flex h-full w-10 shrink-0 flex-col items-center border-r pt-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6"
|
className="h-7 w-7"
|
||||||
onClick={() => setLeftCollapsed(false)}
|
onClick={() => setLeftCollapsed(false)}
|
||||||
title={`${leftTitle} 열기`}
|
title={`${leftTitle} 열기`}
|
||||||
>
|
>
|
||||||
|
|
@ -191,11 +250,15 @@ export function ResponsiveSplitPanel({
|
||||||
{/* 우측 패널 */}
|
{/* 우측 패널 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: leftCollapsed ? "calc(100% - 32px)" : `${100 - leftWidth - 1}%`,
|
width: leftCollapsed
|
||||||
|
? "calc(100% - 40px)"
|
||||||
|
: `${100 - effectiveLeftWidth}%`,
|
||||||
}}
|
}}
|
||||||
className={cn("flex h-full flex-col overflow-hidden pl-3", rightClassName)}
|
className={cn(
|
||||||
|
"flex h-full flex-col overflow-hidden pl-2 transition-[width] duration-200",
|
||||||
|
rightClassName
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{/* 데스크톱 접기 버튼 */}
|
|
||||||
{!leftCollapsed && (
|
{!leftCollapsed && (
|
||||||
<div className="mb-1 flex justify-start">
|
<div className="mb-1 flex justify-start">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useMemo, useRef, useState, useEffect } from "react";
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
import { ComponentData } from "@/types/screen";
|
import { ComponentData } from "@/types/screen";
|
||||||
import { useResponsive } from "@/lib/hooks/useResponsive";
|
import { useResponsive } from "@/lib/hooks/useResponsive";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -111,11 +111,6 @@ interface ProcessedRow {
|
||||||
normalComps: ComponentData[];
|
normalComps: ComponentData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 풀위드스 컴포넌트 + 오버레이 버튼.
|
|
||||||
* 원본 좌표 그대로 배치 → transform: scale 한 방으로 축소.
|
|
||||||
* 디자이너 미리보기와 동일한 원리.
|
|
||||||
*/
|
|
||||||
function FullWidthOverlayRow({
|
function FullWidthOverlayRow({
|
||||||
main,
|
main,
|
||||||
overlayComps,
|
overlayComps,
|
||||||
|
|
@ -150,8 +145,6 @@ function FullWidthOverlayRow({
|
||||||
const maxBtnH = Math.max(
|
const maxBtnH = Math.max(
|
||||||
...overlayComps.map((c) => c.size?.height || 40)
|
...overlayComps.map((c) => c.size?.height || 40)
|
||||||
);
|
);
|
||||||
// 버튼 중심 보정: 스케일 축소 시 버튼이 작아지므로 중심 위치가 위로 올라감
|
|
||||||
// 디자이너와 동일한 중심-대-중심 정렬을 유지하기 위해 Y 오프셋 보정
|
|
||||||
const yOffset = rawYOffset + (maxBtnH / 2) * (1 - scale);
|
const yOffset = rawYOffset + (maxBtnH / 2) * (1 - scale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -209,144 +202,57 @@ function FullWidthOverlayRow({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 데스크톱: 원본 캔버스 좌표 기반 렌더링 (transform: scale 로 축소)
|
|
||||||
* 디자이너에서 설계한 레이아웃을 그대로 유지하면서 뷰포트에 맞춤
|
|
||||||
*/
|
|
||||||
function DesktopCanvasRenderer({
|
|
||||||
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 ? Math.min(containerW / canvasWidth, 1) : 1;
|
|
||||||
const scaledHeight = canvasHeight * scale;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
data-screen-runtime="true"
|
|
||||||
className="bg-background relative w-full overflow-x-hidden"
|
|
||||||
style={{ minHeight: `${scaledHeight}px` }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: `${canvasWidth}px`,
|
|
||||||
height: `${canvasHeight}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 ? `${component.size.width}px` : "auto",
|
|
||||||
height: component.size?.height ? `${component.size.height}px` : "auto",
|
|
||||||
zIndex: component.position.z || 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderComponent(component)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ResponsiveGridRenderer({
|
export function ResponsiveGridRenderer({
|
||||||
components,
|
components,
|
||||||
canvasWidth,
|
canvasWidth,
|
||||||
canvasHeight,
|
canvasHeight,
|
||||||
renderComponent,
|
renderComponent,
|
||||||
}: ResponsiveGridRendererProps) {
|
}: ResponsiveGridRendererProps) {
|
||||||
const { isMobile, isTablet } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
|
|
||||||
// 데스크톱: 원본 캔버스 좌표 유지 (스케일링)
|
|
||||||
// 풀위드스 컴포넌트(테이블, 스플릿패널 등)가 있으면 flex 레이아웃 사용
|
|
||||||
const topLevel = components.filter((c) => !c.parentId);
|
const topLevel = components.filter((c) => !c.parentId);
|
||||||
const hasFullWidthComponent = topLevel.some((c) => isFullWidthComponent(c));
|
|
||||||
const useCanvasLayout = !isMobile && !isTablet && !hasFullWidthComponent;
|
|
||||||
|
|
||||||
if (useCanvasLayout) {
|
const rows = groupComponentsIntoRows(topLevel);
|
||||||
return (
|
const processedRows: ProcessedRow[] = [];
|
||||||
<DesktopCanvasRenderer
|
|
||||||
components={components}
|
|
||||||
canvasWidth={canvasWidth}
|
|
||||||
canvasHeight={canvasHeight}
|
|
||||||
renderComponent={renderComponent}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedRows = useMemo(() => {
|
for (const row of rows) {
|
||||||
const rows = groupComponentsIntoRows(topLevel);
|
const fullWidthComps: ComponentData[] = [];
|
||||||
|
const normalComps: ComponentData[] = [];
|
||||||
|
|
||||||
const result: ProcessedRow[] = [];
|
for (const comp of row) {
|
||||||
for (const row of rows) {
|
if (isFullWidthComponent(comp)) {
|
||||||
const fullWidthComps: ComponentData[] = [];
|
fullWidthComps.push(comp);
|
||||||
const normalComps: ComponentData[] = [];
|
|
||||||
|
|
||||||
for (const comp of row) {
|
|
||||||
if (isFullWidthComponent(comp)) {
|
|
||||||
fullWidthComps.push(comp);
|
|
||||||
} else {
|
|
||||||
normalComps.push(comp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullWidthComps.length > 0 && normalComps.length > 0) {
|
|
||||||
for (const fwComp of fullWidthComps) {
|
|
||||||
result.push({
|
|
||||||
type: "fullwidth",
|
|
||||||
mainComponent: fwComp,
|
|
||||||
overlayComps: normalComps,
|
|
||||||
normalComps: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (fullWidthComps.length > 0) {
|
|
||||||
for (const fwComp of fullWidthComps) {
|
|
||||||
result.push({
|
|
||||||
type: "fullwidth",
|
|
||||||
mainComponent: fwComp,
|
|
||||||
overlayComps: [],
|
|
||||||
normalComps: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
result.push({
|
normalComps.push(comp);
|
||||||
type: "normal",
|
|
||||||
overlayComps: [],
|
|
||||||
normalComps,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}, [topLevel]);
|
if (fullWidthComps.length > 0 && normalComps.length > 0) {
|
||||||
|
for (const fwComp of fullWidthComps) {
|
||||||
|
processedRows.push({
|
||||||
|
type: "fullwidth",
|
||||||
|
mainComponent: fwComp,
|
||||||
|
overlayComps: normalComps,
|
||||||
|
normalComps: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (fullWidthComps.length > 0) {
|
||||||
|
for (const fwComp of fullWidthComps) {
|
||||||
|
processedRows.push({
|
||||||
|
type: "fullwidth",
|
||||||
|
mainComponent: fwComp,
|
||||||
|
overlayComps: [],
|
||||||
|
normalComps: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
processedRows.push({
|
||||||
|
type: "normal",
|
||||||
|
overlayComps: [],
|
||||||
|
normalComps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -371,12 +277,18 @@ export function ResponsiveGridRenderer({
|
||||||
const allButtons = normalComps.every((c) => isButtonComponent(c));
|
const allButtons = normalComps.every((c) => isButtonComponent(c));
|
||||||
const gap = isMobile ? 8 : allButtons ? 8 : getRowGap(normalComps, canvasWidth);
|
const gap = isMobile ? 8 : allButtons ? 8 : getRowGap(normalComps, canvasWidth);
|
||||||
|
|
||||||
|
const hasFlexHeightComp = normalComps.some((c) => {
|
||||||
|
const h = c.size?.height || 0;
|
||||||
|
return h / canvasHeight >= 0.8;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`row-${rowIndex}`}
|
key={`row-${rowIndex}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-shrink-0 flex-wrap overflow-hidden",
|
"flex w-full flex-wrap overflow-hidden",
|
||||||
allButtons && "justify-end px-2 py-1"
|
allButtons && "justify-end px-2 py-1",
|
||||||
|
hasFlexHeightComp ? "min-h-0 flex-1" : "flex-shrink-0"
|
||||||
)}
|
)}
|
||||||
style={{ gap: `${gap}px` }}
|
style={{ gap: `${gap}px` }}
|
||||||
>
|
>
|
||||||
|
|
@ -409,22 +321,26 @@ export function ResponsiveGridRenderer({
|
||||||
const flexBasis = isFullWidth
|
const flexBasis = isFullWidth
|
||||||
? "100%"
|
? "100%"
|
||||||
: `calc(${percentWidth}% - ${gap}px)`;
|
: `calc(${percentWidth}% - ${gap}px)`;
|
||||||
const compFlexGrow = shouldFlexGrow(component);
|
|
||||||
|
const heightPct = (component.size?.height || 0) / canvasHeight;
|
||||||
|
const useFlexHeight = heightPct >= 0.8;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={component.id}
|
key={component.id}
|
||||||
data-component-id={component.id}
|
data-component-id={component.id}
|
||||||
data-component-type={typeId}
|
data-component-type={typeId}
|
||||||
className="min-w-0 flex-shrink-0 overflow-hidden"
|
className={cn("min-w-0 overflow-hidden", useFlexHeight && "min-h-0 flex-1")}
|
||||||
style={{
|
style={{
|
||||||
width: isFullWidth ? "100%" : undefined,
|
width: isFullWidth ? "100%" : undefined,
|
||||||
flexBasis,
|
flexBasis: useFlexHeight ? undefined : flexBasis,
|
||||||
flexGrow: isFullWidth || compFlexGrow ? 1 : 0,
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
minWidth: isMobile ? "100%" : undefined,
|
minWidth: isMobile ? "100%" : undefined,
|
||||||
height: component.size?.height
|
minHeight: useFlexHeight ? "300px" : undefined,
|
||||||
|
height: useFlexHeight ? "100%" : (component.size?.height
|
||||||
? `${component.size.height}px`
|
? `${component.size.height}px`
|
||||||
: "auto",
|
: "auto"),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderComponent(component)}
|
{renderComponent(component)}
|
||||||
|
|
|
||||||
|
|
@ -662,11 +662,15 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// size.width와 size.height를 style.width와 style.height로 변환
|
const isRuntimeMode = !(props.isDesignMode);
|
||||||
const finalStyle: React.CSSProperties = {
|
const finalStyle: React.CSSProperties = {
|
||||||
...component.style,
|
...component.style,
|
||||||
width: component.size?.width ? `${component.size.width}px` : component.style?.width,
|
width: isRuntimeMode
|
||||||
height: component.size?.height ? `${component.size.height}px` : component.style?.height,
|
? "100%"
|
||||||
|
: (component.size?.width ? `${component.size.width}px` : component.style?.width),
|
||||||
|
height: isRuntimeMode
|
||||||
|
? "100%"
|
||||||
|
: (component.size?.height ? `${component.size.height}px` : component.style?.height),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🆕 엔티티 검색 컴포넌트는 componentConfig.tableName을 사용해야 함 (화면 테이블이 아닌 검색 대상 테이블)
|
// 🆕 엔티티 검색 컴포넌트는 componentConfig.tableName을 사용해야 함 (화면 테이블이 아닌 검색 대상 테이블)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue