feat: enhance column width configuration and rendering

- Updated the column width handling in various components to support percentage-based widths, improving layout flexibility.
- Adjusted input fields to enforce minimum and maximum width constraints, ensuring better user experience and preventing layout issues.
- Enhanced the SortableColumnRow and related components to dynamically display width units, allowing for clearer configuration options.

Made-with: Cursor
This commit is contained in:
kmh 2026-03-13 11:29:32 +09:00
parent f28cb5c2f6
commit 4fe023a813
9 changed files with 111 additions and 40 deletions

View File

@ -370,7 +370,46 @@ export function ResponsiveGridRenderer({
const { normalComps } = processedRow; const { normalComps } = processedRow;
const allButtons = normalComps.every((c) => isButtonComponent(c)); const allButtons = normalComps.every((c) => isButtonComponent(c));
const gap = isMobile ? 8 : allButtons ? 8 : getRowGap(normalComps, canvasWidth);
// 데스크톱에서 버튼만 있는 행: 디자이너의 x, width를 비율로 적용
if (allButtons && normalComps.length > 0 && !isMobile) {
const rowHeight = Math.max(...normalComps.map(c => c.size?.height || 40));
return (
<div
key={`row-${rowIndex}`}
className="relative w-full flex-shrink-0"
style={{
height: `${rowHeight}px`,
marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined,
}}
>
{normalComps.map((component) => {
const typeId = getComponentTypeId(component);
const leftPct = (component.position.x / canvasWidth) * 100;
const widthPct = ((component.size?.width || 90) / canvasWidth) * 100;
return (
<div
key={component.id}
data-component-id={component.id}
data-component-type={typeId}
style={{
position: "absolute",
left: `${leftPct}%`,
width: `${widthPct}%`,
height: `${component.size?.height || 40}px`,
}}
>
{renderComponent(component)}
</div>
);
})}
</div>
);
}
const gap = isMobile ? 8 : getRowGap(normalComps, canvasWidth);
const hasFlexHeightComp = normalComps.some((c) => { const hasFlexHeightComp = normalComps.some((c) => {
const h = c.size?.height || 0; const h = c.size?.height || 0;
@ -382,7 +421,6 @@ export function ResponsiveGridRenderer({
key={`row-${rowIndex}`} key={`row-${rowIndex}`}
className={cn( className={cn(
"flex w-full flex-wrap overflow-hidden", "flex w-full flex-wrap overflow-hidden",
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`, marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }} style={{ gap: `${gap}px`, marginTop: rowMarginTop > 0 ? `${rowMarginTop}px` : undefined }}

View File

@ -157,10 +157,13 @@ function SortableColumnRow({
/> />
<Input <Input
value={col.width || ""} value={col.width || ""}
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)} onChange={(e) => onWidthChange(parseInt(e.target.value) || 20)}
placeholder="너비" placeholder="20"
className="h-6 w-14 shrink-0 text-xs" className="h-6 w-14 shrink-0 text-xs"
min={5}
max={100}
/> />
<span className="text-muted-foreground shrink-0 text-[10px]">%</span>
{isNumeric && ( {isNumeric && (
<label <label
className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]"

View File

@ -607,18 +607,20 @@ export const ColumnConfigModal: React.FC<ColumnConfigModalProps> = ({
</div> </div>
<div> <div>
<Label className="text-xs"> (px)</Label> <Label className="text-xs"> (%)</Label>
<Input <Input
type="number" type="number"
value={editingColumn.width || ""} value={editingColumn.width && editingColumn.width <= 100 ? editingColumn.width : ""}
onChange={(e) => onChange={(e) =>
setEditingColumn({ setEditingColumn({
...editingColumn, ...editingColumn,
width: e.target.value ? parseInt(e.target.value) : undefined, width: e.target.value ? Math.min(100, Math.max(5, parseInt(e.target.value) || 20)) : undefined,
}) })
} }
placeholder="자동" placeholder="자동"
className="mt-1 h-9" className="mt-1 h-9"
min={5}
max={100}
/> />
</div> </div>
</div> </div>

View File

@ -1751,7 +1751,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{displayColumns.map((col, idx) => ( {displayColumns.map((col, idx) => (
<TableHead key={idx} style={{ width: col.width ? `${col.width}px` : "auto" }}> <TableHead key={idx} style={{ width: col.width && col.width <= 100 ? `${col.width}%` : "auto" }}>
{col.label || col.name} {col.label || col.name}
</TableHead> </TableHead>
))} ))}
@ -1952,7 +1952,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
</TableHead> </TableHead>
)} )}
{displayColumns.map((col, idx) => ( {displayColumns.map((col, idx) => (
<TableHead key={idx} style={{ width: col.width ? `${col.width}px` : "auto" }}> <TableHead key={idx} style={{ width: col.width && col.width <= 100 ? `${col.width}%` : "auto" }}>
{col.label || col.name} {col.label || col.name}
</TableHead> </TableHead>
))} ))}

View File

@ -3543,6 +3543,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
format: undefined, // 🆕 기본값 format: undefined, // 🆕 기본값
})); }));
// 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤)
const leftTotalColWidth = columnsToShow.reduce((sum, col) => {
const w = col.width && col.width <= 100 ? col.width : 0;
return sum + w;
}, 0);
// 🔧 그룹화된 데이터 렌더링 // 🔧 그룹화된 데이터 렌더링
const hasGroupedLeftActions = !isDesignMode && ( const hasGroupedLeftActions = !isDesignMode && (
(componentConfig.leftPanel?.showEdit !== false) || (componentConfig.leftPanel?.showEdit !== false) ||
@ -3556,7 +3562,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<div className="bg-muted px-3 py-2 text-sm font-semibold"> <div className="bg-muted px-3 py-2 text-sm font-semibold">
{group.groupKey} ({group.count}) {group.groupKey} ({group.count})
</div> </div>
<table className="min-w-full divide-y divide-border"> <table className="divide-y divide-border table-fixed" style={{ width: leftTotalColWidth > 100 ? `${leftTotalColWidth}%` : '100%' }}>
<thead className="bg-muted"> <thead className="bg-muted">
<tr> <tr>
{columnsToShow.map((col, idx) => ( {columnsToShow.map((col, idx) => (
@ -3564,8 +3570,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
key={idx} key={idx}
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
style={{ style={{
width: col.width ? `${col.width}px` : "auto", width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
minWidth: "80px",
textAlign: col.align || "left", textAlign: col.align || "left",
}} }}
> >
@ -3654,7 +3659,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
); );
return ( return (
<div className="overflow-auto"> <div className="overflow-auto">
<table className="min-w-full divide-y divide-border"> <table className="divide-y divide-border table-fixed" style={{ width: leftTotalColWidth > 100 ? `${leftTotalColWidth}%` : '100%' }}>
<thead className="sticky top-0 z-10 bg-muted"> <thead className="sticky top-0 z-10 bg-muted">
<tr> <tr>
{columnsToShow.map((col, idx) => ( {columnsToShow.map((col, idx) => (
@ -3662,8 +3667,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
key={idx} key={idx}
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap" className="px-3 py-2 text-left text-xs font-medium tracking-wider text-muted-foreground uppercase whitespace-nowrap"
style={{ style={{
width: col.width ? `${col.width}px` : "auto", width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
minWidth: "80px",
textAlign: col.align || "left", textAlign: col.align || "left",
}} }}
> >
@ -4659,11 +4663,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
})); }));
} }
const tableMinWidth = columnsToShow.reduce((sum, col) => sum + (col.width || 100), 0) + 80; // 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤)
const rightTotalColWidth = columnsToShow.reduce((sum, col) => {
const w = col.width && col.width <= 100 ? col.width : 0;
return sum + w;
}, 0);
return ( return (
<div className="flex h-full w-full flex-col"> <div className="flex h-full w-full flex-col">
<div className="min-h-0 flex-1 overflow-auto"> <div className="min-h-0 flex-1 overflow-auto">
<table style={{ minWidth: `${tableMinWidth}px` }}> <table className="table-fixed" style={{ width: rightTotalColWidth > 100 ? `${rightTotalColWidth}%` : '100%' }}>
<thead className="sticky top-0 z-10"> <thead className="sticky top-0 z-10">
<tr className="border-b-2 border-border/60"> <tr className="border-b-2 border-border/60">
{columnsToShow.map((col, idx) => ( {columnsToShow.map((col, idx) => (
@ -4671,8 +4680,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
key={idx} key={idx}
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap" className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
style={{ style={{
width: col.width ? `${col.width}px` : "auto", width: col.width && col.width <= 100 ? `${col.width}%` : "auto",
minWidth: "80px",
textAlign: col.align || "left", textAlign: col.align || "left",
}} }}
> >
@ -4683,7 +4691,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{!isDesignMode && {!isDesignMode &&
((componentConfig.rightPanel?.editButton?.enabled ?? true) || ((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && ( (componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold"> <th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold" style={{ width: '80px' }}>
</th> </th>
)} )}
@ -4762,7 +4770,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
{ {
// 표시 컬럼 결정 // 표시 컬럼 결정
const rightColumns = componentConfig.rightPanel?.columns; const rightColumns = componentConfig.rightPanel?.columns;
let columnsToDisplay: { name: string; label: string; format?: string; bold?: boolean }[] = []; let columnsToDisplay: { name: string; label: string; format?: string; bold?: boolean; width?: number }[] = [];
if (rightColumns && rightColumns.length > 0) { if (rightColumns && rightColumns.length > 0) {
// showInSummary가 false가 아닌 것만 메인 테이블에 표시 // showInSummary가 false가 아닌 것만 메인 테이블에 표시
@ -4773,6 +4781,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
label: rightColumnLabels[col.name] || col.label || col.name, label: rightColumnLabels[col.name] || col.label || col.name,
format: col.format, format: col.format,
bold: col.bold, bold: col.bold,
width: col.width,
})); }));
} else if (filteredData.length > 0) { } else if (filteredData.length > 0) {
columnsToDisplay = Object.keys(filteredData[0]) columnsToDisplay = Object.keys(filteredData[0])
@ -4784,24 +4793,33 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
})); }));
} }
// 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤)
const displayTotalColWidth = columnsToDisplay.reduce((sum, col) => {
const w = col.width && col.width <= 100 ? col.width : 0;
return sum + w;
}, 0);
const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true); const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true);
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true); const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
const hasActions = hasEditButton || hasDeleteButton; const hasActions = hasEditButton || hasDeleteButton;
const tableMinW2 = columnsToDisplay.reduce((sum, col) => sum + (col.width || 100), 0) + 80;
return filteredData.length > 0 ? ( return filteredData.length > 0 ? (
<div className="flex h-full w-full flex-col"> <div className="flex h-full w-full flex-col">
<div className="min-h-0 flex-1 overflow-auto"> <div className="min-h-0 flex-1 overflow-auto">
<table className="text-sm" style={{ minWidth: `${tableMinW2}px` }}> <table className="table-fixed text-sm" style={{ width: displayTotalColWidth > 100 ? `${displayTotalColWidth}%` : '100%' }}>
<thead className="sticky top-0 z-10 bg-background"> <thead className="sticky top-0 z-10 bg-background">
<tr className="border-b-2 border-border/60"> <tr className="border-b-2 border-border/60">
{columnsToDisplay.map((col) => ( {columnsToDisplay.map((col) => (
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold"> <th
key={col.name}
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
style={{ width: col.width && col.width <= 100 ? `${col.width}%` : "auto" }}
>
{col.label} {col.label}
</th> </th>
))} ))}
{hasActions && ( {hasActions && (
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold"></th> <th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold" style={{ width: '80px' }}></th>
)} )}
</tr> </tr>
</thead> </thead>

View File

@ -174,10 +174,13 @@ function SortableColumnRow({
/> />
<Input <Input
value={col.width || ""} value={col.width || ""}
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)} onChange={(e) => onWidthChange(parseInt(e.target.value) || 20)}
placeholder="너비" placeholder="20"
className="h-6 w-14 shrink-0 text-xs" className="h-6 w-14 shrink-0 text-xs"
min={5}
max={100}
/> />
<span className="text-muted-foreground shrink-0 text-[10px]">%</span>
{isNumeric && ( {isNumeric && (
<> <>
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)"> <label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">
@ -888,7 +891,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
updateTab({ updateTab({
columns: [ columns: [
...selectedColumns, ...selectedColumns,
{ name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }, { name: column.columnName, label: column.columnLabel || column.columnName, width: 20 },
], ],
}); });
}} }}
@ -1058,7 +1061,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
{ {
name: matchingJoinColumn.joinAlias, name: matchingJoinColumn.joinAlias,
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
width: 100, width: 20,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: tab.tableName!, sourceTable: tab.tableName!,
@ -2396,7 +2399,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
{ {
name: column.columnName, name: column.columnName,
label: column.columnLabel || column.columnName, label: column.columnLabel || column.columnName,
width: 100, width: 20,
}, },
], ],
}); });
@ -2466,7 +2469,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
label: label:
matchingJoinColumn.suggestedLabel || matchingJoinColumn.suggestedLabel ||
matchingJoinColumn.columnLabel, matchingJoinColumn.columnLabel,
width: 100, width: 20,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: leftTable!, sourceTable: leftTable!,
@ -3074,7 +3077,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
{ {
name: column.columnName, name: column.columnName,
label: column.columnLabel || column.columnName, label: column.columnLabel || column.columnName,
width: 100, width: 20,
}, },
], ],
}); });
@ -3141,7 +3144,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
name: matchingJoinColumn.joinAlias, name: matchingJoinColumn.joinAlias,
label: label:
matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
width: 100, width: 20,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: rightTable!, sourceTable: rightTable!,

View File

@ -321,8 +321,10 @@ export const LeftPanelConfigTab: React.FC<LeftPanelConfigTabProps> = ({
updateLeftPanel({ columns: newColumns }); updateLeftPanel({ columns: newColumns });
}} }}
onRemove={() => updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })} onRemove={() => updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })}
widthUnit="%"
/> />
); );
})} })}
</div> </div>
</SortableContext> </SortableContext>
@ -341,7 +343,7 @@ export const LeftPanelConfigTab: React.FC<LeftPanelConfigTabProps> = ({
key={column.columnName} key={column.columnName}
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5" className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5"
onClick={() => { onClick={() => {
updateLeftPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] }); updateLeftPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 20 }] });
}} }}
> >
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" /> <Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
@ -394,7 +396,7 @@ export const LeftPanelConfigTab: React.FC<LeftPanelConfigTabProps> = ({
columns: [...selectedColumns, { columns: [...selectedColumns, {
name: matchingJoinColumn.joinAlias, name: matchingJoinColumn.joinAlias,
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
width: 100, width: 20,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: leftTable!, sourceTable: leftTable!,

View File

@ -321,6 +321,7 @@ export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
newColumns[index] = { ...newColumns[index], showInDetail: checked }; newColumns[index] = { ...newColumns[index], showInDetail: checked };
updateRightPanel({ columns: newColumns }); updateRightPanel({ columns: newColumns });
}} }}
widthUnit="%"
/> />
); );
})} })}
@ -341,7 +342,7 @@ export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
key={column.columnName} key={column.columnName}
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5" className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5"
onClick={() => { onClick={() => {
updateRightPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] }); updateRightPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 20 }] });
}} }}
> >
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" /> <Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
@ -394,7 +395,7 @@ export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
columns: [...selectedColumns, { columns: [...selectedColumns, {
name: matchingJoinColumn.joinAlias, name: matchingJoinColumn.joinAlias,
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
width: 100, width: 20,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: rightTable!, sourceTable: rightTable!,

View File

@ -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, onSuffixChange, onRemove, onShowInSummaryChange, onShowInDetailChange, id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onSuffixChange, onRemove, onShowInSummaryChange, onShowInDetailChange, widthUnit,
}: { }: {
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 };
@ -27,6 +27,7 @@ export function SortableColumnRow({
onRemove: () => void; onRemove: () => void;
onShowInSummaryChange?: (checked: boolean) => void; onShowInSummaryChange?: (checked: boolean) => void;
onShowInDetailChange?: (checked: boolean) => void; onShowInDetailChange?: (checked: boolean) => void;
widthUnit?: string;
}) { }) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
const style = { transform: CSS.Transform.toString(transform), transition }; const style = { transform: CSS.Transform.toString(transform), transition };
@ -57,10 +58,13 @@ export function SortableColumnRow({
/> />
<Input <Input
value={col.width || ""} value={col.width || ""}
onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)} onChange={(e) => onWidthChange(parseInt(e.target.value) || (widthUnit === "%" ? 20 : 100))}
placeholder="너비" placeholder={widthUnit === "%" ? "20" : "너비"}
className="h-6 w-14 shrink-0 text-xs" className="h-6 w-14 shrink-0 text-xs"
min={widthUnit === "%" ? 5 : undefined}
max={widthUnit === "%" ? 100 : undefined}
/> />
<span className="text-muted-foreground shrink-0 text-[10px]">{widthUnit || "px"}</span>
{isNumeric && ( {isNumeric && (
<> <>
<label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)"> <label className="flex shrink-0 cursor-pointer items-center gap-1 text-[10px]" title="천 단위 구분자 (,)">