ERP-node/frontend/components/report/designer/CanvasComponent.tsx

246 lines
8.3 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
import { ComponentConfig } from "@/types/report";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
interface CanvasComponentProps {
component: ComponentConfig;
}
export function CanvasComponent({ component }: CanvasComponentProps) {
const { selectedComponentId, selectComponent, updateComponent, getQueryResult } = 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<HTMLDivElement>(null);
const isSelected = selectedComponentId === component.id;
// 드래그 시작
const handleMouseDown = (e: React.MouseEvent) => {
if ((e.target as HTMLElement).classList.contains("resize-handle")) {
return;
}
e.stopPropagation();
selectComponent(component.id);
setIsDragging(true);
setDragStart({
x: e.clientX - component.x,
y: e.clientY - component.y,
});
};
// 리사이즈 시작
const handleResizeStart = (e: React.MouseEvent) => {
e.stopPropagation();
setIsResizing(true);
setResizeStart({
x: e.clientX,
y: e.clientY,
width: component.width,
height: component.height,
});
};
// 마우스 이동 핸들러 (전역)
useState(() => {
const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
const newX = Math.max(0, e.clientX - dragStart.x);
const newY = Math.max(0, e.clientY - dragStart.y);
updateComponent(component.id, { x: newX, y: newY });
} 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);
updateComponent(component.id, { width: newWidth, height: newHeight });
}
};
const handleMouseUp = () => {
setIsDragging(false);
setIsResizing(false);
};
if (isDragging || isResizing) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}
});
// 표시할 값 결정
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 (
<div className="h-full w-full">
<div className="mb-1 flex items-center justify-between text-xs text-gray-500">
<span> </span>
{hasBinding && <span className="text-blue-600"> </span>}
</div>
<input
type="text"
className="w-full rounded border px-2 py-1 text-sm"
placeholder={displayValue}
value={displayValue}
readOnly
onClick={(e) => e.stopPropagation()}
/>
</div>
);
case "label":
return (
<div className="h-full w-full">
<div className="mb-1 flex items-center justify-between text-xs text-gray-500">
<span></span>
{hasBinding && <span className="text-blue-600"> </span>}
</div>
<div className="font-semibold">{displayValue}</div>
</div>
);
case "table":
// 테이블은 쿼리 결과의 모든 행과 필드를 표시
if (component.queryId) {
const queryResult = getQueryResult(component.queryId);
if (queryResult && queryResult.rows.length > 0) {
return (
<div className="h-full w-full overflow-auto">
<div className="mb-1 flex items-center justify-between text-xs text-gray-500">
<span> ( )</span>
<span className="text-blue-600"> </span>
</div>
<table className="w-full border-collapse text-xs">
<thead>
<tr className="bg-gray-100">
{queryResult.fields.map((field) => (
<th key={field} className="border p-1">
{field}
</th>
))}
</tr>
</thead>
<tbody>
{queryResult.rows.slice(0, 3).map((row, idx) => (
<tr key={idx}>
{queryResult.fields.map((field) => (
<td key={field} className="border p-1">
{String(row[field] ?? "")}
</td>
))}
</tr>
))}
{queryResult.rows.length > 3 && (
<tr>
<td colSpan={queryResult.fields.length} className="border p-1 text-center text-gray-400">
... {queryResult.rows.length - 3}
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
}
// 기본 테이블 (데이터 없을 때)
return (
<div className="h-full w-full overflow-auto">
<div className="mb-1 text-xs text-gray-500"> ( )</div>
<table className="w-full border-collapse text-xs">
<thead>
<tr className="bg-gray-100">
<th className="border p-1"></th>
<th className="border p-1"></th>
<th className="border p-1"></th>
</tr>
</thead>
<tbody>
<tr>
<td className="border p-1">1</td>
<td className="border p-1">10</td>
<td className="border p-1">50,000</td>
</tr>
</tbody>
</table>
</div>
);
default:
return <div> </div>;
}
};
return (
<div
ref={componentRef}
className={`absolute cursor-move rounded border-2 bg-white p-2 shadow-sm ${
isSelected ? "border-blue-500 ring-2 ring-blue-300" : "border-gray-400"
}`}
style={{
left: `${component.x}px`,
top: `${component.y}px`,
width: `${component.width}px`,
height: `${component.height}px`,
zIndex: component.zIndex,
}}
onMouseDown={handleMouseDown}
>
{renderContent()}
{/* 리사이즈 핸들 (선택된 경우만) */}
{isSelected && (
<div
className="resize-handle absolute right-0 bottom-0 h-3 w-3 cursor-se-resize rounded-full bg-blue-500"
style={{ transform: "translate(50%, 50%)" }}
onMouseDown={handleResizeStart}
/>
)}
</div>
);
}