"use client"; import { useRef, useEffect } from "react"; import { useDrop } from "react-dnd"; import { useReportDesigner } from "@/contexts/ReportDesignerContext"; import { ComponentConfig } from "@/types/report"; import { CanvasComponent } from "./CanvasComponent"; import { v4 as uuidv4 } from "uuid"; export function ReportDesignerCanvas() { const canvasRef = useRef(null); const { components, addComponent, canvasWidth, canvasHeight, selectComponent, selectedComponentId, selectedComponentIds, removeComponent, showGrid, gridSize, snapValueToGrid, alignmentGuides, copyComponents, pasteComponents, undo, redo, } = useReportDesigner(); const [{ isOver }, drop] = useDrop(() => ({ accept: "component", drop: (item: { componentType: string }, monitor) => { if (!canvasRef.current) return; const offset = monitor.getClientOffset(); const canvasRect = canvasRef.current.getBoundingClientRect(); if (!offset) return; const x = offset.x - canvasRect.left; const y = offset.y - canvasRect.top; // 새 컴포넌트 생성 (Grid Snap 적용) const newComponent: ComponentConfig = { id: `comp_${uuidv4()}`, type: item.componentType, x: snapValueToGrid(Math.max(0, x - 100)), y: snapValueToGrid(Math.max(0, y - 25)), width: snapValueToGrid(200), height: snapValueToGrid(item.componentType === "table" ? 200 : 100), zIndex: components.length, fontSize: 13, fontFamily: "Malgun Gothic", fontWeight: "normal", fontColor: "#000000", backgroundColor: "transparent", borderWidth: 0, borderColor: "#cccccc", borderRadius: 5, textAlign: "left", padding: 10, visible: true, printable: true, }; addComponent(newComponent); }, collect: (monitor) => ({ isOver: monitor.isOver(), }), })); const handleCanvasClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { selectComponent(null); } }; // 키보드 단축키 (Delete, Ctrl+C, Ctrl+V) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // 입력 필드에서는 단축키 무시 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, selectedComponentIds, removeComponent, copyComponents, pasteComponents, undo, redo]); return (
{/* 작업 영역 제목 */}
작업 영역
{/* 캔버스 스크롤 영역 */}
{ canvasRef.current = node; drop(node); }} className={`relative mx-auto bg-white shadow-lg ${isOver ? "ring-2 ring-blue-500" : ""}`} style={{ width: `${canvasWidth}mm`, minHeight: `${canvasHeight}mm`, backgroundImage: showGrid ? ` linear-gradient(to right, #e5e7eb 1px, transparent 1px), linear-gradient(to bottom, #e5e7eb 1px, transparent 1px) ` : undefined, backgroundSize: showGrid ? `${gridSize}px ${gridSize}px` : undefined, }} onClick={handleCanvasClick} > {/* 정렬 가이드라인 렌더링 */} {alignmentGuides.vertical.map((x, index) => (
))} {alignmentGuides.horizontal.map((y, index) => (
))} {/* 컴포넌트 렌더링 */} {components.map((component) => ( ))} {/* 빈 캔버스 안내 */} {components.length === 0 && (

왼쪽에서 컴포넌트를 드래그하여 추가하세요

)}
); }