164 lines
5.1 KiB
TypeScript
164 lines
5.1 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 } = 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 renderContent = () => {
|
||
|
|
switch (component.type) {
|
||
|
|
case "text":
|
||
|
|
return (
|
||
|
|
<div className="h-full w-full">
|
||
|
|
<div className="mb-1 text-xs text-gray-500">텍스트 필드</div>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
className="w-full rounded border px-2 py-1 text-sm"
|
||
|
|
placeholder="텍스트 입력"
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "label":
|
||
|
|
return (
|
||
|
|
<div className="h-full w-full">
|
||
|
|
<div className="mb-1 text-xs text-gray-500">레이블</div>
|
||
|
|
<div className="font-semibold">레이블 텍스트</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "table":
|
||
|
|
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>
|
||
|
|
<tr>
|
||
|
|
<td className="border p-1">품목2</td>
|
||
|
|
<td className="border p-1">5</td>
|
||
|
|
<td className="border p-1">30,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>
|
||
|
|
);
|
||
|
|
}
|