ERP-node/frontend/components/admin/dashboard/DashboardCanvas.tsx

176 lines
5.7 KiB
TypeScript

"use client";
import React, { forwardRef, useState, useCallback, useMemo } from "react";
import { DashboardElement, ElementType, ElementSubtype, DragData } from "./types";
import { CanvasElement } from "./CanvasElement";
import { GRID_CONFIG, snapToGrid, calculateGridConfig } from "./gridUtils";
interface DashboardCanvasProps {
elements: DashboardElement[];
selectedElement: string | null;
onCreateElement: (type: ElementType, subtype: ElementSubtype, x: number, y: number) => void;
onUpdateElement: (id: string, updates: Partial<DashboardElement>) => void;
onRemoveElement: (id: string) => void;
onSelectElement: (id: string | null) => void;
onConfigureElement?: (element: DashboardElement) => void;
backgroundColor?: string;
canvasWidth?: number;
canvasHeight?: number;
}
/**
* 대시보드 캔버스 컴포넌트
* - 드래그 앤 드롭 영역
* - 12 컬럼 그리드 배경
* - 스냅 기능
* - 요소 배치 및 관리
*/
export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
(
{
elements,
selectedElement,
onCreateElement,
onUpdateElement,
onRemoveElement,
onSelectElement,
onConfigureElement,
backgroundColor = "#f9fafb",
canvasWidth = 1560,
canvasHeight = 768,
},
ref,
) => {
const [isDragOver, setIsDragOver] = useState(false);
// 현재 캔버스 크기에 맞는 그리드 설정 계산
const gridConfig = useMemo(() => calculateGridConfig(canvasWidth), [canvasWidth]);
const cellSize = gridConfig.CELL_SIZE;
// 드래그 오버 처리
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
setIsDragOver(true);
}, []);
// 드래그 리브 처리
const handleDragLeave = useCallback((e: React.DragEvent) => {
if (e.currentTarget === e.target) {
setIsDragOver(false);
}
}, []);
// 드롭 처리 (그리드 스냅 적용)
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
try {
const dragData: DragData = JSON.parse(e.dataTransfer.getData("application/json"));
if (!ref || typeof ref === "function") return;
const rect = ref.current?.getBoundingClientRect();
if (!rect) return;
// 캔버스 스크롤을 고려한 정확한 위치 계산
const rawX = e.clientX - rect.left + (ref.current?.scrollLeft || 0);
const rawY = e.clientY - rect.top + (ref.current?.scrollTop || 0);
// 그리드에 스냅 (동적 셀 크기 사용)
let snappedX = snapToGrid(rawX, cellSize);
const snappedY = snapToGrid(rawY, cellSize);
// X 좌표가 캔버스 너비를 벗어나지 않도록 제한
const maxX = canvasWidth - cellSize * 2; // 최소 2칸 너비 보장
snappedX = Math.max(0, Math.min(snappedX, maxX));
onCreateElement(dragData.type, dragData.subtype, snappedX, snappedY);
} catch {
// 드롭 데이터 파싱 오류 무시
}
},
[ref, onCreateElement, canvasWidth, cellSize],
);
// 캔버스 클릭 시 선택 해제
const handleCanvasClick = useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onSelectElement(null);
}
},
[onSelectElement],
);
// 동적 그리드 크기 계산
const cellWithGap = cellSize + GRID_CONFIG.GAP;
const gridSize = `${cellWithGap}px ${cellWithGap}px`;
// 12개 컬럼 구분선 위치 계산
const columnLines = Array.from({ length: GRID_CONFIG.COLUMNS + 1 }, (_, i) => i * cellWithGap);
return (
<div
ref={ref}
className={`relative w-full rounded-lg shadow-inner ${isDragOver ? "bg-blue-50/50" : ""} `}
style={{
backgroundColor,
height: `${canvasHeight}px`,
minHeight: `${canvasHeight}px`,
// 세밀한 그리드 배경
backgroundImage: `
linear-gradient(rgba(59, 130, 246, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(59, 130, 246, 0.08) 1px, transparent 1px)
`,
backgroundSize: gridSize,
backgroundPosition: "0 0",
backgroundRepeat: "repeat",
}}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={handleCanvasClick}
>
{/* 12개 컬럼 메인 구분선 */}
{columnLines.map((x, i) => (
<div
key={`col-${i}`}
className="pointer-events-none absolute top-0 h-full"
style={{
left: `${x}px`,
width: "2px",
zIndex: 1,
}}
/>
))}
{/* 배치된 요소들 렌더링 */}
{elements.length === 0 && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center text-gray-400">
<div className="text-center">
<div className="text-sm"> </div>
</div>
</div>
)}
{elements.map((element) => (
<CanvasElement
key={element.id}
element={element}
isSelected={selectedElement === element.id}
cellSize={cellSize}
canvasWidth={canvasWidth}
onUpdate={onUpdateElement}
onRemove={onRemoveElement}
onSelect={onSelectElement}
onConfigure={onConfigureElement}
/>
))}
</div>
);
},
);
DashboardCanvas.displayName = "DashboardCanvas";