컴포넌트 다중 선택 및 복붙, Re/undo 구현
This commit is contained in:
@@ -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<HTMLDivElement>(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 (
|
||||
<div
|
||||
ref={componentRef}
|
||||
className={`absolute cursor-move p-2 shadow-sm ${isSelected ? "ring-2 ring-blue-500" : ""}`}
|
||||
className={`absolute cursor-move p-2 shadow-sm ${
|
||||
isSelected ? "ring-2 ring-blue-500" : isMultiSelected ? "ring-2 ring-blue-300" : ""
|
||||
}`}
|
||||
style={{
|
||||
left: `${component.x}px`,
|
||||
top: `${component.y}px`,
|
||||
|
||||
@@ -16,11 +16,16 @@ export function ReportDesignerCanvas() {
|
||||
canvasHeight,
|
||||
selectComponent,
|
||||
selectedComponentId,
|
||||
selectedComponentIds,
|
||||
removeComponent,
|
||||
showGrid,
|
||||
gridSize,
|
||||
snapValueToGrid,
|
||||
alignmentGuides,
|
||||
copyComponents,
|
||||
pasteComponents,
|
||||
undo,
|
||||
redo,
|
||||
} = useReportDesigner();
|
||||
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
@@ -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 (
|
||||
<div className="flex flex-1 flex-col overflow-hidden bg-gray-100">
|
||||
|
||||
@@ -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() {
|
||||
<Grid3x3 className="h-4 w-4" />
|
||||
{snapToGrid && showGrid ? "Grid ON" : "Grid OFF"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={undo}
|
||||
disabled={!canUndo}
|
||||
className="gap-2"
|
||||
title="실행 취소 (Ctrl+Z)"
|
||||
>
|
||||
<Undo2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={redo}
|
||||
disabled={!canRedo}
|
||||
className="gap-2"
|
||||
title="다시 실행 (Ctrl+Shift+Z)"
|
||||
>
|
||||
<Redo2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleReset} className="gap-2">
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
초기화
|
||||
|
||||
Reference in New Issue
Block a user