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
This commit is contained in:
kjs
2026-03-12 14:19:48 +09:00
parent 966191786a
commit df47c27b77
7 changed files with 261 additions and 139 deletions

View File

@@ -91,6 +91,103 @@ const SplitPanelCellImage: React.FC<{ value: string }> = React.memo(({ value })
});
SplitPanelCellImage.displayName = "SplitPanelCellImage";
/**
* 커스텀 모드 런타임: 디자이너 좌표를 비례 스케일링하여 렌더링
*/
const ScaledCustomPanel: React.FC<{
components: PanelInlineComponent[];
formData: Record<string, any>;
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<HTMLDivElement>(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 (
<div ref={containerRef} className="relative w-full" style={{ height: `${canvasH}px` }}>
{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 (
<div
key={comp.id}
className="absolute"
style={{
left: `${(x / canvasW) * 100}%`,
top: `${y}px`,
width: `${(w / canvasW) * 100}%`,
minHeight: `${h}px`,
}}
>
<DynamicComponentRenderer
component={componentData as any}
isDesignMode={false}
isInteractive={true}
formData={formData}
onFormDataChange={onFormDataChange}
tableName={tableName}
menuObjid={restProps.menuObjid}
screenId={restProps.screenId}
userId={restProps.userId}
userName={restProps.userName}
companyCode={restProps.companyCode}
allComponents={restProps.allComponents}
selectedRowsData={restProps.selectedRowsData}
onSelectedRowsChange={restProps.onSelectedRowsChange}
/>
</div>
);
})}
</div>
);
};
/**
* SplitPanelLayout 컴포넌트
* 마스터-디테일 패턴의 좌우 분할 레이아웃
@@ -741,8 +838,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
: {
position: "relative",
width: "100%",
height: "100%",
minHeight: getHeightValue(),
height: getHeightValue(),
};
// 계층 구조 빌드 함수 (트리 구조 유지)
@@ -3073,59 +3169,28 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
{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 (
<ResponsiveGridRenderer
components={compDataList}
canvasWidth={canvasW}
canvasHeight={canvasH}
renderComponent={(comp) => (
<DynamicComponentRenderer
component={comp as any}
isDesignMode={false}
isInteractive={true}
formData={{}}
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}
onFormDataChange={(data: any) => {
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);
}
}}
/>
)}
/>
);
})()
<ScaledCustomPanel
components={componentConfig.leftPanel!.components}
formData={{}}
onFormDataChange={(data: any) => {
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}
/>
) : (
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
{componentConfig.leftPanel.components.map((comp: PanelInlineComponent) => {
@@ -3416,7 +3481,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</th>
))}
{hasGroupedLeftActions && (
<th className="px-3 py-2 text-right text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
<th className="bg-muted sticky right-0 z-10 px-3 py-2 text-right text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
</th>
)}
</tr>
@@ -3452,7 +3517,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</td>
))}
{hasGroupedLeftActions && (
<td className="px-3 py-2 text-right">
<td className="bg-card sticky right-0 z-10 px-3 py-2 text-right group-hover:bg-accent">
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
{(componentConfig.leftPanel?.showEdit !== false) && (
<button
@@ -3514,7 +3579,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</th>
))}
{hasLeftTableActions && (
<th className="px-3 py-2 text-right text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
<th className="bg-muted sticky right-0 z-10 px-3 py-2 text-right text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" style={{ width: "80px" }}>
</th>
)}
</tr>
@@ -3550,7 +3615,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</td>
))}
{hasLeftTableActions && (
<td className="px-3 py-2 text-right">
<td className="bg-card sticky right-0 z-10 px-3 py-2 text-right group-hover:bg-accent">
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
{(componentConfig.leftPanel?.showEdit !== false) && (
<button
@@ -4214,53 +4279,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
{componentConfig.rightPanel?.components && componentConfig.rightPanel.components.length > 0 ? (
!isDesignMode ? (
// 런타임: ResponsiveGridRenderer로 반응형 렌더링
(() => {
const rightComps = componentConfig.rightPanel!.components;
const canvasW = Math.max(...rightComps.map((c: PanelInlineComponent) => (c.position?.x || 0) + (c.size?.width || 200)), 800);
const canvasH = Math.max(...rightComps.map((c: PanelInlineComponent) => (c.position?.y || 0) + (c.size?.height || 100)), 400);
const compDataList = rightComps.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 (
<ResponsiveGridRenderer
components={compDataList}
canvasWidth={canvasW}
canvasHeight={canvasH}
renderComponent={(comp) => (
<DynamicComponentRenderer
component={comp as any}
isDesignMode={false}
isInteractive={true}
formData={customLeftSelectedData}
onFormDataChange={(fieldName: string, value: any) => {
setCustomLeftSelectedData((prev: Record<string, any>) => ({ ...prev, [fieldName]: value }));
}}
tableName={componentConfig.rightPanel?.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}
/>
)}
/>
);
})()
<ScaledCustomPanel
components={componentConfig.rightPanel!.components}
formData={customLeftSelectedData}
onFormDataChange={(fieldName: string, value: any) => {
setCustomLeftSelectedData((prev: Record<string, any>) => ({ ...prev, [fieldName]: value }));
}}
tableName={componentConfig.rightPanel?.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}
/>
) : (
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
{componentConfig.rightPanel.components.map((comp: PanelInlineComponent) => {