"use client"; import { useRef, useState, useEffect } from "react"; import { ComponentConfig } from "@/types/report"; import { useReportDesigner } from "@/contexts/ReportDesignerContext"; interface CanvasComponentProps { component: ComponentConfig; } export function CanvasComponent({ component }: CanvasComponentProps) { const { selectedComponentId, selectedComponentIds, selectComponent, updateComponent, getQueryResult, snapValueToGrid, calculateAlignmentGuides, clearAlignmentGuides, } = useReportDesigner(); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const [resizeStart, setResizeStart] = useState({ x: 0, y: 0, width: 0, height: 0 }); const componentRef = useRef(null); const isSelected = selectedComponentId === component.id; const isMultiSelected = selectedComponentIds.includes(component.id); const isLocked = component.locked === true; // 드래그 시작 const handleMouseDown = (e: React.MouseEvent) => { if ((e.target as HTMLElement).classList.contains("resize-handle")) { return; } // 잠긴 컴포넌트는 드래그 불가 if (isLocked) { e.stopPropagation(); // Ctrl/Cmd 키 감지 (다중 선택) const isMultiSelect = e.ctrlKey || e.metaKey; selectComponent(component.id, isMultiSelect); return; } e.stopPropagation(); // Ctrl/Cmd 키 감지 (다중 선택) const isMultiSelect = e.ctrlKey || e.metaKey; selectComponent(component.id, isMultiSelect); setIsDragging(true); setDragStart({ x: e.clientX - component.x, y: e.clientY - component.y, }); }; // 리사이즈 시작 const handleResizeStart = (e: React.MouseEvent) => { // 잠긴 컴포넌트는 리사이즈 불가 if (isLocked) { e.stopPropagation(); return; } e.stopPropagation(); setIsResizing(true); setResizeStart({ x: e.clientX, y: e.clientY, width: component.width, height: component.height, }); }; // 마우스 이동 핸들러 (전역) useEffect(() => { if (!isDragging && !isResizing) return; const handleMouseMove = (e: MouseEvent) => { if (isDragging) { const newX = Math.max(0, e.clientX - dragStart.x); const newY = Math.max(0, e.clientY - dragStart.y); const snappedX = snapValueToGrid(newX); const snappedY = snapValueToGrid(newY); // 정렬 가이드라인 계산 calculateAlignmentGuides(component.id, snappedX, snappedY, component.width, component.height); // Grid Snap 적용 updateComponent(component.id, { x: snappedX, y: snappedY, }); } else if (isResizing) { const deltaX = e.clientX - resizeStart.x; const deltaY = e.clientY - resizeStart.y; const newWidth = Math.max(50, resizeStart.width + deltaX); const newHeight = Math.max(30, resizeStart.height + deltaY); // Grid Snap 적용 updateComponent(component.id, { width: snapValueToGrid(newWidth), height: snapValueToGrid(newHeight), }); } }; const handleMouseUp = () => { setIsDragging(false); setIsResizing(false); // 가이드라인 초기화 clearAlignmentGuides(); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [ isDragging, isResizing, dragStart.x, dragStart.y, resizeStart.x, resizeStart.y, resizeStart.width, resizeStart.height, component.id, component.width, component.height, updateComponent, snapValueToGrid, calculateAlignmentGuides, clearAlignmentGuides, ]); // 표시할 값 결정 const getDisplayValue = (): string => { // 쿼리와 필드가 연결되어 있으면 실제 데이터 조회 if (component.queryId && component.fieldName) { const queryResult = getQueryResult(component.queryId); // 실행 결과가 있으면 첫 번째 행의 해당 필드 값 표시 if (queryResult && queryResult.rows.length > 0) { const firstRow = queryResult.rows[0]; const value = firstRow[component.fieldName]; // 값이 있으면 문자열로 변환하여 반환 if (value !== null && value !== undefined) { return String(value); } } // 실행 결과가 없거나 값이 없으면 필드명 표시 return `{${component.fieldName}}`; } // 기본값이 있으면 기본값 표시 if (component.defaultValue) { return component.defaultValue; } // 둘 다 없으면 타입에 따라 기본 텍스트 return component.type === "text" ? "텍스트 입력" : "레이블 텍스트"; }; // 컴포넌트 타입별 렌더링 const renderContent = () => { const displayValue = getDisplayValue(); const hasBinding = component.queryId && component.fieldName; switch (component.type) { case "text": return (
텍스트 필드 {hasBinding && ● 연결됨}
{displayValue}
); case "label": return (
레이블 {hasBinding && ● 연결됨}
{displayValue}
); case "table": // 테이블은 쿼리 결과의 모든 행과 필드를 표시 if (component.queryId) { const queryResult = getQueryResult(component.queryId); if (queryResult && queryResult.rows.length > 0) { return (
테이블 (디테일 데이터) ● 연결됨
{queryResult.fields.map((field) => ( ))} {queryResult.rows.slice(0, 3).map((row, idx) => ( {queryResult.fields.map((field) => ( ))} ))} {queryResult.rows.length > 3 && ( )}
{field}
{String(row[field] ?? "")}
... 외 {queryResult.rows.length - 3}건
); } } // 기본 테이블 (데이터 없을 때) return (
테이블 (디테일 데이터)
품목명 수량 단가
품목1 10 50,000
); default: return
알 수 없는 컴포넌트
; } }; return (
{renderContent()} {/* 잠금 표시 */} {isLocked && (
🔒
)} {/* 리사이즈 핸들 (선택된 경우만, 잠금 안 된 경우만) */} {isSelected && !isLocked && (
)}
); }