From f0513e20d8de18e895c462961cd8eace15837c0e Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 25 Nov 2025 17:19:39 +0900 Subject: [PATCH] =?UTF-8?q?3D=20=EC=97=90=EB=94=94=ED=84=B0=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=9E=85=EB=A0=A5=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/yard-3d/DigitalTwinEditor.tsx | 110 +++++++++++++++--- 1 file changed, 91 insertions(+), 19 deletions(-) diff --git a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx index f3d826b5..3a4b1901 100644 --- a/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx +++ b/frontend/components/admin/dashboard/widgets/yard-3d/DigitalTwinEditor.tsx @@ -39,6 +39,77 @@ import { DialogTitle, } from "@/components/ui/dialog"; +// 성능 최적화를 위한 디바운스/Blur 처리된 Input 컴포넌트 +const DebouncedInput = ({ + value, + onChange, + onCommit, + type = "text", + debounce = 0, + ...props +}: React.InputHTMLAttributes & { + 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) => { + setLocalValue(e.target.value); + if (onChange) onChange(e); + }; + + const handleBlur = (e: React.FocusEvent) => { + 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) => { + setIsEditing(true); + if (props.onFocus) props.onFocus(e); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.currentTarget.blur(); + } + if (props.onKeyDown) props.onKeyDown(e); + }; + + return ( + + ); +}; + // 백엔드 DB 객체 타입 (snake_case) interface DbObject { id: number; @@ -2070,10 +2141,10 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi - handleObjectUpdate({ name: e.target.value })} + onCommit={(val) => handleObjectUpdate({ name: val })} className="mt-1.5 h-9 text-sm" /> @@ -2086,15 +2157,15 @@ export default function DigitalTwinEditor({ layoutId, layoutName, onBack }: Digi - + 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 - + 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 - + 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 - + 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 - + 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 - handleObjectUpdate({ color: e.target.value })} + onCommit={(val) => handleObjectUpdate({ color: val })} className="mt-1.5 h-9" />