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:
parent
966191786a
commit
df47c27b77
|
|
@ -109,6 +109,8 @@ interface ProcessedRow {
|
||||||
mainComponent?: ComponentData;
|
mainComponent?: ComponentData;
|
||||||
overlayComps: ComponentData[];
|
overlayComps: ComponentData[];
|
||||||
normalComps: ComponentData[];
|
normalComps: ComponentData[];
|
||||||
|
rowMinY?: number;
|
||||||
|
rowMaxBottom?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FullWidthOverlayRow({
|
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) {
|
if (fullWidthComps.length > 0 && normalComps.length > 0) {
|
||||||
for (const fwComp of fullWidthComps) {
|
for (const fwComp of fullWidthComps) {
|
||||||
processedRows.push({
|
processedRows.push({
|
||||||
|
|
@ -234,6 +240,8 @@ export function ResponsiveGridRenderer({
|
||||||
mainComponent: fwComp,
|
mainComponent: fwComp,
|
||||||
overlayComps: normalComps,
|
overlayComps: normalComps,
|
||||||
normalComps: [],
|
normalComps: [],
|
||||||
|
rowMinY,
|
||||||
|
rowMaxBottom,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (fullWidthComps.length > 0) {
|
} else if (fullWidthComps.length > 0) {
|
||||||
|
|
@ -243,6 +251,8 @@ export function ResponsiveGridRenderer({
|
||||||
mainComponent: fwComp,
|
mainComponent: fwComp,
|
||||||
overlayComps: [],
|
overlayComps: [],
|
||||||
normalComps: [],
|
normalComps: [],
|
||||||
|
rowMinY,
|
||||||
|
rowMaxBottom,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -250,6 +260,8 @@ export function ResponsiveGridRenderer({
|
||||||
type: "normal",
|
type: "normal",
|
||||||
overlayComps: [],
|
overlayComps: [],
|
||||||
normalComps,
|
normalComps,
|
||||||
|
rowMinY,
|
||||||
|
rowMaxBottom,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -261,15 +273,26 @@ export function ResponsiveGridRenderer({
|
||||||
style={{ minHeight: "200px" }}
|
style={{ minHeight: "200px" }}
|
||||||
>
|
>
|
||||||
{processedRows.map((processedRow, rowIndex) => {
|
{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) {
|
if (processedRow.type === "fullwidth" && processedRow.mainComponent) {
|
||||||
return (
|
return (
|
||||||
<FullWidthOverlayRow
|
<div key={`row-${rowIndex}`} style={{ marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }}>
|
||||||
key={`row-${rowIndex}`}
|
<FullWidthOverlayRow
|
||||||
main={processedRow.mainComponent}
|
main={processedRow.mainComponent}
|
||||||
overlayComps={processedRow.overlayComps}
|
overlayComps={processedRow.overlayComps}
|
||||||
canvasWidth={canvasWidth}
|
canvasWidth={canvasWidth}
|
||||||
renderComponent={renderComponent}
|
renderComponent={renderComponent}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,7 +313,7 @@ export function ResponsiveGridRenderer({
|
||||||
allButtons && "justify-end px-2 py-1",
|
allButtons && "justify-end px-2 py-1",
|
||||||
hasFlexHeightComp ? "min-h-0 flex-1" : "flex-shrink-0"
|
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) => {
|
{normalComps.map((component) => {
|
||||||
const typeId = getComponentTypeId(component);
|
const typeId = getComponentTypeId(component);
|
||||||
|
|
@ -337,10 +360,10 @@ export function ResponsiveGridRenderer({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
minWidth: isMobile ? "100%" : undefined,
|
minWidth: isMobile ? "100%" : undefined,
|
||||||
minHeight: useFlexHeight ? "300px" : undefined,
|
minHeight: useFlexHeight ? "300px" : (component.size?.height
|
||||||
height: useFlexHeight ? "100%" : (component.size?.height
|
|
||||||
? `${component.size.height}px`
|
? `${component.size.height}px`
|
||||||
: "auto"),
|
: undefined),
|
||||||
|
height: useFlexHeight ? "100%" : "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderComponent(component)}
|
{renderComponent(component)}
|
||||||
|
|
|
||||||
|
|
@ -429,8 +429,7 @@ export function TabsWidget({
|
||||||
})) as any;
|
})) as any;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<ResponsiveGridRenderer
|
||||||
<ResponsiveGridRenderer
|
|
||||||
components={componentDataList}
|
components={componentDataList}
|
||||||
canvasWidth={canvasWidth}
|
canvasWidth={canvasWidth}
|
||||||
canvasHeight={canvasHeight}
|
canvasHeight={canvasHeight}
|
||||||
|
|
@ -453,7 +452,6 @@ export function TabsWidget({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -498,7 +496,7 @@ export function TabsWidget({
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex flex-1 flex-col overflow-auto">
|
<div className="relative flex-1 overflow-auto">
|
||||||
{visibleTabs.map((tab) => {
|
{visibleTabs.map((tab) => {
|
||||||
const shouldRender = mountedTabs.has(tab.id);
|
const shouldRender = mountedTabs.has(tab.id);
|
||||||
const isActive = selectedTab === tab.id;
|
const isActive = selectedTab === tab.id;
|
||||||
|
|
@ -508,7 +506,7 @@ export function TabsWidget({
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
value={tab.id}
|
value={tab.id}
|
||||||
forceMount
|
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)}
|
{shouldRender && renderTabContent(tab)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,103 @@ const SplitPanelCellImage: React.FC<{ value: string }> = React.memo(({ value })
|
||||||
});
|
});
|
||||||
SplitPanelCellImage.displayName = "SplitPanelCellImage";
|
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 컴포넌트
|
* SplitPanelLayout 컴포넌트
|
||||||
* 마스터-디테일 패턴의 좌우 분할 레이아웃
|
* 마스터-디테일 패턴의 좌우 분할 레이아웃
|
||||||
|
|
@ -741,8 +838,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
: {
|
: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: getHeightValue(),
|
||||||
minHeight: getHeightValue(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 계층 구조 빌드 함수 (트리 구조 유지)
|
// 계층 구조 빌드 함수 (트리 구조 유지)
|
||||||
|
|
@ -3073,59 +3169,28 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
|
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
|
||||||
{componentConfig.leftPanel?.components && componentConfig.leftPanel.components.length > 0 ? (
|
{componentConfig.leftPanel?.components && componentConfig.leftPanel.components.length > 0 ? (
|
||||||
!isDesignMode ? (
|
!isDesignMode ? (
|
||||||
// 런타임: ResponsiveGridRenderer로 반응형 렌더링
|
<ScaledCustomPanel
|
||||||
(() => {
|
components={componentConfig.leftPanel!.components}
|
||||||
const leftComps = componentConfig.leftPanel!.components;
|
formData={{}}
|
||||||
const canvasW = Math.max(...leftComps.map((c: PanelInlineComponent) => (c.position?.x || 0) + (c.size?.width || 200)), 800);
|
onFormDataChange={(data: any) => {
|
||||||
const canvasH = Math.max(...leftComps.map((c: PanelInlineComponent) => (c.position?.y || 0) + (c.size?.height || 100)), 400);
|
if (data?.selectedRowsData && data.selectedRowsData.length > 0) {
|
||||||
const compDataList = leftComps.map((c: PanelInlineComponent) => ({
|
setCustomLeftSelectedData(data.selectedRowsData[0]);
|
||||||
id: c.id,
|
setSelectedLeftItem(data.selectedRowsData[0]);
|
||||||
type: "component" as const,
|
} else if (data?.selectedRowsData && data.selectedRowsData.length === 0) {
|
||||||
componentType: c.componentType,
|
setCustomLeftSelectedData({});
|
||||||
label: c.label,
|
setSelectedLeftItem(null);
|
||||||
position: c.position || { x: 0, y: 0 },
|
}
|
||||||
size: c.size || { width: 400, height: 300 },
|
}}
|
||||||
componentConfig: c.componentConfig || {},
|
tableName={componentConfig.leftPanel?.tableName}
|
||||||
style: c.style || {},
|
menuObjid={(props as any).menuObjid}
|
||||||
tableName: c.componentConfig?.tableName,
|
screenId={(props as any).screenId}
|
||||||
columnName: c.componentConfig?.columnName,
|
userId={(props as any).userId}
|
||||||
webType: c.componentConfig?.webType,
|
userName={(props as any).userName}
|
||||||
inputType: (c as any).inputType || c.componentConfig?.inputType,
|
companyCode={companyCode}
|
||||||
})) as any;
|
allComponents={(props as any).allComponents}
|
||||||
return (
|
selectedRowsData={localSelectedRowsData}
|
||||||
<ResponsiveGridRenderer
|
onSelectedRowsChange={handleLocalSelectedRowsChange}
|
||||||
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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
) : (
|
) : (
|
||||||
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
|
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
|
||||||
{componentConfig.leftPanel.components.map((comp: PanelInlineComponent) => {
|
{componentConfig.leftPanel.components.map((comp: PanelInlineComponent) => {
|
||||||
|
|
@ -3416,7 +3481,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{hasGroupedLeftActions && (
|
{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>
|
</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -3452,7 +3517,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{hasGroupedLeftActions && (
|
{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">
|
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
{(componentConfig.leftPanel?.showEdit !== false) && (
|
{(componentConfig.leftPanel?.showEdit !== false) && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -3514,7 +3579,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{hasLeftTableActions && (
|
{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>
|
</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -3550,7 +3615,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{hasLeftTableActions && (
|
{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">
|
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
{(componentConfig.leftPanel?.showEdit !== false) && (
|
{(componentConfig.leftPanel?.showEdit !== false) && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -4214,53 +4279,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
|
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
|
||||||
{componentConfig.rightPanel?.components && componentConfig.rightPanel.components.length > 0 ? (
|
{componentConfig.rightPanel?.components && componentConfig.rightPanel.components.length > 0 ? (
|
||||||
!isDesignMode ? (
|
!isDesignMode ? (
|
||||||
// 런타임: ResponsiveGridRenderer로 반응형 렌더링
|
<ScaledCustomPanel
|
||||||
(() => {
|
components={componentConfig.rightPanel!.components}
|
||||||
const rightComps = componentConfig.rightPanel!.components;
|
formData={customLeftSelectedData}
|
||||||
const canvasW = Math.max(...rightComps.map((c: PanelInlineComponent) => (c.position?.x || 0) + (c.size?.width || 200)), 800);
|
onFormDataChange={(fieldName: string, value: any) => {
|
||||||
const canvasH = Math.max(...rightComps.map((c: PanelInlineComponent) => (c.position?.y || 0) + (c.size?.height || 100)), 400);
|
setCustomLeftSelectedData((prev: Record<string, any>) => ({ ...prev, [fieldName]: value }));
|
||||||
const compDataList = rightComps.map((c: PanelInlineComponent) => ({
|
}}
|
||||||
id: c.id,
|
tableName={componentConfig.rightPanel?.tableName || componentConfig.leftPanel?.tableName}
|
||||||
type: "component" as const,
|
menuObjid={(props as any).menuObjid}
|
||||||
componentType: c.componentType,
|
screenId={(props as any).screenId}
|
||||||
label: c.label,
|
userId={(props as any).userId}
|
||||||
position: c.position || { x: 0, y: 0 },
|
userName={(props as any).userName}
|
||||||
size: c.size || { width: 400, height: 300 },
|
companyCode={companyCode}
|
||||||
componentConfig: c.componentConfig || {},
|
allComponents={(props as any).allComponents}
|
||||||
style: c.style || {},
|
selectedRowsData={localSelectedRowsData}
|
||||||
tableName: c.componentConfig?.tableName,
|
onSelectedRowsChange={handleLocalSelectedRowsChange}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
) : (
|
) : (
|
||||||
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
|
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
|
||||||
{componentConfig.rightPanel.components.map((comp: PanelInlineComponent) => {
|
{componentConfig.rightPanel.components.map((comp: PanelInlineComponent) => {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ function SortableColumnRow({
|
||||||
onLabelChange,
|
onLabelChange,
|
||||||
onWidthChange,
|
onWidthChange,
|
||||||
onFormatChange,
|
onFormatChange,
|
||||||
|
onSuffixChange,
|
||||||
onRemove,
|
onRemove,
|
||||||
onShowInSummaryChange,
|
onShowInSummaryChange,
|
||||||
onShowInDetailChange,
|
onShowInDetailChange,
|
||||||
|
|
@ -87,6 +88,7 @@ function SortableColumnRow({
|
||||||
onLabelChange: (value: string) => void;
|
onLabelChange: (value: string) => void;
|
||||||
onWidthChange: (value: number) => void;
|
onWidthChange: (value: number) => void;
|
||||||
onFormatChange: (checked: boolean) => void;
|
onFormatChange: (checked: boolean) => void;
|
||||||
|
onSuffixChange?: (value: string) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onShowInSummaryChange?: (checked: boolean) => void;
|
onShowInSummaryChange?: (checked: boolean) => void;
|
||||||
onShowInDetailChange?: (checked: boolean) => void;
|
onShowInDetailChange?: (checked: boolean) => void;
|
||||||
|
|
@ -177,15 +179,24 @@ function SortableColumnRow({
|
||||||
className="h-6 w-14 shrink-0 text-xs"
|
className="h-6 w-14 shrink-0 text-xs"
|
||||||
/>
|
/>
|
||||||
{isNumeric && (
|
{isNumeric && (
|
||||||
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
<>
|
||||||
<input
|
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
||||||
type="checkbox"
|
<input
|
||||||
checked={col.format?.thousandSeparator ?? false}
|
type="checkbox"
|
||||||
onChange={(e) => onFormatChange(e.target.checked)}
|
checked={col.format?.thousandSeparator ?? false}
|
||||||
className="h-3 w-3"
|
onChange={(e) => onFormatChange(e.target.checked)}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
,
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={col.format?.suffix || ""}
|
||||||
|
onChange={(e) => onSuffixChange?.(e.target.value)}
|
||||||
|
placeholder="단위"
|
||||||
|
title="값 뒤에 붙는 단위 (예: mm, kg, %)"
|
||||||
|
className="h-6 w-10 shrink-0 text-[10px]"
|
||||||
/>
|
/>
|
||||||
,
|
</>
|
||||||
</label>
|
|
||||||
)}
|
)}
|
||||||
{/* 헤더/상세 표시 토글 */}
|
{/* 헤더/상세 표시 토글 */}
|
||||||
{onShowInSummaryChange && (
|
{onShowInSummaryChange && (
|
||||||
|
|
@ -818,6 +829,18 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
};
|
};
|
||||||
updateTab({ columns: newColumns });
|
updateTab({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
|
onSuffixChange={(value) => {
|
||||||
|
const newColumns = [...selectedColumns];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
format: {
|
||||||
|
...newColumns[index].format,
|
||||||
|
type: "number",
|
||||||
|
suffix: value || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
updateTab({ columns: newColumns });
|
||||||
|
}}
|
||||||
onRemove={() => updateTab({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
onRemove={() => updateTab({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
||||||
onShowInSummaryChange={(checked) => {
|
onShowInSummaryChange={(checked) => {
|
||||||
const newColumns = [...selectedColumns];
|
const newColumns = [...selectedColumns];
|
||||||
|
|
@ -2330,6 +2353,18 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
};
|
};
|
||||||
updateLeftPanel({ columns: newColumns });
|
updateLeftPanel({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
|
onSuffixChange={(value) => {
|
||||||
|
const newColumns = [...selectedColumns];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
format: {
|
||||||
|
...newColumns[index].format,
|
||||||
|
type: "number",
|
||||||
|
suffix: value || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
updateLeftPanel({ columns: newColumns });
|
||||||
|
}}
|
||||||
onRemove={() =>
|
onRemove={() =>
|
||||||
updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })
|
updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })
|
||||||
}
|
}
|
||||||
|
|
@ -2988,6 +3023,18 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||||
};
|
};
|
||||||
updateRightPanel({ columns: newColumns });
|
updateRightPanel({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
|
onSuffixChange={(value) => {
|
||||||
|
const newColumns = [...selectedColumns];
|
||||||
|
newColumns[index] = {
|
||||||
|
...newColumns[index],
|
||||||
|
format: {
|
||||||
|
...newColumns[index].format,
|
||||||
|
type: "number",
|
||||||
|
suffix: value || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
updateRightPanel({ columns: newColumns });
|
||||||
|
}}
|
||||||
onRemove={() =>
|
onRemove={() =>
|
||||||
updateRightPanel({ columns: selectedColumns.filter((_, i) => i !== index) })
|
updateRightPanel({ columns: selectedColumns.filter((_, i) => i !== index) })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,11 @@ export const LeftPanelConfigTab: React.FC<LeftPanelConfigTabProps> = ({
|
||||||
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } };
|
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } };
|
||||||
updateLeftPanel({ columns: newColumns });
|
updateLeftPanel({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
|
onSuffixChange={(value) => {
|
||||||
|
const newColumns = [...selectedColumns];
|
||||||
|
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", suffix: value || undefined } };
|
||||||
|
updateLeftPanel({ columns: newColumns });
|
||||||
|
}}
|
||||||
onRemove={() => updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
onRemove={() => updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,11 @@ export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
|
||||||
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } };
|
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } };
|
||||||
updateRightPanel({ columns: newColumns });
|
updateRightPanel({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
|
onSuffixChange={(value) => {
|
||||||
|
const newColumns = [...selectedColumns];
|
||||||
|
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", suffix: value || undefined } };
|
||||||
|
updateRightPanel({ columns: newColumns });
|
||||||
|
}}
|
||||||
onRemove={() => updateRightPanel({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
onRemove={() => updateRightPanel({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
||||||
onShowInSummaryChange={(checked) => {
|
onShowInSummaryChange={(checked) => {
|
||||||
const newColumns = [...selectedColumns];
|
const newColumns = [...selectedColumns];
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { Check, ChevronsUpDown, GripVertical, Link2, X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function SortableColumnRow({
|
export function SortableColumnRow({
|
||||||
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove, onShowInSummaryChange, onShowInDetailChange,
|
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onSuffixChange, onRemove, onShowInSummaryChange, onShowInDetailChange,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean };
|
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean };
|
||||||
|
|
@ -23,6 +23,7 @@ export function SortableColumnRow({
|
||||||
onLabelChange: (value: string) => void;
|
onLabelChange: (value: string) => void;
|
||||||
onWidthChange: (value: number) => void;
|
onWidthChange: (value: number) => void;
|
||||||
onFormatChange: (checked: boolean) => void;
|
onFormatChange: (checked: boolean) => void;
|
||||||
|
onSuffixChange?: (value: string) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onShowInSummaryChange?: (checked: boolean) => void;
|
onShowInSummaryChange?: (checked: boolean) => void;
|
||||||
onShowInDetailChange?: (checked: boolean) => void;
|
onShowInDetailChange?: (checked: boolean) => void;
|
||||||
|
|
@ -61,15 +62,24 @@ export function SortableColumnRow({
|
||||||
className="h-6 w-14 shrink-0 text-xs"
|
className="h-6 w-14 shrink-0 text-xs"
|
||||||
/>
|
/>
|
||||||
{isNumeric && (
|
{isNumeric && (
|
||||||
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
<>
|
||||||
<input
|
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
||||||
type="checkbox"
|
<input
|
||||||
checked={col.format?.thousandSeparator ?? false}
|
type="checkbox"
|
||||||
onChange={(e) => onFormatChange(e.target.checked)}
|
checked={col.format?.thousandSeparator ?? false}
|
||||||
className="h-3 w-3"
|
onChange={(e) => onFormatChange(e.target.checked)}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
,
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={col.format?.suffix || ""}
|
||||||
|
onChange={(e) => onSuffixChange?.(e.target.value)}
|
||||||
|
placeholder="단위"
|
||||||
|
title="값 뒤에 붙는 단위 (예: mm, kg, %)"
|
||||||
|
className="h-6 w-10 shrink-0 text-[10px]"
|
||||||
/>
|
/>
|
||||||
,
|
</>
|
||||||
</label>
|
|
||||||
)}
|
)}
|
||||||
{onShowInSummaryChange && (
|
{onShowInSummaryChange && (
|
||||||
<label className="flex shrink-0 cursor-pointer items-center gap-0.5 text-[10px]" title="테이블 헤더에 표시">
|
<label className="flex shrink-0 cursor-pointer items-center gap-0.5 text-[10px]" title="테이블 헤더에 표시">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue