diff --git a/frontend/components/report/designer/ReportDesignerCanvas.tsx b/frontend/components/report/designer/ReportDesignerCanvas.tsx index 3bfbfa38..3c148dbe 100644 --- a/frontend/components/report/designer/ReportDesignerCanvas.tsx +++ b/frontend/components/report/designer/ReportDesignerCanvas.tsx @@ -12,6 +12,7 @@ export function ReportDesignerCanvas() { const { components, addComponent, + updateComponent, canvasWidth, canvasHeight, selectComponent, @@ -77,7 +78,7 @@ export function ReportDesignerCanvas() { } }; - // 키보드 단축키 (Delete, Ctrl+C, Ctrl+V) + // 키보드 단축키 (Delete, Ctrl+C, Ctrl+V, 화살표 이동) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // 입력 필드에서는 단축키 무시 @@ -86,6 +87,51 @@ export function ReportDesignerCanvas() { return; } + // 화살표 키: 선택된 컴포넌트 이동 + if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { + e.preventDefault(); + + // 선택된 컴포넌트가 없으면 무시 + if (!selectedComponentId && selectedComponentIds.length === 0) { + return; + } + + // 이동 거리 (Shift 키를 누르면 10px, 아니면 1px) + const moveDistance = e.shiftKey ? 10 : 1; + + // 이동할 컴포넌트 ID 목록 + const idsToMove = + selectedComponentIds.length > 0 ? selectedComponentIds : ([selectedComponentId].filter(Boolean) as string[]); + + // 각 컴포넌트 이동 + idsToMove.forEach((id) => { + const component = components.find((c) => c.id === id); + if (!component) return; + + let newX = component.x; + let newY = component.y; + + switch (e.key) { + case "ArrowLeft": + newX = Math.max(0, component.x - moveDistance); + break; + case "ArrowRight": + newX = component.x + moveDistance; + break; + case "ArrowUp": + newY = Math.max(0, component.y - moveDistance); + break; + case "ArrowDown": + newY = component.y + moveDistance; + break; + } + + updateComponent(id, { x: newX, y: newY }); + }); + + return; + } + // Delete 키: 삭제 if (e.key === "Delete") { if (selectedComponentIds.length > 0) { @@ -128,7 +174,17 @@ export function ReportDesignerCanvas() { window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [selectedComponentId, selectedComponentIds, removeComponent, copyComponents, pasteComponents, undo, redo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + selectedComponentId, + selectedComponentIds, + components, + removeComponent, + copyComponents, + pasteComponents, + undo, + redo, + ]); return (