"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 { Ruler } from "./Ruler"; import { v4 as uuidv4 } from "uuid"; export function ReportDesignerCanvas() { const canvasRef = useRef(null); const { components, addComponent, updateComponent, canvasWidth, canvasHeight, selectComponent, selectedComponentId, selectedComponentIds, removeComponent, showGrid, gridSize, snapValueToGrid, alignmentGuides, copyComponents, pasteComponents, undo, redo, showRuler, } = 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; // 컴포넌트 타입별 기본 설정 let width = 200; let height = 100; if (item.componentType === "table") { height = 200; } else if (item.componentType === "image") { width = 150; height = 150; } else if (item.componentType === "divider") { width = 300; height = 2; } // 새 컴포넌트 생성 (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(width), height: snapValueToGrid(height), 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, // 이미지 전용 ...(item.componentType === "image" && { imageUrl: "", objectFit: "contain" as const, }), // 구분선 전용 ...(item.componentType === "divider" && { orientation: "horizontal" as const, lineStyle: "solid" as const, lineWidth: 1, lineColor: "#000000", }), }; 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; } // 화살표 키: 선택된 컴포넌트 이동 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 || component.locked) 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) { selectedComponentIds.forEach((id) => { const component = components.find((c) => c.id === id); if (component && !component.locked) { removeComponent(id); } }); } else if (selectedComponentId) { const component = components.find((c) => c.id === selectedComponentId); if (component && !component.locked) { 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); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ selectedComponentId, selectedComponentIds, components, removeComponent, copyComponents, pasteComponents, undo, redo, ]); return (
{/* 작업 영역 제목 */}
작업 영역
{/* 캔버스 스크롤 영역 */}
{/* 눈금자와 캔버스를 감싸는 컨테이너 */}
{/* 좌상단 코너 + 가로 눈금자 */} {showRuler && (
{/* 좌상단 코너 (20x20) */}
{/* 가로 눈금자 */}
)} {/* 세로 눈금자 + 캔버스 */}
{/* 세로 눈금자 */} {showRuler && } {/* 캔버스 */}
{ canvasRef.current = node; drop(node); }} className={`relative 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 && (

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

)}
); }