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;
|
||||
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 (
|
||||
<FullWidthOverlayRow
|
||||
key={`row-${rowIndex}`}
|
||||
main={processedRow.mainComponent}
|
||||
overlayComps={processedRow.overlayComps}
|
||||
canvasWidth={canvasWidth}
|
||||
renderComponent={renderComponent}
|
||||
/>
|
||||
<div key={`row-${rowIndex}`} style={{ marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }}>
|
||||
<FullWidthOverlayRow
|
||||
main={processedRow.mainComponent}
|
||||
overlayComps={processedRow.overlayComps}
|
||||
canvasWidth={canvasWidth}
|
||||
renderComponent={renderComponent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -429,8 +429,7 @@ export function TabsWidget({
|
|||
})) as any;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<ResponsiveGridRenderer
|
||||
<ResponsiveGridRenderer
|
||||
components={componentDataList}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
|
|
@ -453,7 +452,6 @@ export function TabsWidget({
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -498,7 +496,7 @@ export function TabsWidget({
|
|||
</TabsList>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-1 flex-col overflow-auto">
|
||||
<div className="relative flex-1 overflow-auto">
|
||||
{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)}
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ function SortableColumnRow({
|
|||
onLabelChange,
|
||||
onWidthChange,
|
||||
onFormatChange,
|
||||
onSuffixChange,
|
||||
onRemove,
|
||||
onShowInSummaryChange,
|
||||
onShowInDetailChange,
|
||||
|
|
@ -87,6 +88,7 @@ function SortableColumnRow({
|
|||
onLabelChange: (value: string) => void;
|
||||
onWidthChange: (value: number) => void;
|
||||
onFormatChange: (checked: boolean) => void;
|
||||
onSuffixChange?: (value: string) => void;
|
||||
onRemove: () => void;
|
||||
onShowInSummaryChange?: (checked: boolean) => void;
|
||||
onShowInDetailChange?: (checked: boolean) => void;
|
||||
|
|
@ -177,15 +179,24 @@ function SortableColumnRow({
|
|||
className="h-6 w-14 shrink-0 text-xs"
|
||||
/>
|
||||
{isNumeric && (
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={col.format?.thousandSeparator ?? false}
|
||||
onChange={(e) => onFormatChange(e.target.checked)}
|
||||
className="h-3 w-3"
|
||||
<>
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={col.format?.thousandSeparator ?? false}
|
||||
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 && (
|
||||
|
|
@ -818,6 +829,18 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
|||
};
|
||||
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) })}
|
||||
onShowInSummaryChange={(checked) => {
|
||||
const newColumns = [...selectedColumns];
|
||||
|
|
@ -2330,6 +2353,18 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
};
|
||||
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) })
|
||||
}
|
||||
|
|
@ -2988,6 +3023,18 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
};
|
||||
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) })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,6 +315,11 @@ export const LeftPanelConfigTab: React.FC<LeftPanelConfigTabProps> = ({
|
|||
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } };
|
||||
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) })}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -305,6 +305,11 @@ export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
|
|||
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } };
|
||||
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) })}
|
||||
onShowInSummaryChange={(checked) => {
|
||||
const newColumns = [...selectedColumns];
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { Check, ChevronsUpDown, GripVertical, Link2, X } from "lucide-react";
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
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;
|
||||
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean };
|
||||
|
|
@ -23,6 +23,7 @@ export function SortableColumnRow({
|
|||
onLabelChange: (value: string) => void;
|
||||
onWidthChange: (value: number) => void;
|
||||
onFormatChange: (checked: boolean) => void;
|
||||
onSuffixChange?: (value: string) => void;
|
||||
onRemove: () => void;
|
||||
onShowInSummaryChange?: (checked: boolean) => void;
|
||||
onShowInDetailChange?: (checked: boolean) => void;
|
||||
|
|
@ -61,15 +62,24 @@ export function SortableColumnRow({
|
|||
className="h-6 w-14 shrink-0 text-xs"
|
||||
/>
|
||||
{isNumeric && (
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={col.format?.thousandSeparator ?? false}
|
||||
onChange={(e) => onFormatChange(e.target.checked)}
|
||||
className="h-3 w-3"
|
||||
<>
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={col.format?.thousandSeparator ?? false}
|
||||
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 && (
|
||||
<label className="flex shrink-0 cursor-pointer items-center gap-0.5 text-[10px]" title="테이블 헤더에 표시">
|
||||
|
|
|
|||
Loading…
Reference in New Issue