From 63553e23b1afdb1ff57110dc989227a36358a632 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 22 Oct 2025 10:10:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EC=9C=84=EC=A0=AF=20=EC=84=A0=ED=83=9D=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 109 +++++++- .../admin/dashboard/DashboardCanvas.tsx | 252 +++++++++++++++++- .../admin/dashboard/DashboardDesigner.tsx | 12 +- 3 files changed, 363 insertions(+), 10 deletions(-) diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index a4b0fc6f..838a96f8 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -129,10 +129,17 @@ const CustomStatsWidget = dynamic(() => import("@/components/dashboard/widgets/C interface CanvasElementProps { element: DashboardElement; isSelected: boolean; + selectedElements?: string[]; // πŸ”₯ 닀쀑 μ„ νƒλœ μš”μ†Œ ID λ°°μ—΄ + allElements?: DashboardElement[]; // πŸ”₯ λͺ¨λ“  μš”μ†Œ λ°°μ—΄ + multiDragOffset?: { x: number; y: number }; // πŸ”₯ 닀쀑 λ“œλž˜κ·Έ μ‹œ 이 μš”μ†Œμ˜ μ˜€ν”„μ…‹ cellSize: number; subGridSize: number; canvasWidth?: number; onUpdate: (id: string, updates: Partial) => void; + onUpdateMultiple?: (updates: { id: string; updates: Partial }[]) => void; // πŸ”₯ 닀쀑 μ—…λ°μ΄νŠΈ + onMultiDragStart?: (draggedId: string, otherOffsets: Record) => void; // πŸ”₯ 닀쀑 λ“œλž˜κ·Έ μ‹œμž‘ + onMultiDragMove?: (draggedElement: DashboardElement, tempPosition: { x: number; y: number }) => void; // πŸ”₯ 닀쀑 λ“œλž˜κ·Έ 쀑 + onMultiDragEnd?: () => void; // πŸ”₯ 닀쀑 λ“œλž˜κ·Έ μ’…λ£Œ onRemove: (id: string) => void; onSelect: (id: string | null) => void; onConfigure?: (element: DashboardElement) => void; @@ -147,10 +154,17 @@ interface CanvasElementProps { export function CanvasElement({ element, isSelected, + selectedElements = [], + allElements = [], + multiDragOffset, cellSize, subGridSize, canvasWidth = 1560, onUpdate, + onUpdateMultiple, + onMultiDragStart, + onMultiDragMove, + onMultiDragEnd, onRemove, onSelect, onConfigure, @@ -205,9 +219,27 @@ export function CanvasElement({ elementX: element.position.x, elementY: element.position.y, }); + + // πŸ”₯ 닀쀑 μ„ νƒλœ 경우, λ‹€λ₯Έ μœ„μ ―λ“€μ˜ μ˜€ν”„μ…‹ 계산 + if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragStart) { + const offsets: Record = {}; + selectedElements.forEach((id) => { + if (id !== element.id) { + const targetElement = allElements.find((el) => el.id === id); + if (targetElement) { + offsets[id] = { + x: targetElement.position.x - element.position.x, + y: targetElement.position.y - element.position.y, + }; + } + } + }); + onMultiDragStart(element.id, offsets); + } + e.preventDefault(); }, - [element.id, element.position.x, element.position.y, onSelect, isSelected], + [element.id, element.position.x, element.position.y, onSelect, isSelected, selectedElements, allElements, onMultiDragStart], ); // λ¦¬μ‚¬μ΄μ¦ˆ ν•Έλ“€ λ§ˆμš°μŠ€λ‹€μš΄ @@ -263,6 +295,11 @@ export function CanvasElement({ const snappedY = distToGridY <= magneticThreshold ? nearestGridY : Math.round(rawY / subGridSize) * subGridSize; setTempPosition({ x: snappedX, y: snappedY }); + + // πŸ”₯ 닀쀑 λ“œλž˜κ·Έ 쀑 - λ‹€λ₯Έ μœ„μ ―λ“€μ˜ μœ„μΉ˜ μ—…λ°μ΄νŠΈ + if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragMove) { + onMultiDragMove(element, { x: snappedX, y: snappedY }); + } } else if (isResizing) { const deltaX = e.clientX - resizeStart.x; const deltaY = e.clientY - resizeStart.y; @@ -345,12 +382,14 @@ export function CanvasElement({ isResizing, dragStart, resizeStart, - element.size.width, - element.type, - element.subtype, + element, canvasWidth, cellSize, subGridSize, + selectedElements, + allElements, + onUpdateMultiple, + onMultiDragMove, ], ); @@ -370,7 +409,43 @@ export function CanvasElement({ position: { x: finalX, y: finalY }, }); + // πŸ”₯ 닀쀑 μ„ νƒλœ μš”μ†Œλ“€λ„ ν•¨κ»˜ μ—…λ°μ΄νŠΈ + if (selectedElements.length > 1 && selectedElements.includes(element.id) && onUpdateMultiple) { + const updates = selectedElements + .filter((id) => id !== element.id) // ν˜„μž¬ μš”μ†Œ μ œμ™Έ + .map((id) => { + const targetElement = allElements.find((el) => el.id === id); + if (!targetElement) return null; + + // ν˜„μž¬ μš”μ†Œμ™€μ˜ μƒλŒ€μ  μœ„μΉ˜ μœ μ§€ + const relativeX = targetElement.position.x - dragStart.elementX; + const relativeY = targetElement.position.y - dragStart.elementY; + + return { + id, + updates: { + position: { + x: Math.max(0, Math.min(canvasWidth - targetElement.size.width, finalX + relativeX)), + y: Math.max(0, finalY + relativeY), + z: targetElement.position.z, + }, + }, + }; + }) + .filter((update): update is { id: string; updates: Partial } => update !== null); + + if (updates.length > 0) { + console.log("πŸ”₯ 닀쀑 선택 μš”μ†Œ ν•¨κ»˜ 이동:", updates); + onUpdateMultiple(updates); + } + } + setTempPosition(null); + + // πŸ”₯ 닀쀑 λ“œλž˜κ·Έ μ’…λ£Œ + if (onMultiDragEnd) { + onMultiDragEnd(); + } } if (isResizing && tempPosition && tempSize) { @@ -396,7 +471,23 @@ export function CanvasElement({ setIsDragging(false); setIsResizing(false); - }, [isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, cellSize, canvasWidth]); + }, [ + isDragging, + isResizing, + tempPosition, + tempSize, + element.id, + element.size.width, + onUpdate, + onUpdateMultiple, + onMultiDragEnd, + cellSize, + canvasWidth, + selectedElements, + allElements, + dragStart.elementX, + dragStart.elementY, + ]); // μ „μ—­ 마우슀 이벀트 등둝 React.useEffect(() => { @@ -525,12 +616,18 @@ export function CanvasElement({ }; // λ“œλž˜κ·Έ/λ¦¬μ‚¬μ΄μ¦ˆ 쀑일 λ•ŒλŠ” μž„μ‹œ μœ„μΉ˜/크기 μ‚¬μš©, μ•„λ‹ˆλ©΄ μ‹€μ œ κ°’ μ‚¬μš© - const displayPosition = tempPosition || element.position; + // πŸ”₯ 닀쀑 λ“œλž˜κ·Έ 쀑이면 multiDragOffset 적용 (단, λ“œλž˜κ·Έ 쀑인 μœ„μ ―μ€ tempPosition μš°μ„ ) + const displayPosition = tempPosition || (multiDragOffset && !isDragging ? { + x: element.position.x + multiDragOffset.x, + y: element.position.y + multiDragOffset.y, + z: element.position.z, + } : element.position); const displaySize = tempSize || element.size; return (
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; @@ -31,10 +33,12 @@ export const DashboardCanvas = forwardRef( { elements, selectedElement, + selectedElements = [], onCreateElement, onUpdateElement, onRemoveElement, onSelectElement, + onSelectMultiple, onConfigureElement, backgroundColor = "#f9fafb", canvasWidth = 1560, @@ -43,6 +47,19 @@ export const DashboardCanvas = forwardRef( 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 [multiDragOffsets, setMultiDragOffsets] = useState>({}); // ν˜„μž¬ μΊ”λ²„μŠ€ 크기에 λ§žλŠ” κ·Έλ¦¬λ“œ μ„€μ • 계산 const gridConfig = useMemo(() => calculateGridConfig(canvasWidth), [canvasWidth]); @@ -182,14 +199,174 @@ export const DashboardCanvas = forwardRef( [ref, onCreateElement, canvasWidth, cellSize], ); + // πŸ”₯ 선택 λ°•μŠ€ λ“œλž˜κ·Έ μ‹œμž‘ + 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); + + 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]); + // μΊ”λ²„μŠ€ 클릭 μ‹œ 선택 ν•΄μ œ const handleCanvasClick = useCallback( (e: React.MouseEvent) => { + // πŸ”₯ 방금 μ„ νƒν–ˆμœΌλ©΄ 클릭 이벀트 λ¬΄μ‹œ (선택 ν•΄μ œ λ°©μ§€) + if (justSelected) { + console.log("🚫 방금 μ„ νƒν–ˆμœΌλ―€λ‘œ 클릭 이벀트 λ¬΄μ‹œ"); + return; + } + if (e.target === e.currentTarget) { + console.log("βœ… 빈 곡간 클릭 - 선택 ν•΄μ œ"); onSelectElement(null); + if (onSelectMultiple) { + onSelectMultiple([]); + } } }, - [onSelectElement], + [onSelectElement, onSelectMultiple, justSelected], ); // 동적 κ·Έλ¦¬λ“œ 크기 계산 @@ -202,6 +379,23 @@ export const DashboardCanvas = forwardRef( // 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 (
( backgroundSize: `${subGridSize}px ${subGridSize}px`, backgroundPosition: "0 0", backgroundRepeat: "repeat", + cursor: isSelecting ? "crosshair" : "default", }} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} onClick={handleCanvasClick} + onMouseDown={handleMouseDown} > {/* 12개 컬럼 메인 ꡬ뢄선 */} {columnLines.map((x, i) => ( @@ -249,16 +445,66 @@ export const DashboardCanvas = forwardRef( { + // πŸ”₯ μ—¬λŸ¬ μš”μ†Œ λ™μ‹œ μ—…λ°μ΄νŠΈ (좩돌 감지 κ±΄λ„ˆλ›°κΈ°) + updates.forEach(({ id, updates: elementUpdates }) => { + onUpdateElement(id, elementUpdates); + }); + }} + onMultiDragStart={(draggedId, initialOffsets) => { + // πŸ”₯ 닀쀑 λ“œλž˜κ·Έ μ‹œμž‘ - 초기 μ˜€ν”„μ…‹ μ €μž₯ + setMultiDragOffsets(initialOffsets); + }} + 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({}); + }} onRemove={onRemoveElement} onSelect={onSelectElement} onConfigure={onConfigureElement} /> ))} + + {/* πŸ”₯ 선택 λ°•μŠ€ λ Œλ”λ§ */} + {selectionBox && selectionBoxStyle && ( +
+ )}
); }, diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index c0d08083..4f504c1f 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -43,6 +43,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D const { refreshMenus } = useMenu(); const [elements, setElements] = useState([]); const [selectedElement, setSelectedElement] = useState(null); + const [selectedElements, setSelectedElements] = useState([]); // πŸ”₯ 닀쀑 선택 const [elementCounter, setElementCounter] = useState(0); const [configModalElement, setConfigModalElement] = useState(null); const [dashboardId, setDashboardId] = useState(initialDashboardId || null); @@ -504,10 +505,19 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D ref={canvasRef} elements={elements} selectedElement={selectedElement} + selectedElements={selectedElements} onCreateElement={createElement} onUpdateElement={updateElement} onRemoveElement={removeElement} - onSelectElement={setSelectedElement} + onSelectElement={(id) => { + setSelectedElement(id); + setSelectedElements([]); // 단일 선택 μ‹œ 닀쀑 선택 ν•΄μ œ + }} + onSelectMultiple={(ids) => { + console.log("🎯 DashboardDesigner - onSelectMultiple 호좜:", ids); + setSelectedElements(ids); + setSelectedElement(null); // 닀쀑 선택 μ‹œ 단일 선택 ν•΄μ œ + }} onConfigureElement={openConfigModal} backgroundColor={canvasBackgroundColor} canvasWidth={canvasConfig.width} From 01f92d6132fc9bf830610b384e0261ddacafe4da Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 22 Oct 2025 10:13:59 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 50 +++---------------- .../admin/dashboard/DashboardCanvas.tsx | 6 +-- 2 files changed, 11 insertions(+), 45 deletions(-) diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 838a96f8..7bd4165e 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -280,19 +280,9 @@ export function CanvasElement({ const maxX = canvasWidth - element.size.width; rawX = Math.min(rawX, maxX); - // λ“œλž˜κ·Έ 쀑 μ‹€μ‹œκ°„ μŠ€λƒ… (λ§ˆκ·Έλ„€ν‹± μŠ€λƒ…) - const gridSize = cellSize + 5; // GAP ν¬ν•¨ν•œ μ‹€μ œ κ·Έλ¦¬λ“œ 크기 - const magneticThreshold = 15; // 큰 κ·Έλ¦¬λ“œμ— λŒλ¦¬λŠ” 거리 (px) - - // X μ’Œν‘œ μŠ€λƒ… (큰 κ·Έλ¦¬λ“œ μš°μ„ , μ—†μœΌλ©΄ μ„œλΈŒκ·Έλ¦¬λ“œ) - const nearestGridX = Math.round(rawX / gridSize) * gridSize; - const distToGridX = Math.abs(rawX - nearestGridX); - const snappedX = distToGridX <= magneticThreshold ? nearestGridX : Math.round(rawX / subGridSize) * subGridSize; - - // Y μ’Œν‘œ μŠ€λƒ… (큰 κ·Έλ¦¬λ“œ μš°μ„ , μ—†μœΌλ©΄ μ„œλΈŒκ·Έλ¦¬λ“œ) - const nearestGridY = Math.round(rawY / gridSize) * gridSize; - const distToGridY = Math.abs(rawY - nearestGridY); - const snappedY = distToGridY <= magneticThreshold ? nearestGridY : Math.round(rawY / subGridSize) * subGridSize; + // λ“œλž˜κ·Έ 쀑 μ‹€μ‹œκ°„ μŠ€λƒ… (μ„œλΈŒκ·Έλ¦¬λ“œλ§Œ μ‚¬μš©) + const snappedX = Math.round(rawX / subGridSize) * subGridSize; + const snappedY = Math.round(rawY / subGridSize) * subGridSize; setTempPosition({ x: snappedX, y: snappedY }); @@ -342,35 +332,11 @@ export function CanvasElement({ const maxWidth = canvasWidth - newX; newWidth = Math.min(newWidth, maxWidth); - // λ¦¬μ‚¬μ΄μ¦ˆ 쀑 μ‹€μ‹œκ°„ μŠ€λƒ… (λ§ˆκ·Έλ„€ν‹± μŠ€λƒ…) - const gridSize = cellSize + 5; // GAP ν¬ν•¨ν•œ μ‹€μ œ κ·Έλ¦¬λ“œ 크기 - const magneticThreshold = 15; - - // μœ„μΉ˜ μŠ€λƒ… - const nearestGridX = Math.round(newX / gridSize) * gridSize; - const distToGridX = Math.abs(newX - nearestGridX); - const snappedX = distToGridX <= magneticThreshold ? nearestGridX : Math.round(newX / subGridSize) * subGridSize; - - const nearestGridY = Math.round(newY / gridSize) * gridSize; - const distToGridY = Math.abs(newY - nearestGridY); - const snappedY = distToGridY <= magneticThreshold ? nearestGridY : Math.round(newY / subGridSize) * subGridSize; - - // 크기 μŠ€λƒ… (κ·Έλ¦¬λ“œ μΉΈ λ‹¨μœ„λ‘œ μŠ€λƒ…ν•˜λ˜, λ§ˆμ§€λ§‰ GAP은 μ œμ™Έ) - // 예: 1μΉΈ = cellSize, 2μΉΈ = cellSize*2 + GAP, 3μΉΈ = cellSize*3 + GAP*2 - const calculateGridWidth = (cells: number) => cells * cellSize + Math.max(0, cells - 1) * 5; - - // κ°€μž₯ κ°€κΉŒμš΄ κ·Έλ¦¬λ“œ μΉΈ 수 계산 - const nearestWidthCells = Math.round(newWidth / gridSize); - const nearestGridWidth = calculateGridWidth(nearestWidthCells); - const distToGridWidth = Math.abs(newWidth - nearestGridWidth); - const snappedWidth = - distToGridWidth <= magneticThreshold ? nearestGridWidth : Math.round(newWidth / subGridSize) * subGridSize; - - const nearestHeightCells = Math.round(newHeight / gridSize); - const nearestGridHeight = calculateGridWidth(nearestHeightCells); - const distToGridHeight = Math.abs(newHeight - nearestGridHeight); - const snappedHeight = - distToGridHeight <= magneticThreshold ? nearestGridHeight : Math.round(newHeight / subGridSize) * subGridSize; + // λ¦¬μ‚¬μ΄μ¦ˆ 쀑 μ‹€μ‹œκ°„ μŠ€λƒ… (μ„œλΈŒκ·Έλ¦¬λ“œλ§Œ μ‚¬μš©) + const snappedX = Math.round(newX / subGridSize) * subGridSize; + const snappedY = Math.round(newY / subGridSize) * subGridSize; + const snappedWidth = Math.round(newWidth / subGridSize) * subGridSize; + const snappedHeight = Math.round(newHeight / subGridSize) * subGridSize; // μž„μ‹œ 크기/μœ„μΉ˜ μ €μž₯ (μŠ€λƒ…λ¨) setTempPosition({ x: Math.max(0, snappedX), y: Math.max(0, snappedY) }); diff --git a/frontend/components/admin/dashboard/DashboardCanvas.tsx b/frontend/components/admin/dashboard/DashboardCanvas.tsx index 073ddb37..f586df57 100644 --- a/frontend/components/admin/dashboard/DashboardCanvas.tsx +++ b/frontend/components/admin/dashboard/DashboardCanvas.tsx @@ -420,8 +420,8 @@ export const DashboardCanvas = forwardRef( onClick={handleCanvasClick} onMouseDown={handleMouseDown} > - {/* 12개 컬럼 메인 ꡬ뢄선 */} - {columnLines.map((x, i) => ( + {/* 12개 컬럼 메인 ꡬ뢄선 - 주석 처리 (μ„œλΈŒκ·Έλ¦¬λ“œλ§Œ μ‚¬μš©) */} + {/* {columnLines.map((x, i) => (
( zIndex: 0, }} /> - ))} + ))} */} {/* 배치된 μš”μ†Œλ“€ λ Œλ”λ§ */} {elements.length === 0 && (