From df47c27b770fda10122988551b87636981d9bfbf Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 12 Mar 2026 14:19:48 +0900 Subject: [PATCH] feat: enhance ResponsiveGridRenderer with row margin calculations - Added rowMinY and rowMaxBottom properties to ProcessedRow for improved layout calculations. - Implemented dynamic margin adjustments between rows in the ResponsiveGridRenderer to enhance visual spacing. - Refactored TabsWidget to streamline the ResponsiveGridRenderer integration, removing unnecessary wrapper divs for cleaner structure. - Introduced ScaledCustomPanel for better handling of component rendering in split panel layouts. Made-with: Cursor --- .../screen/ResponsiveGridRenderer.tsx | 45 +++- .../components/screen/widgets/TabsWidget.tsx | 8 +- .../SplitPanelLayoutComponent.tsx | 246 ++++++++++-------- .../SplitPanelLayoutConfigPanel.tsx | 63 ++++- .../config-panels/LeftPanelConfigTab.tsx | 5 + .../config-panels/RightPanelConfigTab.tsx | 5 + .../config-panels/SharedComponents.tsx | 28 +- 7 files changed, 261 insertions(+), 139 deletions(-) diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index 8f671c21..a902d7f3 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -109,6 +109,8 @@ interface ProcessedRow { mainComponent?: ComponentData; overlayComps: ComponentData[]; normalComps: ComponentData[]; + rowMinY?: number; + rowMaxBottom?: number; } function FullWidthOverlayRow({ @@ -227,6 +229,10 @@ export function ResponsiveGridRenderer({ } } + 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({ @@ -234,6 +240,8 @@ export function ResponsiveGridRenderer({ mainComponent: fwComp, overlayComps: normalComps, normalComps: [], + rowMinY, + rowMaxBottom, }); } } else if (fullWidthComps.length > 0) { @@ -243,6 +251,8 @@ export function ResponsiveGridRenderer({ mainComponent: fwComp, overlayComps: [], normalComps: [], + rowMinY, + rowMaxBottom, }); } } else { @@ -250,6 +260,8 @@ export function ResponsiveGridRenderer({ type: "normal", overlayComps: [], normalComps, + rowMinY, + rowMaxBottom, }); } } @@ -261,15 +273,26 @@ export function ResponsiveGridRenderer({ 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 ( - +
0 ? `${rowMarginTop}px` : undefined }}> + +
); } @@ -290,7 +313,7 @@ export function ResponsiveGridRenderer({ allButtons && "justify-end px-2 py-1", hasFlexHeightComp ? "min-h-0 flex-1" : "flex-shrink-0" )} - style={{ gap: `${gap}px` }} + style={{ gap: `${gap}px`, marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }} > {normalComps.map((component) => { const typeId = getComponentTypeId(component); @@ -337,10 +360,10 @@ export function ResponsiveGridRenderer({ flexGrow: 1, flexShrink: 1, minWidth: isMobile ? "100%" : undefined, - minHeight: useFlexHeight ? "300px" : undefined, - height: useFlexHeight ? "100%" : (component.size?.height + minHeight: useFlexHeight ? "300px" : (component.size?.height ? `${component.size.height}px` - : "auto"), + : undefined), + height: useFlexHeight ? "100%" : "auto", }} > {renderComponent(component)} diff --git a/frontend/components/screen/widgets/TabsWidget.tsx b/frontend/components/screen/widgets/TabsWidget.tsx index 51060ce2..688a6ca7 100644 --- a/frontend/components/screen/widgets/TabsWidget.tsx +++ b/frontend/components/screen/widgets/TabsWidget.tsx @@ -429,8 +429,7 @@ export function TabsWidget({ })) as any; return ( -
- )} /> -
); }; @@ -498,7 +496,7 @@ export function TabsWidget({ -
+
{visibleTabs.map((tab) => { const shouldRender = mountedTabs.has(tab.id); const isActive = selectedTab === tab.id; @@ -508,7 +506,7 @@ export function TabsWidget({ key={tab.id} value={tab.id} forceMount - className={cn("flex min-h-0 flex-1 flex-col overflow-auto", !isActive && "hidden")} + className={cn("h-full overflow-auto", !isActive && "hidden")} > {shouldRender && renderTabContent(tab)} diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index d5954c6b..9ac5b01b 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -91,6 +91,103 @@ const SplitPanelCellImage: React.FC<{ value: string }> = React.memo(({ value }) }); SplitPanelCellImage.displayName = "SplitPanelCellImage"; +/** + * 커스텀 모드 런타임: 디자이너 좌표를 비례 스케일링하여 렌더링 + */ +const ScaledCustomPanel: React.FC<{ + components: PanelInlineComponent[]; + formData: Record; + onFormDataChange: (fieldName: string, value: any) => void; + tableName?: string; + menuObjid?: number; + screenId?: number; + userId?: string; + userName?: string; + companyCode?: string; + allComponents?: any; + selectedRowsData?: any[]; + onSelectedRowsChange?: any; +}> = ({ components, formData, onFormDataChange, tableName, ...restProps }) => { + const containerRef = React.useRef(null); + const [containerWidth, setContainerWidth] = React.useState(0); + + React.useEffect(() => { + const el = containerRef.current; + if (!el) return; + const ro = new ResizeObserver((entries) => { + const w = entries[0]?.contentRect.width; + if (w && w > 0) setContainerWidth(w); + }); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + const canvasW = Math.max( + ...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)), + 400, + ); + const canvasH = Math.max( + ...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)), + 200, + ); + return ( +
+ {containerWidth > 0 && + components.map((comp) => { + const x = comp.position?.x || 0; + const y = comp.position?.y || 0; + const w = comp.size?.width || 200; + const h = comp.size?.height || 36; + + const componentData = { + id: comp.id, + type: "component" as const, + componentType: comp.componentType, + label: comp.label, + position: { x, y }, + size: { width: undefined, height: h }, + componentConfig: comp.componentConfig || {}, + style: { ...(comp.style || {}), width: "100%", height: "100%" }, + tableName: comp.componentConfig?.tableName, + columnName: comp.componentConfig?.columnName, + webType: comp.componentConfig?.webType, + inputType: (comp as any).inputType || comp.componentConfig?.inputType, + }; + + return ( +
+ +
+ ); + })} +
+ ); +}; + /** * SplitPanelLayout 컴포넌트 * 마스터-디테일 패턴의 좌우 분할 레이아웃 @@ -741,8 +838,7 @@ export const SplitPanelLayoutComponent: React.FC : { position: "relative", width: "100%", - height: "100%", - minHeight: getHeightValue(), + height: getHeightValue(), }; // 계층 구조 빌드 함수 (트리 구조 유지) @@ -3073,59 +3169,28 @@ export const SplitPanelLayoutComponent: React.FC {/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */} {componentConfig.leftPanel?.components && componentConfig.leftPanel.components.length > 0 ? ( !isDesignMode ? ( - // 런타임: ResponsiveGridRenderer로 반응형 렌더링 - (() => { - const leftComps = componentConfig.leftPanel!.components; - const canvasW = Math.max(...leftComps.map((c: PanelInlineComponent) => (c.position?.x || 0) + (c.size?.width || 200)), 800); - const canvasH = Math.max(...leftComps.map((c: PanelInlineComponent) => (c.position?.y || 0) + (c.size?.height || 100)), 400); - const compDataList = leftComps.map((c: PanelInlineComponent) => ({ - id: c.id, - type: "component" as const, - componentType: c.componentType, - label: c.label, - position: c.position || { x: 0, y: 0 }, - size: c.size || { width: 400, height: 300 }, - componentConfig: c.componentConfig || {}, - style: c.style || {}, - tableName: c.componentConfig?.tableName, - columnName: c.componentConfig?.columnName, - webType: c.componentConfig?.webType, - inputType: (c as any).inputType || c.componentConfig?.inputType, - })) as any; - return ( - ( - { - if (data?.selectedRowsData && data.selectedRowsData.length > 0) { - setCustomLeftSelectedData(data.selectedRowsData[0]); - setSelectedLeftItem(data.selectedRowsData[0]); - } else if (data?.selectedRowsData && data.selectedRowsData.length === 0) { - setCustomLeftSelectedData({}); - setSelectedLeftItem(null); - } - }} - /> - )} - /> - ); - })() + { + if (data?.selectedRowsData && data.selectedRowsData.length > 0) { + setCustomLeftSelectedData(data.selectedRowsData[0]); + setSelectedLeftItem(data.selectedRowsData[0]); + } else if (data?.selectedRowsData && data.selectedRowsData.length === 0) { + setCustomLeftSelectedData({}); + setSelectedLeftItem(null); + } + }} + tableName={componentConfig.leftPanel?.tableName} + menuObjid={(props as any).menuObjid} + screenId={(props as any).screenId} + userId={(props as any).userId} + userName={(props as any).userName} + companyCode={companyCode} + allComponents={(props as any).allComponents} + selectedRowsData={localSelectedRowsData} + onSelectedRowsChange={handleLocalSelectedRowsChange} + /> ) : (
{componentConfig.leftPanel.components.map((comp: PanelInlineComponent) => { @@ -3416,7 +3481,7 @@ export const SplitPanelLayoutComponent: React.FC ))} {hasGroupedLeftActions && ( - + )} @@ -3452,7 +3517,7 @@ export const SplitPanelLayoutComponent: React.FC ))} {hasGroupedLeftActions && ( - +
{(componentConfig.leftPanel?.showEdit !== false) && (