Compare commits

..

No commits in common. "bd08b341f06399c8bbf3fb73ac480e6712f6f6e2" and "3df9a39ebea1461174935f74ab4c5352f74888ea" have entirely different histories.

11 changed files with 876 additions and 1104 deletions

View File

@ -370,46 +370,7 @@ 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;
@ -421,6 +382,7 @@ 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

@ -13,18 +13,7 @@ import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { import { Settings, ChevronDown, Loader2, Type, Hash, Lock, AlignLeft, SlidersHorizontal, Palette, ListOrdered } from "lucide-react";
Settings,
ChevronDown,
Loader2,
Type,
Hash,
Lock,
AlignLeft,
SlidersHorizontal,
Palette,
ListOrdered,
} from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen"; import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
@ -35,15 +24,9 @@ interface V2InputConfigPanelProps {
config: Record<string, any>; config: Record<string, any>;
onChange: (config: Record<string, any>) => void; onChange: (config: Record<string, any>) => void;
menuObjid?: number; menuObjid?: number;
allComponents?: any[];
} }
export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({ config, onChange, menuObjid }) => {
config,
onChange,
menuObjid,
allComponents = [],
}) => {
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]); const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
const [loadingRules, setLoadingRules] = useState(false); const [loadingRules, setLoadingRules] = useState(false);
const [parentMenus, setParentMenus] = useState<any[]>([]); const [parentMenus, setParentMenus] = useState<any[]>([]);
@ -68,7 +51,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
const userMenus = allMenus.filter((menu: any) => { const userMenus = allMenus.filter((menu: any) => {
const menuType = menu.menu_type || menu.menuType; const menuType = menu.menu_type || menu.menuType;
const level = menu.level || menu.lev || menu.LEVEL; const level = menu.level || menu.lev || menu.LEVEL;
return menuType === "1" && (level === 2 || level === 3 || level === "2" || level === "3"); return menuType === '1' && (level === 2 || level === 3 || level === '2' || level === '3');
}); });
setParentMenus(userMenus); setParentMenus(userMenus);
} }
@ -87,10 +70,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
const loadRules = async () => { const loadRules = async () => {
const isNumbering = inputType === "numbering" || config.autoGeneration?.type === "numbering_rule"; const isNumbering = inputType === "numbering" || config.autoGeneration?.type === "numbering_rule";
if (!isNumbering) return; if (!isNumbering) return;
if (!selectedMenuObjid) { if (!selectedMenuObjid) { setNumberingRules([]); return; }
setNumberingRules([]);
return;
}
setLoadingRules(true); setLoadingRules(true);
try { try {
const response = await getAvailableNumberingRules(selectedMenuObjid); const response = await getAvailableNumberingRules(selectedMenuObjid);
@ -112,10 +92,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{/* ─── 1단계: 입력 타입 선택 (카드 방식) ─── */} {/* ─── 1단계: 입력 타입 선택 (카드 방식) ─── */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Type className="text-muted-foreground h-4 w-4" /> <Type className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium"> </p> <p className="text-sm font-medium"> </p>
</div> </div>
<p className="text-muted-foreground text-[11px]"> </p> <p className="text-[11px] text-muted-foreground"> </p>
</div> </div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@ -152,23 +132,20 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
className={cn( className={cn(
"flex items-center gap-2 rounded-lg border p-2.5 text-left transition-all", "flex items-center gap-2 rounded-lg border p-2.5 text-left transition-all",
inputType === item.value inputType === item.value
? "border-primary bg-primary/5 ring-primary/20 ring-1" ? "border-primary bg-primary/5 ring-1 ring-primary/20"
: "border-border hover:border-primary/30 hover:bg-muted/30", : "border-border hover:border-primary/30 hover:bg-muted/30"
)} )}
> >
<item.icon <item.icon className={cn(
className={cn("h-4 w-4 shrink-0", inputType === item.value ? "text-primary" : "text-muted-foreground")} "h-4 w-4 shrink-0",
/> inputType === item.value ? "text-primary" : "text-muted-foreground"
)} />
<div className="min-w-0"> <div className="min-w-0">
<span <span className={cn(
className={cn( "text-xs font-medium block",
"block text-xs font-medium", inputType === item.value ? "text-primary" : "text-foreground"
inputType === item.value ? "text-primary" : "text-foreground", )}>{item.label}</span>
)} <span className="text-[10px] text-muted-foreground block truncate">{item.desc}</span>
>
{item.label}
</span>
<span className="text-muted-foreground block truncate text-[10px]">{item.desc}</span>
</div> </div>
</button> </button>
))} ))}
@ -176,34 +153,34 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{/* ─── 채번 타입 전용 설정 ─── */} {/* ─── 채번 타입 전용 설정 ─── */}
{inputType === "numbering" && ( {inputType === "numbering" && (
<div className="bg-muted/30 space-y-3 rounded-lg border p-4"> <div className="rounded-lg border bg-muted/30 p-4 space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ListOrdered className="text-primary h-4 w-4" /> <ListOrdered className="h-4 w-4 text-primary" />
<span className="text-sm font-medium"> </span> <span className="text-sm font-medium"> </span>
</div> </div>
<div> <div>
<p className="text-muted-foreground mb-1.5 text-xs"> </p> <p className="mb-1.5 text-xs text-muted-foreground"> </p>
{menuObjid && selectedMenuObjid === menuObjid ? ( {menuObjid && selectedMenuObjid === menuObjid ? (
<div className="bg-background rounded-md border p-2"> <div className="rounded-md border bg-background p-2">
<p className="text-muted-foreground text-xs"> </p> <p className="text-xs text-muted-foreground"> </p>
<div className="mt-1 flex items-center justify-between"> <div className="mt-1 flex items-center justify-between">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
{parentMenus.find((m: any) => m.objid === menuObjid)?.menu_name_kor || {parentMenus.find((m: any) => m.objid === menuObjid)?.menu_name_kor
parentMenus.find((m: any) => m.objid === menuObjid)?.translated_name || || parentMenus.find((m: any) => m.objid === menuObjid)?.translated_name
`메뉴 #${menuObjid}`} || `메뉴 #${menuObjid}`}
</p> </p>
<button <button
type="button" type="button"
onClick={() => setSelectedMenuObjid(undefined)} onClick={() => setSelectedMenuObjid(undefined)}
className="text-muted-foreground hover:text-foreground text-[10px]" className="text-[10px] text-muted-foreground hover:text-foreground"
> >
</button> </button>
</div> </div>
</div> </div>
) : loadingMenus ? ( ) : loadingMenus ? (
<div className="text-muted-foreground flex items-center gap-2 py-1 text-xs"> <div className="text-muted-foreground flex items-center gap-2 text-xs py-1">
<Loader2 className="h-3 w-3 animate-spin" /> <Loader2 className="h-3 w-3 animate-spin" />
... ...
</div> </div>
@ -239,9 +216,9 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{selectedMenuObjid && ( {selectedMenuObjid && (
<div> <div>
<p className="text-muted-foreground mb-1.5 text-xs"> </p> <p className="mb-1.5 text-xs text-muted-foreground"> </p>
{loadingRules ? ( {loadingRules ? (
<div className="text-muted-foreground flex items-center gap-2 py-1 text-xs"> <div className="text-muted-foreground flex items-center gap-2 text-xs py-1">
<Loader2 className="h-3 w-3 animate-spin" /> <Loader2 className="h-3 w-3 animate-spin" />
... ...
</div> </div>
@ -266,14 +243,13 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
<SelectContent> <SelectContent>
{numberingRules.map((rule) => ( {numberingRules.map((rule) => (
<SelectItem key={rule.ruleId} value={String(rule.ruleId)}> <SelectItem key={rule.ruleId} value={String(rule.ruleId)}>
{rule.ruleName} ({rule.separator || "-"} {rule.ruleName} ({rule.separator || "-"}{"{번호}"})
{"{번호}"})
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
) : ( ) : (
<p className="text-muted-foreground text-xs"> </p> <p className="text-xs text-muted-foreground"> </p>
)} )}
</div> </div>
)} )}
@ -281,7 +257,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<div> <div>
<p className="text-sm"></p> <p className="text-sm"></p>
<p className="text-muted-foreground text-[11px]"> </p> <p className="text-[11px] text-muted-foreground"> </p>
</div> </div>
<Switch <Switch
checked={config.readonly !== false} checked={config.readonly !== false}
@ -295,10 +271,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{inputType !== "numbering" && ( {inputType !== "numbering" && (
<> <>
{/* 기본 설정 영역 */} {/* 기본 설정 영역 */}
<div className="bg-muted/30 space-y-3 rounded-lg border p-4"> <div className="rounded-lg border bg-muted/30 p-4 space-y-3">
{/* 안내 텍스트 (placeholder) */} {/* 안내 텍스트 (placeholder) */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-muted-foreground text-xs"> </span> <span className="text-xs text-muted-foreground"> </span>
<Input <Input
value={config.placeholder || ""} value={config.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)} onChange={(e) => updateConfig("placeholder", e.target.value)}
@ -310,7 +286,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{/* 입력 형식 - 텍스트 타입 전용 */} {/* 입력 형식 - 텍스트 타입 전용 */}
{(inputType === "text" || !config.inputType) && ( {(inputType === "text" || !config.inputType) && (
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-muted-foreground text-xs"> </span> <span className="text-xs text-muted-foreground"> </span>
<Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}> <Select value={config.format || "none"} onValueChange={(value) => updateConfig("format", value)}>
<SelectTrigger className="h-7 w-[160px] text-xs"> <SelectTrigger className="h-7 w-[160px] text-xs">
<SelectValue placeholder="형식 선택" /> <SelectValue placeholder="형식 선택" />
@ -330,8 +306,8 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{/* 입력 마스크 */} {/* 입력 마스크 */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<div> <div>
<span className="text-muted-foreground text-xs"> </span> <span className="text-xs text-muted-foreground"> </span>
<p className="text-muted-foreground mt-0.5 text-[10px]"># = , A = , * = </p> <p className="text-[10px] text-muted-foreground mt-0.5"># = , A = , * = </p>
</div> </div>
<Input <Input
value={config.mask || ""} value={config.mask || ""}
@ -344,10 +320,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{/* 숫자/슬라이더: 범위 설정 */} {/* 숫자/슬라이더: 범위 설정 */}
{(inputType === "number" || inputType === "slider") && ( {(inputType === "number" || inputType === "slider") && (
<div className="space-y-2 pt-1"> <div className="space-y-2 pt-1">
<p className="text-muted-foreground text-xs"> </p> <p className="text-xs text-muted-foreground"> </p>
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex-1"> <div className="flex-1">
<Label className="text-muted-foreground text-[10px]"></Label> <Label className="text-[10px] text-muted-foreground"></Label>
<Input <Input
type="number" type="number"
value={config.min ?? ""} value={config.min ?? ""}
@ -357,7 +333,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
/> />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Label className="text-muted-foreground text-[10px]"></Label> <Label className="text-[10px] text-muted-foreground"></Label>
<Input <Input
type="number" type="number"
value={config.max ?? ""} value={config.max ?? ""}
@ -367,7 +343,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
/> />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Label className="text-muted-foreground text-[10px]"></Label> <Label className="text-[10px] text-muted-foreground"></Label>
<Input <Input
type="number" type="number"
value={config.step ?? ""} value={config.step ?? ""}
@ -383,7 +359,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{/* 여러 줄 텍스트: 줄 수 */} {/* 여러 줄 텍스트: 줄 수 */}
{inputType === "textarea" && ( {inputType === "textarea" && (
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-muted-foreground text-xs"> </span> <span className="text-xs text-muted-foreground"> </span>
<Input <Input
type="number" type="number"
value={config.rows || 3} value={config.rows || 3}
@ -401,27 +377,27 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button <button
type="button" type="button"
className="bg-muted/30 hover:bg-muted/50 flex w-full items-center justify-between rounded-lg border px-4 py-2.5 text-left transition-colors" className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="text-muted-foreground h-4 w-4" /> <Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span> <span className="text-sm font-medium"> </span>
</div> </div>
<ChevronDown <ChevronDown
className={cn( className={cn(
"text-muted-foreground h-4 w-4 transition-transform duration-200", "h-4 w-4 text-muted-foreground transition-transform duration-200",
advancedOpen && "rotate-180", advancedOpen && "rotate-180"
)} )}
/> />
</button> </button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="space-y-3 rounded-b-lg border border-t-0 p-4"> <div className="rounded-b-lg border border-t-0 p-4 space-y-3">
{/* 자동 생성 토글 */} {/* 자동 생성 토글 */}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<div> <div>
<p className="text-sm"> </p> <p className="text-sm"> </p>
<p className="text-muted-foreground text-[11px]"> </p> <p className="text-[11px] text-muted-foreground"> </p>
</div> </div>
<Switch <Switch
checked={config.autoGeneration?.enabled || false} checked={config.autoGeneration?.enabled || false}
@ -436,10 +412,10 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
</div> </div>
{config.autoGeneration?.enabled && ( {config.autoGeneration?.enabled && (
<div className="border-primary/20 ml-1 space-y-3 border-l-2 pl-3"> <div className="space-y-3 ml-1 border-l-2 border-primary/20 pl-3">
{/* 자동 생성 타입 */} {/* 자동 생성 타입 */}
<div> <div>
<p className="text-muted-foreground mb-1.5 text-xs"> </p> <p className="mb-1.5 text-xs text-muted-foreground"> </p>
<Select <Select
value={config.autoGeneration?.type || "none"} value={config.autoGeneration?.type || "none"}
onValueChange={(value: AutoGenerationType) => { onValueChange={(value: AutoGenerationType) => {
@ -469,7 +445,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
</div> </div>
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && ( {config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
<p className="text-muted-foreground text-[11px]"> <p className="text-[11px] text-muted-foreground">
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)} {AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
</p> </p>
)} )}
@ -478,7 +454,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{config.autoGeneration?.type === "numbering_rule" && ( {config.autoGeneration?.type === "numbering_rule" && (
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<p className="text-muted-foreground mb-1.5 text-xs"> <p className="mb-1.5 text-xs text-muted-foreground">
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</p> </p>
<Select <Select
@ -514,11 +490,11 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
{selectedMenuObjid ? ( {selectedMenuObjid ? (
<div> <div>
<p className="text-muted-foreground mb-1.5 text-xs"> <p className="mb-1.5 text-xs text-muted-foreground">
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</p> </p>
{loadingRules ? ( {loadingRules ? (
<div className="text-muted-foreground flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" /> <Loader2 className="h-3 w-3 animate-spin" />
... ...
</div> </div>
@ -568,7 +544,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
<div className="space-y-3"> <div className="space-y-3">
{["random_string", "random_number"].includes(config.autoGeneration.type) && ( {["random_string", "random_number"].includes(config.autoGeneration.type) && (
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-muted-foreground text-xs"></span> <span className="text-xs text-muted-foreground"></span>
<Input <Input
type="number" type="number"
min="1" min="1"
@ -589,7 +565,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
)} )}
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-muted-foreground text-xs"></span> <span className="text-xs text-muted-foreground"></span>
<Input <Input
value={config.autoGeneration?.options?.prefix || ""} value={config.autoGeneration?.options?.prefix || ""}
onChange={(e) => { onChange={(e) => {
@ -607,7 +583,7 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
</div> </div>
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-1">
<span className="text-muted-foreground text-xs"></span> <span className="text-xs text-muted-foreground"></span>
<Input <Input
value={config.autoGeneration?.options?.suffix || ""} value={config.autoGeneration?.options?.suffix || ""}
onChange={(e) => { onChange={(e) => {
@ -624,8 +600,8 @@ export const V2InputConfigPanel: React.FC<V2InputConfigPanelProps> = ({
</div> </div>
<div> <div>
<span className="text-muted-foreground text-xs"></span> <span className="text-xs text-muted-foreground"></span>
<div className="bg-muted mt-1 rounded-md border p-2 font-mono text-xs"> <div className="mt-1 rounded-md border bg-muted p-2 text-xs font-mono">
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)} {AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
</div> </div>
</div> </div>
@ -668,7 +644,10 @@ function DataBindingSection({
const tableListComponents = React.useMemo(() => { const tableListComponents = React.useMemo(() => {
return allComponents.filter((comp) => { return allComponents.filter((comp) => {
const type = const type =
comp.componentType || comp.widgetType || comp.componentConfig?.type || (comp.url && comp.url.split("/").pop()); comp.componentType ||
comp.widgetType ||
comp.componentConfig?.type ||
(comp.url && comp.url.split("/").pop());
return type === "v2-table-list"; return type === "v2-table-list";
}); });
}, [allComponents]); }, [allComponents]);
@ -681,7 +660,11 @@ function DataBindingSection({
const selectedTableName = React.useMemo(() => { const selectedTableName = React.useMemo(() => {
if (!selectedTableComponent) return null; if (!selectedTableComponent) return null;
return selectedTableComponent.componentConfig?.selectedTable || selectedTableComponent.selectedTable || null; return (
selectedTableComponent.componentConfig?.selectedTable ||
selectedTableComponent.selectedTable ||
null
);
}, [selectedTableComponent]); }, [selectedTableComponent]);
// 선택된 테이블의 컬럼 목록 로드 // 선택된 테이블의 컬럼 목록 로드
@ -742,7 +725,9 @@ function DataBindingSection({
{config.dataBinding && ( {config.dataBinding && (
<div className="space-y-2 rounded border p-2"> <div className="space-y-2 rounded border p-2">
<p className="text-muted-foreground text-[10px]"> </p> <p className="text-[10px] text-muted-foreground">
</p>
{/* 소스 테이블 컴포넌트 선택 */} {/* 소스 테이블 컴포넌트 선택 */}
<div className="space-y-1"> <div className="space-y-1">
@ -765,7 +750,8 @@ function DataBindingSection({
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{tableListComponents.map((comp) => { {tableListComponents.map((comp) => {
const tblName = comp.componentConfig?.selectedTable || comp.selectedTable || ""; const tblName =
comp.componentConfig?.selectedTable || comp.selectedTable || "";
const label = comp.componentConfig?.label || comp.label || comp.id; const label = comp.componentConfig?.label || comp.label || comp.id;
return ( return (
<SelectItem key={comp.id} value={comp.id}> <SelectItem key={comp.id} value={comp.id}>
@ -783,7 +769,7 @@ function DataBindingSection({
<div className="space-y-1"> <div className="space-y-1">
<Label className="text-xs font-medium"> </Label> <Label className="text-xs font-medium"> </Label>
{loadingColumns ? ( {loadingColumns ? (
<p className="text-muted-foreground text-[10px]"> ...</p> <p className="text-[10px] text-muted-foreground"> ...</p>
) : tableColumns.length === 0 ? ( ) : tableColumns.length === 0 ? (
<> <>
<Input <Input
@ -797,7 +783,7 @@ function DataBindingSection({
placeholder="컬럼명 직접 입력" placeholder="컬럼명 직접 입력"
className="h-7 text-xs" className="h-7 text-xs"
/> />
<p className="text-muted-foreground text-[10px]"> </p> <p className="text-[10px] text-muted-foreground"> </p>
</> </>
) : ( ) : (
<Select <Select

File diff suppressed because it is too large Load Diff

View File

@ -157,13 +157,10 @@ function SortableColumnRow({
/> />
<Input <Input
value={col.width || ""} value={col.width || ""}
onChange={(e) => onWidthChange(parseInt(e.target.value) || 20)} onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
placeholder="20" placeholder="너비"
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,20 +607,18 @@ export const ColumnConfigModal: React.FC<ColumnConfigModalProps> = ({
</div> </div>
<div> <div>
<Label className="text-xs"> (%)</Label> <Label className="text-xs"> (px)</Label>
<Input <Input
type="number" type="number"
value={editingColumn.width && editingColumn.width <= 100 ? editingColumn.width : ""} value={editingColumn.width || ""}
onChange={(e) => onChange={(e) =>
setEditingColumn({ setEditingColumn({
...editingColumn, ...editingColumn,
width: e.target.value ? Math.min(100, Math.max(5, parseInt(e.target.value) || 20)) : undefined, width: e.target.value ? parseInt(e.target.value) : 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 <= 100 ? `${col.width}%` : "auto" }}> <TableHead key={idx} style={{ width: col.width ? `${col.width}px` : "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 <= 100 ? `${col.width}%` : "auto" }}> <TableHead key={idx} style={{ width: col.width ? `${col.width}px` : "auto" }}>
{col.label || col.name} {col.label || col.name}
</TableHead> </TableHead>
))} ))}

View File

@ -3543,12 +3543,6 @@ 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) ||
@ -3562,7 +3556,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="divide-y divide-border table-fixed" style={{ width: leftTotalColWidth > 100 ? `${leftTotalColWidth}%` : '100%' }}> <table className="min-w-full divide-y divide-border">
<thead className="bg-muted"> <thead className="bg-muted">
<tr> <tr>
{columnsToShow.map((col, idx) => ( {columnsToShow.map((col, idx) => (
@ -3570,7 +3564,8 @@ 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 <= 100 ? `${col.width}%` : "auto", width: col.width ? `${col.width}px` : "auto",
minWidth: "80px",
textAlign: col.align || "left", textAlign: col.align || "left",
}} }}
> >
@ -3659,7 +3654,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
); );
return ( return (
<div className="overflow-auto"> <div className="overflow-auto">
<table className="divide-y divide-border table-fixed" style={{ width: leftTotalColWidth > 100 ? `${leftTotalColWidth}%` : '100%' }}> <table className="min-w-full divide-y divide-border">
<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) => (
@ -3667,7 +3662,8 @@ 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 <= 100 ? `${col.width}%` : "auto", width: col.width ? `${col.width}px` : "auto",
minWidth: "80px",
textAlign: col.align || "left", textAlign: col.align || "left",
}} }}
> >
@ -4663,16 +4659,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
})); }));
} }
// 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤) const tableMinWidth = columnsToShow.reduce((sum, col) => sum + (col.width || 100), 0) + 80;
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 className="table-fixed" style={{ width: rightTotalColWidth > 100 ? `${rightTotalColWidth}%` : '100%' }}> <table style={{ minWidth: `${tableMinWidth}px` }}>
<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) => (
@ -4680,7 +4671,8 @@ 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 <= 100 ? `${col.width}%` : "auto", width: col.width ? `${col.width}px` : "auto",
minWidth: "80px",
textAlign: col.align || "left", textAlign: col.align || "left",
}} }}
> >
@ -4691,7 +4683,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" style={{ width: '80px' }}> <th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">
</th> </th>
)} )}
@ -4770,7 +4762,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; width?: number }[] = []; let columnsToDisplay: { name: string; label: string; format?: string; bold?: boolean }[] = [];
if (rightColumns && rightColumns.length > 0) { if (rightColumns && rightColumns.length > 0) {
// showInSummary가 false가 아닌 것만 메인 테이블에 표시 // showInSummary가 false가 아닌 것만 메인 테이블에 표시
@ -4781,7 +4773,6 @@ 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])
@ -4793,33 +4784,24 @@ 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="table-fixed text-sm" style={{ width: displayTotalColWidth > 100 ? `${displayTotalColWidth}%` : '100%' }}> <table className="text-sm" style={{ minWidth: `${tableMinW2}px` }}>
<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 <th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
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" style={{ width: '80px' }}></th> <th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold"></th>
)} )}
</tr> </tr>
</thead> </thead>

View File

@ -174,13 +174,10 @@ function SortableColumnRow({
/> />
<Input <Input
value={col.width || ""} value={col.width || ""}
onChange={(e) => onWidthChange(parseInt(e.target.value) || 20)} onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
placeholder="20" placeholder="너비"
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="천 단위 구분자 (,)">
@ -891,7 +888,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
updateTab({ updateTab({
columns: [ columns: [
...selectedColumns, ...selectedColumns,
{ name: column.columnName, label: column.columnLabel || column.columnName, width: 20 }, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 },
], ],
}); });
}} }}
@ -1061,7 +1058,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
{ {
name: matchingJoinColumn.joinAlias, name: matchingJoinColumn.joinAlias,
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
width: 20, width: 100,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: tab.tableName!, sourceTable: tab.tableName!,
@ -2399,7 +2396,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
{ {
name: column.columnName, name: column.columnName,
label: column.columnLabel || column.columnName, label: column.columnLabel || column.columnName,
width: 20, width: 100,
}, },
], ],
}); });
@ -2469,7 +2466,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
label: label:
matchingJoinColumn.suggestedLabel || matchingJoinColumn.suggestedLabel ||
matchingJoinColumn.columnLabel, matchingJoinColumn.columnLabel,
width: 20, width: 100,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: leftTable!, sourceTable: leftTable!,
@ -3077,7 +3074,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
{ {
name: column.columnName, name: column.columnName,
label: column.columnLabel || column.columnName, label: column.columnLabel || column.columnName,
width: 20, width: 100,
}, },
], ],
}); });
@ -3144,7 +3141,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
name: matchingJoinColumn.joinAlias, name: matchingJoinColumn.joinAlias,
label: label:
matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
width: 20, width: 100,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: rightTable!, sourceTable: rightTable!,

View File

@ -321,10 +321,8 @@ 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>
@ -343,7 +341,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: 20 }] }); updateLeftPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] });
}} }}
> >
<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" />
@ -396,7 +394,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: 20, width: 100,
isEntityJoin: true, isEntityJoin: true,
joinInfo: { joinInfo: {
sourceTable: leftTable!, sourceTable: leftTable!,

View File

@ -321,7 +321,6 @@ 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="%"
/> />
); );
})} })}
@ -342,7 +341,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: 20 }] }); updateRightPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] });
}} }}
> >
<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" />
@ -395,7 +394,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: 20, width: 100,
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, widthUnit, 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 };
@ -27,7 +27,6 @@ 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 };
@ -58,13 +57,10 @@ export function SortableColumnRow({
/> />
<Input <Input
value={col.width || ""} value={col.width || ""}
onChange={(e) => onWidthChange(parseInt(e.target.value) || (widthUnit === "%" ? 20 : 100))} onChange={(e) => onWidthChange(parseInt(e.target.value) || 100)}
placeholder={widthUnit === "%" ? "20" : "너비"} placeholder="너비"
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="천 단위 구분자 (,)">