3D 에디터 속성 입력 성능 최적화
This commit is contained in:
parent
710ca122ea
commit
f0513e20d8
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue