3D 에디터 속성 입력 성능 최적화

This commit is contained in:
dohyeons 2025-11-25 17:19:39 +09:00
parent 710ca122ea
commit f0513e20d8
1 changed files with 91 additions and 19 deletions

View File

@ -39,6 +39,77 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
// 성능 최적화를 위한 디바운스/Blur 처리된 Input 컴포넌트
const DebouncedInput = ({
value,
onChange,
onCommit,
type = "text",
debounce = 0,
...props
}: React.InputHTMLAttributes<HTMLInputElement> & {
onCommit?: (value: any) => void;
debounce?: number;
}) => {
const [localValue, setLocalValue] = useState(value);
const [isEditing, setIsEditing] = useState(false);
useEffect(() => {
if (!isEditing) {
setLocalValue(value);
}
}, [value, isEditing]);
// 색상 입력 등을 위한 디바운스 커밋
useEffect(() => {
if (debounce > 0 && isEditing && onCommit) {
const timer = setTimeout(() => {
onCommit(type === "number" ? parseFloat(localValue as string) : localValue);
}, debounce);
return () => clearTimeout(timer);
}
}, [localValue, debounce, isEditing, onCommit, type]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocalValue(e.target.value);
if (onChange) onChange(e);
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
setIsEditing(false);
if (onCommit && debounce === 0) {
// 값이 변경되었을 때만 커밋하도록 하면 좋겠지만,
// 부모 상태와 비교하기 어려우므로 항상 커밋 (handleObjectUpdate 내부에서 처리됨)
onCommit(type === "number" ? parseFloat(localValue as string) : localValue);
}
if (props.onBlur) props.onBlur(e);
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
setIsEditing(true);
if (props.onFocus) props.onFocus(e);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.currentTarget.blur();
}
if (props.onKeyDown) props.onKeyDown(e);
};
return (
<Input
{...props}
type={type}
value={localValue}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
/>
);
};
// 백엔드 DB 객체 타입 (snake_case)
interface DbObject {
id: number;
@ -2070,10 +2141,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<Label htmlFor="object-name" className="text-sm">
</Label>
<Input
<DebouncedInput
id="object-name"
value={selectedObject.name || ""}
onChange={(e) => handleObjectUpdate({ name: e.target.value })}
onCommit={(val) => handleObjectUpdate({ name: val })}
className="mt-1.5 h-9 text-sm"
/>
</div>
@ -2086,15 +2157,15 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<Label htmlFor="pos-x" className="text-muted-foreground text-xs">
X
</Label>
<Input
<DebouncedInput
id="pos-x"
type="number"
value={(selectedObject.position?.x || 0).toFixed(1)}
onChange={(e) =>
onCommit={(val) =>
handleObjectUpdate({
position: {
...selectedObject.position,
x: parseFloat(e.target.value),
x: val,
},
})
}
@ -2105,15 +2176,15 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<Label htmlFor="pos-z" className="text-muted-foreground text-xs">
Z
</Label>
<Input
<DebouncedInput
id="pos-z"
type="number"
value={(selectedObject.position?.z || 0).toFixed(1)}
onChange={(e) =>
onCommit={(val) =>
handleObjectUpdate({
position: {
...selectedObject.position,
z: parseFloat(e.target.value),
z: val,
},
})
}
@ -2131,17 +2202,17 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<Label htmlFor="size-x" className="text-muted-foreground text-xs">
W (5 )
</Label>
<Input
<DebouncedInput
id="size-x"
type="number"
step="5"
min="5"
value={selectedObject.size?.x || 5}
onChange={(e) =>
onCommit={(val) =>
handleObjectUpdate({
size: {
...selectedObject.size,
x: parseFloat(e.target.value),
x: val,
},
})
}
@ -2152,15 +2223,15 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<Label htmlFor="size-y" className="text-muted-foreground text-xs">
H
</Label>
<Input
<DebouncedInput
id="size-y"
type="number"
value={selectedObject.size?.y || 5}
onChange={(e) =>
onCommit={(val) =>
handleObjectUpdate({
size: {
...selectedObject.size,
y: parseFloat(e.target.value),
y: val,
},
})
}
@ -2171,17 +2242,17 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<Label htmlFor="size-z" className="text-muted-foreground text-xs">
D (5 )
</Label>
<Input
<DebouncedInput
id="size-z"
type="number"
step="5"
min="5"
value={selectedObject.size?.z || 5}
onChange={(e) =>
onCommit={(val) =>
handleObjectUpdate({
size: {
...selectedObject.size,
z: parseFloat(e.target.value),
z: val,
},
})
}
@ -2196,11 +2267,12 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi
<Label htmlFor="object-color" className="text-sm">
</Label>
<Input
<DebouncedInput
id="object-color"
type="color"
debounce={100}
value={selectedObject.color || "#3b82f6"}
onChange={(e) => handleObjectUpdate({ color: e.target.value })}
onCommit={(val) => handleObjectUpdate({ color: val })}
className="mt-1.5 h-9"
/>
</div>