diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index f770b4f7..b6a16839 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -11,6 +11,7 @@ interface CanvasComponentProps { export function CanvasComponent({ component }: CanvasComponentProps) { const { selectedComponentId, + selectedComponentIds, selectComponent, updateComponent, getQueryResult, @@ -25,6 +26,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) { const componentRef = useRef(null); const isSelected = selectedComponentId === component.id; + const isMultiSelected = selectedComponentIds.includes(component.id); // 드래그 시작 const handleMouseDown = (e: React.MouseEvent) => { @@ -33,7 +35,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) { } e.stopPropagation(); - selectComponent(component.id); + + // Ctrl/Cmd 키 감지 (다중 선택) + const isMultiSelect = e.ctrlKey || e.metaKey; + selectComponent(component.id, isMultiSelect); + setIsDragging(true); setDragStart({ x: e.clientX - component.x, @@ -271,7 +277,9 @@ export function CanvasComponent({ component }: CanvasComponentProps) { return (
({ @@ -72,17 +77,58 @@ export function ReportDesignerCanvas() { } }; - // Delete 키 삭제 처리 + // 키보드 단축키 (Delete, Ctrl+C, Ctrl+V) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Delete" && selectedComponentId) { - removeComponent(selectedComponentId); + // 입력 필드에서는 단축키 무시 + const target = e.target as HTMLElement; + if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") { + return; + } + + // Delete 키: 삭제 + if (e.key === "Delete") { + if (selectedComponentIds.length > 0) { + selectedComponentIds.forEach((id) => removeComponent(id)); + } else if (selectedComponentId) { + removeComponent(selectedComponentId); + } + } + + // Ctrl+C (또는 Cmd+C): 복사 + if ((e.ctrlKey || e.metaKey) && e.key === "c") { + e.preventDefault(); + copyComponents(); + } + + // Ctrl+V (또는 Cmd+V): 붙여넣기 + if ((e.ctrlKey || e.metaKey) && e.key === "v") { + e.preventDefault(); + pasteComponents(); + } + + // Ctrl+Shift+Z 또는 Ctrl+Y (또는 Cmd+Shift+Z / Cmd+Y): Redo (Undo보다 먼저 체크) + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "z") { + e.preventDefault(); + redo(); + return; + } + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "y") { + e.preventDefault(); + redo(); + return; + } + + // Ctrl+Z (또는 Cmd+Z): Undo + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") { + e.preventDefault(); + undo(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [selectedComponentId, removeComponent]); + }, [selectedComponentId, selectedComponentIds, removeComponent, copyComponents, pasteComponents, undo, redo]); return (
diff --git a/frontend/components/report/designer/ReportDesignerToolbar.tsx b/frontend/components/report/designer/ReportDesignerToolbar.tsx index 66b50a73..ebb7c7cb 100644 --- a/frontend/components/report/designer/ReportDesignerToolbar.tsx +++ b/frontend/components/report/designer/ReportDesignerToolbar.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; -import { Save, Eye, RotateCcw, ArrowLeft, Loader2, BookTemplate, Grid3x3 } from "lucide-react"; +import { Save, Eye, RotateCcw, ArrowLeft, Loader2, BookTemplate, Grid3x3, Undo2, Redo2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useReportDesigner } from "@/contexts/ReportDesignerContext"; import { useState } from "react"; @@ -26,6 +26,10 @@ export function ReportDesignerToolbar() { setSnapToGrid, showGrid, setShowGrid, + undo, + redo, + canUndo, + canRedo, } = useReportDesigner(); const [showPreview, setShowPreview] = useState(false); const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false); @@ -149,6 +153,26 @@ export function ReportDesignerToolbar() { {snapToGrid && showGrid ? "Grid ON" : "Grid OFF"} + +