"use client"; import React, { forwardRef, useState, useCallback, useMemo, useEffect } from "react"; import { DashboardElement, ElementType, ElementSubtype, DragData } from "./types"; import { CanvasElement } from "./CanvasElement"; import { GRID_CONFIG, snapToGrid, calculateGridConfig, calculateVerticalGuidelines, calculateHorizontalGuidelines, calculateBoxSize, magneticSnap, } from "./gridUtils"; import { resolveAllCollisions } from "./collisionUtils"; interface DashboardCanvasProps { elements: DashboardElement[]; selectedElement: string | null; selectedElements?: string[]; // ๐Ÿ”ฅ ๋‹ค์ค‘ ์„ ํƒ๋œ ์š”์†Œ ID ๋ฐฐ์—ด onCreateElement: (type: ElementType, subtype: ElementSubtype, x: number, y: number) => void; onUpdateElement: (id: string, updates: Partial) => void; onRemoveElement: (id: string) => void; onSelectElement: (id: string | null) => void; onSelectMultiple?: (ids: string[]) => void; // ๐Ÿ”ฅ ๋‹ค์ค‘ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ onConfigureElement?: (element: DashboardElement) => void; backgroundColor?: string; canvasWidth?: number; canvasHeight?: number; } /** * ๋Œ€์‹œ๋ณด๋“œ ์บ”๋ฒ„์Šค ์ปดํฌ๋„ŒํŠธ * - ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์˜์—ญ * - 12 ์ปฌ๋Ÿผ ๊ทธ๋ฆฌ๋“œ ๋ฐฐ๊ฒฝ * - ์Šค๋ƒ… ๊ธฐ๋Šฅ * - ์š”์†Œ ๋ฐฐ์น˜ ๋ฐ ๊ด€๋ฆฌ */ export const DashboardCanvas = forwardRef( ( { elements, selectedElement, selectedElements = [], onCreateElement, onUpdateElement, onRemoveElement, onSelectElement, onSelectMultiple, onConfigureElement, backgroundColor = "transparent", canvasWidth = 1560, canvasHeight = 768, }, ref, ) => { const [isDragOver, setIsDragOver] = useState(false); // ๐Ÿ”ฅ ์„ ํƒ ๋ฐ•์Šค ์ƒํƒœ const [selectionBox, setSelectionBox] = useState<{ startX: number; startY: number; endX: number; endY: number; } | null>(null); const [isSelecting, setIsSelecting] = useState(false); const [justSelected, setJustSelected] = useState(false); // ๐Ÿ”ฅ ๋ฐฉ๊ธˆ ์„ ํƒํ–ˆ๋Š”์ง€ ํ”Œ๋ž˜๊ทธ const [isDraggingAny, setIsDraggingAny] = useState(false); // ๐Ÿ”ฅ ํ˜„์žฌ ๋“œ๋ž˜๊ทธ ์ค‘์ธ์ง€ ํ”Œ๋ž˜๊ทธ // ๐Ÿ”ฅ ๋‹ค์ค‘ ์„ ํƒ๋œ ์œ„์ ฏ๋“ค์˜ ์ž„์‹œ ์œ„์น˜ (๋“œ๋ž˜๊ทธ ์ค‘ ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ) const [multiDragOffsets, setMultiDragOffsets] = useState>({}); // ๐Ÿ”ฅ ์„ ํƒ ๋ฐ•์Šค ๋“œ๋ž˜๊ทธ ์ค‘ ์ž๋™ ์Šคํฌ๋กค const lastMouseYForSelectionRef = React.useRef(window.innerHeight / 2); const selectionAutoScrollFrameRef = React.useRef(null); // ํ˜„์žฌ ์บ”๋ฒ„์Šค ํฌ๊ธฐ์— ๋งž๋Š” ๊ทธ๋ฆฌ๋“œ ์„ค์ • ๊ณ„์‚ฐ const gridConfig = useMemo(() => calculateGridConfig(canvasWidth), [canvasWidth]); const cellSize = gridConfig.CELL_SIZE; // ๐Ÿ”ฅ ๊ทธ๋ฆฌ๋“œ ๋ฐ•์Šค ์‹œ์Šคํ…œ - 12๊ฐœ ๋ฐ•์Šค๊ฐ€ ์บ”๋ฒ„์Šค ๋„ˆ๋น„์— ๊ฝ‰ ์ฐจ๊ฒŒ const verticalGuidelines = useMemo(() => calculateVerticalGuidelines(canvasWidth), [canvasWidth]); const horizontalGuidelines = useMemo( () => calculateHorizontalGuidelines(canvasHeight, canvasWidth), [canvasHeight, canvasWidth], ); const boxSize = useMemo(() => calculateBoxSize(canvasWidth), [canvasWidth]); // ์ถฉ๋Œ ๋ฐฉ์ง€ ๊ธฐ๋Šฅ์ด ํฌํ•จ๋œ ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ const handleUpdateWithCollisionDetection = useCallback( (id: string, updates: Partial) => { // position์ด๋‚˜ size๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ์†์„ฑ ์—…๋ฐ์ดํŠธ๋Š” ์ถฉ๋Œ ๊ฐ์ง€ ์—†์ด ๋ฐ”๋กœ ์ฒ˜๋ฆฌ if (!updates.position && !updates.size) { onUpdateElement(id, updates); return; } // ์—…๋ฐ์ดํŠธํ•  ์š”์†Œ ์ฐพ๊ธฐ const elementIndex = elements.findIndex((el) => el.id === id); if (elementIndex === -1) { onUpdateElement(id, updates); return; } // position์ด๋‚˜ size์™€ ๋‹ค๋ฅธ ์†์„ฑ์ด ํ•จ๊ป˜ ์žˆ์œผ๋ฉด ๋ถ„๋ฆฌํ•ด์„œ ์ฒ˜๋ฆฌ const positionSizeUpdates: any = {}; const otherUpdates: any = {}; Object.keys(updates).forEach((key) => { if (key === "position" || key === "size") { positionSizeUpdates[key] = (updates as any)[key]; } else { otherUpdates[key] = (updates as any)[key]; } }); // ๋‹ค๋ฅธ ์†์„ฑ๋“ค์€ ๋จผ์ € ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธ if (Object.keys(otherUpdates).length > 0) { onUpdateElement(id, otherUpdates); } // position/size๊ฐ€ ์—†์œผ๋ฉด ์—ฌ๊ธฐ์„œ ์ข…๋ฃŒ if (Object.keys(positionSizeUpdates).length === 0) { return; } // ์ž„์‹œ๋กœ ์—…๋ฐ์ดํŠธ๋œ ์š”์†Œ ๋ฐฐ์—ด ์ƒ์„ฑ const updatedElements = elements.map((el) => el.id === id ? { ...el, ...positionSizeUpdates, position: positionSizeUpdates.position || el.position, size: positionSizeUpdates.size || el.size, } : el, ); // ์„œ๋ธŒ ๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ ๊ณ„์‚ฐ (cellSize / 3) const subGridSize = Math.floor(cellSize / 3); // ์ถฉ๋Œ ํ•ด๊ฒฐ (์„œ๋ธŒ ๊ทธ๋ฆฌ๋“œ ๋‹จ์œ„๋กœ ์Šค๋ƒ… ๋ฐ ์ถฉ๋Œ ๊ฐ์ง€) const resolvedElements = resolveAllCollisions(updatedElements, id, subGridSize, canvasWidth, cellSize); // ๋ณ€๊ฒฝ๋œ ์š”์†Œ๋“ค๋งŒ ์—…๋ฐ์ดํŠธ resolvedElements.forEach((resolvedEl, idx) => { const originalEl = elements[idx]; if ( resolvedEl.position.x !== originalEl.position.x || resolvedEl.position.y !== originalEl.position.y || resolvedEl.size.width !== originalEl.size.width || resolvedEl.size.height !== originalEl.size.height ) { onUpdateElement(resolvedEl.id, { position: resolvedEl.position, size: resolvedEl.size, }); } }); }, [elements, onUpdateElement, cellSize, canvasWidth], ); // ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„ ์ฒ˜๋ฆฌ 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 = magneticSnap(rawX, verticalGuidelines); let snappedY = magneticSnap(rawY, horizontalGuidelines); // X ์ขŒํ‘œ๊ฐ€ ์บ”๋ฒ„์Šค ๋„ˆ๋น„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ์ œํ•œ (์ตœ์†Œ 2์นธ ๋„ˆ๋น„ ๋ณด์žฅ) const minElementWidth = cellSize * 2 + GRID_CONFIG.GAP; const maxX = canvasWidth - minElementWidth; snappedX = Math.max(0, Math.min(snappedX, maxX)); onCreateElement(dragData.type, dragData.subtype, snappedX, snappedY); } catch { // ๋“œ๋กญ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ๋ฌด์‹œ } }, [ref, onCreateElement, canvasWidth, cellSize, verticalGuidelines, horizontalGuidelines], ); // ๐Ÿ”ฅ ์„ ํƒ ๋ฐ•์Šค ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ const handleMouseDown = useCallback( (e: React.MouseEvent) => { // ๐Ÿ”ฅ ์œ„์ ฏ ๋‚ด๋ถ€ ํด๋ฆญ์ด ์•„๋‹Œ ๊ฒฝ์šฐ๋งŒ (data-element-id๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ) const target = e.target as HTMLElement; const isWidget = target.closest("[data-element-id]"); if (isWidget) { // console.log("๐Ÿšซ ์œ„์ ฏ ๋‚ด๋ถ€ ํด๋ฆญ - ์„ ํƒ ๋ฐ•์Šค ์‹œ์ž‘ ์•ˆํ•จ"); return; } // console.log("โœ… ๋นˆ ๊ณต๊ฐ„ ํด๋ฆญ - ์„ ํƒ ๋ฐ•์Šค ์‹œ์ž‘"); if (!ref || typeof ref === "function") return; const rect = ref.current?.getBoundingClientRect(); if (!rect) return; const x = e.clientX - rect.left + (ref.current?.scrollLeft || 0); const y = e.clientY - rect.top + (ref.current?.scrollTop || 0); // ๐Ÿ”ฅ ์ผ๋‹จ ์‹œ์ž‘ ์œ„์น˜๋งŒ ์ €์žฅ (์•„์ง isSelecting์€ false) setSelectionBox({ startX: x, startY: y, endX: x, endY: y }); }, [ref], ); // ๐Ÿ”ฅ ์„ ํƒ ๋ฐ•์Šค ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ const handleMouseUp = useCallback(() => { if (!isSelecting || !selectionBox) { setIsSelecting(false); setSelectionBox(null); return; } if (!onSelectMultiple) { setIsSelecting(false); setSelectionBox(null); return; } // ์„ ํƒ ๋ฐ•์Šค ์˜์—ญ ๊ณ„์‚ฐ const minX = Math.min(selectionBox.startX, selectionBox.endX); const maxX = Math.max(selectionBox.startX, selectionBox.endX); const minY = Math.min(selectionBox.startY, selectionBox.endY); const maxY = Math.max(selectionBox.startY, selectionBox.endY); // console.log("๐Ÿ” ์„ ํƒ ๋ฐ•์Šค:", { minX, maxX, minY, maxY }); // ์„ ํƒ ๋ฐ•์Šค ์•ˆ์— ์žˆ๋Š” ์š”์†Œ๋“ค ์ฐพ๊ธฐ (70% ์ด์ƒ ๊ฒน์น˜๋ฉด ์„ ํƒ) const selectedIds = elements .filter((el) => { const elLeft = el.position.x; const elRight = el.position.x + el.size.width; const elTop = el.position.y; const elBottom = el.position.y + el.size.height; // ๊ฒน์น˜๋Š” ์˜์—ญ ๊ณ„์‚ฐ const overlapLeft = Math.max(elLeft, minX); const overlapRight = Math.min(elRight, maxX); const overlapTop = Math.max(elTop, minY); const overlapBottom = Math.min(elBottom, maxY); // ๊ฒน์น˜๋Š” ์˜์—ญ์ด ์—†์œผ๋ฉด false if (overlapRight < overlapLeft || overlapBottom < overlapTop) { return false; } // ๊ฒน์น˜๋Š” ์˜์—ญ์˜ ๋„“์ด const overlapArea = (overlapRight - overlapLeft) * (overlapBottom - overlapTop); // ์š”์†Œ์˜ ์ „์ฒด ๋„“์ด const elementArea = el.size.width * el.size.height; // 70% ์ด์ƒ ๊ฒน์น˜๋ฉด ์„ ํƒ const overlapPercentage = overlapArea / elementArea; // console.log(`๐Ÿ“ฆ ์š”์†Œ ${el.id}:`, { // position: el.position, // size: el.size, // overlapPercentage: (overlapPercentage * 100).toFixed(1) + "%", // selected: overlapPercentage >= 0.7, // }); return overlapPercentage >= 0.7; }) .map((el) => el.id); // console.log("โœ… ์„ ํƒ๋œ ์š”์†Œ:", selectedIds); if (selectedIds.length > 0) { onSelectMultiple(selectedIds); setJustSelected(true); // ๐Ÿ”ฅ ๋ฐฉ๊ธˆ ์„ ํƒํ–ˆ์Œ์„ ํ‘œ์‹œ setTimeout(() => setJustSelected(false), 100); // 100ms ํ›„ ํ”Œ๋ž˜๊ทธ ํ•ด์ œ } else { onSelectMultiple([]); // ๋นˆ ๋ฐฐ์—ด๋„ ์ „๋‹ฌ } setIsSelecting(false); setSelectionBox(null); }, [isSelecting, selectionBox, elements, onSelectMultiple]); // ๐Ÿ”ฅ document ๋ ˆ๋ฒจ์—์„œ ๋งˆ์šฐ์Šค ์ด๋™/ํ•ด์ œ ๊ฐ์ง€ (์œ„์ ฏ ์œ„์—์„œ๋„ ์ž‘๋™) useEffect(() => { if (!selectionBox) return; const handleDocumentMouseMove = (e: MouseEvent) => { if (!ref || typeof ref === "function") return; const rect = ref.current?.getBoundingClientRect(); if (!rect) return; const x = e.clientX - rect.left + (ref.current?.scrollLeft || 0); const y = e.clientY - rect.top + (ref.current?.scrollTop || 0); // ๐Ÿ”ฅ ์ž๋™ ์Šคํฌ๋กค์„ ์œ„ํ•œ ๋งˆ์šฐ์Šค Y ์œ„์น˜ ์ €์žฅ lastMouseYForSelectionRef.current = e.clientY; // console.log("๐Ÿ–ฑ๏ธ ๋งˆ์šฐ์Šค ์ด๋™:", { x, y, startX: selectionBox.startX, startY: selectionBox.startY, isSelecting }); // ๐Ÿ”ฅ selectionBox๊ฐ€ ์žˆ์ง€๋งŒ ์•„์ง isSelecting์ด false์ธ ๊ฒฝ์šฐ (๋“œ๋ž˜๊ทธ ์‹œ์ž‘ ๋Œ€๊ธฐ) if (!isSelecting) { const deltaX = Math.abs(x - selectionBox.startX); const deltaY = Math.abs(y - selectionBox.startY); // console.log("๐Ÿ“ ์ด๋™ ๊ฑฐ๋ฆฌ:", { deltaX, deltaY }); // ๐Ÿ”ฅ 5px ์ด์ƒ ์›€์ง์ด๋ฉด ์„ ํƒ ๋ฐ•์Šค ํ™œ์„ฑํ™” (์œ„์ ฏ ๋“œ๋ž˜๊ทธ์™€ ๊ตฌ๋ถ„) if (deltaX > 5 || deltaY > 5) { // console.log("๐ŸŽฏ ์„ ํƒ ๋ฐ•์Šค ํ™œ์„ฑํ™” (5px ์ด์ƒ ์ด๋™)"); setIsSelecting(true); } return; } // ๐Ÿ”ฅ ์„ ํƒ ๋ฐ•์Šค ์—…๋ฐ์ดํŠธ // console.log("๐Ÿ“ฆ ์„ ํƒ ๋ฐ•์Šค ์—…๋ฐ์ดํŠธ:", { startX: selectionBox.startX, startY: selectionBox.startY, endX: x, endY: y }); setSelectionBox((prev) => (prev ? { ...prev, endX: x, endY: y } : null)); }; const handleDocumentMouseUp = () => { // console.log("๐Ÿ–ฑ๏ธ ๋งˆ์šฐ์Šค ์—… - handleMouseUp ํ˜ธ์ถœ"); handleMouseUp(); }; document.addEventListener("mousemove", handleDocumentMouseMove); document.addEventListener("mouseup", handleDocumentMouseUp); return () => { document.removeEventListener("mousemove", handleDocumentMouseMove); document.removeEventListener("mouseup", handleDocumentMouseUp); }; }, [selectionBox, isSelecting, ref, handleMouseUp]); // ๐Ÿ”ฅ ์„ ํƒ ๋ฐ•์Šค ๋“œ๋ž˜๊ทธ ์ค‘ ์ž๋™ ์Šคํฌ๋กค useEffect(() => { if (!isSelecting) { // console.log("โŒ ์ž๋™ ์Šคํฌ๋กค ๋น„ํ™œ์„ฑํ™”: isSelecting =", isSelecting); return; } // console.log("โœ… ์ž๋™ ์Šคํฌ๋กค ํ™œ์„ฑํ™”: isSelecting =", isSelecting); const scrollSpeed = 3; const scrollThreshold = 100; let animationFrameId: number; let lastTime = performance.now(); const autoScrollLoop = (currentTime: number) => { const viewportHeight = window.innerHeight; const lastMouseY = lastMouseYForSelectionRef.current; let shouldScroll = false; let scrollDirection = 0; if (lastMouseY < scrollThreshold) { shouldScroll = true; scrollDirection = -scrollSpeed; // console.log("โฌ†๏ธ ์œ„๋กœ ์Šคํฌ๋กค (์„ ํƒ ๋ฐ•์Šค):", { lastMouseY, scrollThreshold }); } else if (lastMouseY > viewportHeight - scrollThreshold) { shouldScroll = true; scrollDirection = scrollSpeed; // console.log("โฌ‡๏ธ ์•„๋ž˜๋กœ ์Šคํฌ๋กค (์„ ํƒ ๋ฐ•์Šค):", { lastMouseY, boundary: viewportHeight - scrollThreshold }); } const deltaTime = currentTime - lastTime; if (shouldScroll && deltaTime >= 10) { window.scrollBy(0, scrollDirection); // console.log("โœ… ์Šคํฌ๋กค ์‹คํ–‰ (์„ ํƒ ๋ฐ•์Šค):", { scrollDirection, deltaTime }); lastTime = currentTime; } animationFrameId = requestAnimationFrame(autoScrollLoop); }; animationFrameId = requestAnimationFrame(autoScrollLoop); selectionAutoScrollFrameRef.current = animationFrameId; return () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); } // console.log("๐Ÿ›‘ ์ž๋™ ์Šคํฌ๋กค ์ •๋ฆฌ"); }; }, [isSelecting]); // ์บ”๋ฒ„์Šค ํด๋ฆญ ์‹œ ์„ ํƒ ํ•ด์ œ const handleCanvasClick = useCallback( (e: React.MouseEvent) => { // ๐Ÿ”ฅ ๋ฐฉ๊ธˆ ์„ ํƒํ–ˆ๊ฑฐ๋‚˜ ๋“œ๋ž˜๊ทธ ์ค‘์ด๋ฉด ํด๋ฆญ ์ด๋ฒคํŠธ ๋ฌด์‹œ (์„ ํƒ ํ•ด์ œ ๋ฐฉ์ง€) if (justSelected || isDraggingAny) { // console.log("๐Ÿšซ ๋ฐฉ๊ธˆ ์„ ํƒํ–ˆ๊ฑฐ๋‚˜ ๋“œ๋ž˜๊ทธ ์ค‘์ด๋ฏ€๋กœ ํด๋ฆญ ์ด๋ฒคํŠธ ๋ฌด์‹œ"); return; } if (e.target === e.currentTarget) { // console.log("โœ… ๋นˆ ๊ณต๊ฐ„ ํด๋ฆญ - ์„ ํƒ ํ•ด์ œ"); onSelectElement(null); if (onSelectMultiple) { onSelectMultiple([]); } } }, [onSelectElement, onSelectMultiple, justSelected, isDraggingAny], ); // ๋™์  ๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ ๊ณ„์‚ฐ const cellWithGap = cellSize + GRID_CONFIG.GAP; const gridSize = `${cellWithGap}px ${cellWithGap}px`; // ์„œ๋ธŒ๊ทธ๋ฆฌ๋“œ ํฌ๊ธฐ ๊ณ„์‚ฐ (gridConfig์—์„œ ์ •ํ™•ํ•˜๊ฒŒ ๊ณ„์‚ฐ๋œ ๊ฐ’ ์‚ฌ์šฉ) const subGridSize = gridConfig.SUB_GRID_SIZE; // 12๊ฐœ ์ปฌ๋Ÿผ ๊ตฌ๋ถ„์„  ์œ„์น˜ ๊ณ„์‚ฐ const columnLines = Array.from({ length: GRID_CONFIG.COLUMNS + 1 }, (_, i) => i * cellWithGap); // ๐Ÿ”ฅ ์„ ํƒ ๋ฐ•์Šค ์Šคํƒ€์ผ ๊ณ„์‚ฐ const selectionBoxStyle = useMemo(() => { if (!selectionBox) return null; const minX = Math.min(selectionBox.startX, selectionBox.endX); const maxX = Math.max(selectionBox.startX, selectionBox.endX); const minY = Math.min(selectionBox.startY, selectionBox.endY); const maxY = Math.max(selectionBox.startY, selectionBox.endY); return { left: `${minX}px`, top: `${minY}px`, width: `${maxX - minX}px`, height: `${maxY - minY}px`, }; }, [selectionBox]); return (
{/* 12๊ฐœ ์ปฌ๋Ÿผ ๋ฉ”์ธ ๊ตฌ๋ถ„์„  - ์ฃผ์„ ์ฒ˜๋ฆฌ (์„œ๋ธŒ๊ทธ๋ฆฌ๋“œ๋งŒ ์‚ฌ์šฉ) */} {/* {columnLines.map((x, i) => (
))} */} {/* ๊ทธ๋ฆฌ๋“œ ๋ฐ•์Šค๋“ค (12px ๊ฐ„๊ฒฉ, ์บ”๋ฒ„์Šค ๋„ˆ๋น„์— ๊ฝ‰ ์ฐจ๊ฒŒ, ๋งˆ์ง€๋ง‰ ํ–‰ ์ œ์™ธ) */} {verticalGuidelines.map((x, xIdx) => horizontalGuidelines.slice(0, -1).map((y, yIdx) => (
)), )} {/* ๋ฐฐ์น˜๋œ ์š”์†Œ๋“ค ๋ Œ๋”๋ง */} {elements.length === 0 && (
์ƒ๋‹จ ๋ฉ”๋‰ด์—์„œ ์ฐจํŠธ๋‚˜ ์œ„์ ฏ์„ ์„ ํƒํ•˜์„ธ์š”
)} {elements.map((element) => ( { // ๐Ÿ”ฅ ์—ฌ๋Ÿฌ ์š”์†Œ ๋™์‹œ ์—…๋ฐ์ดํŠธ (์ถฉ๋Œ ๊ฐ์ง€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ) updates.forEach(({ id, updates: elementUpdates }) => { onUpdateElement(id, elementUpdates); }); }} onMultiDragStart={(draggedId, initialOffsets) => { // ๐Ÿ”ฅ ๋‹ค์ค‘ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ - ์ดˆ๊ธฐ ์˜คํ”„์…‹ ์ €์žฅ setMultiDragOffsets(initialOffsets); setIsDraggingAny(true); }} onMultiDragMove={(draggedElement, tempPosition) => { // ๐Ÿ”ฅ ๋‹ค์ค‘ ๋“œ๋ž˜๊ทธ ์ค‘ - ๋‹ค๋ฅธ ์œ„์ ฏ๋“ค์˜ ์œ„์น˜ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ if (selectedElements.length > 1 && selectedElements.includes(draggedElement.id)) { const newOffsets: Record = {}; selectedElements.forEach((id) => { if (id !== draggedElement.id) { const targetElement = elements.find((el) => el.id === id); if (targetElement) { const relativeX = targetElement.position.x - draggedElement.position.x; const relativeY = targetElement.position.y - draggedElement.position.y; newOffsets[id] = { x: tempPosition.x + relativeX - targetElement.position.x, y: tempPosition.y + relativeY - targetElement.position.y, }; } } }); setMultiDragOffsets(newOffsets); } }} onMultiDragEnd={() => { // ๐Ÿ”ฅ ๋‹ค์ค‘ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ - ์˜คํ”„์…‹ ์ดˆ๊ธฐํ™” setMultiDragOffsets({}); setIsDraggingAny(false); }} onRemove={onRemoveElement} onSelect={onSelectElement} /> ))} {/* ๐Ÿ”ฅ ์„ ํƒ ๋ฐ•์Šค ๋ Œ๋”๋ง */} {selectionBox && selectionBoxStyle && (
)}
); }, ); DashboardCanvas.displayName = "DashboardCanvas";