/** * 대시보드 위젯 충돌 감지 및 자동 재배치 유틸리티 */ import { DashboardElement } from "./types"; export interface Rectangle { x: number; y: number; width: number; height: number; } /** * 두 사각형이 겹치는지 확인 (여유있는 충돌 감지) * @param rect1 첫 번째 사각형 * @param rect2 두 번째 사각형 * @param cellSize 한 그리드 칸의 크기 (기본: 130px) */ export function isColliding(rect1: Rectangle, rect2: Rectangle, cellSize: number = 130): boolean { // 겹친 영역 계산 const overlapX = Math.max( 0, Math.min(rect1.x + rect1.width, rect2.x + rect2.width) - Math.max(rect1.x, rect2.x) ); const overlapY = Math.max( 0, Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - Math.max(rect1.y, rect2.y) ); // 큰 그리드의 절반(cellSize/2 ≈ 65px) 이상 겹쳐야 충돌로 간주 const collisionThreshold = Math.floor(cellSize / 2); return overlapX >= collisionThreshold && overlapY >= collisionThreshold; } /** * 특정 위젯과 충돌하는 다른 위젯들을 찾기 */ export function findCollisions( element: DashboardElement, allElements: DashboardElement[], cellSize: number = 130, excludeId?: string ): DashboardElement[] { const elementRect: Rectangle = { x: element.position.x, y: element.position.y, width: element.size.width, height: element.size.height, }; return allElements.filter((other) => { if (other.id === element.id || other.id === excludeId) { return false; } const otherRect: Rectangle = { x: other.position.x, y: other.position.y, width: other.size.width, height: other.size.height, }; return isColliding(elementRect, otherRect, cellSize); }); } /** * 충돌을 해결하기 위해 위젯을 아래로 이동 */ export function resolveCollisionVertically( movingElement: DashboardElement, collidingElement: DashboardElement, gridSize: number = 10 ): { x: number; y: number } { // 충돌하는 위젯 아래로 이동 const newY = collidingElement.position.y + collidingElement.size.height + gridSize; return { x: collidingElement.position.x, y: Math.round(newY / gridSize) * gridSize, // 그리드에 스냅 }; } /** * 여러 위젯의 충돌을 재귀적으로 해결 */ export function resolveAllCollisions( elements: DashboardElement[], movedElementId: string, subGridSize: number = 10, canvasWidth: number = 1560, cellSize: number = 130, maxIterations: number = 50 ): DashboardElement[] { let result = [...elements]; let iterations = 0; // 이동한 위젯부터 시작 const movedIndex = result.findIndex((el) => el.id === movedElementId); if (movedIndex === -1) return result; // Y 좌표로 정렬 (위에서 아래로 처리) const sortedIndices = result .map((el, idx) => ({ el, idx })) .sort((a, b) => a.el.position.y - b.el.position.y) .map((item) => item.idx); while (iterations < maxIterations) { let hasCollision = false; for (const idx of sortedIndices) { const element = result[idx]; const collisions = findCollisions(element, result, cellSize); if (collisions.length > 0) { hasCollision = true; // 첫 번째 충돌만 처리 (가장 위에 있는 것) const collision = collisions.sort((a, b) => a.position.y - b.position.y)[0]; // 충돌하는 위젯을 아래로 이동 const collisionIdx = result.findIndex((el) => el.id === collision.id); if (collisionIdx !== -1) { const newY = element.position.y + element.size.height + subGridSize; result[collisionIdx] = { ...result[collisionIdx], position: { ...result[collisionIdx].position, y: Math.round(newY / subGridSize) * subGridSize, }, }; } } } if (!hasCollision) break; iterations++; } return result; } /** * 위젯이 캔버스 경계를 벗어나지 않도록 제한 */ export function constrainToCanvas( element: DashboardElement, canvasWidth: number, canvasHeight: number, gridSize: number = 10 ): { x: number; y: number } { const maxX = canvasWidth - element.size.width; const maxY = canvasHeight - element.size.height; return { x: Math.max(0, Math.min(Math.round(element.position.x / gridSize) * gridSize, maxX)), y: Math.max(0, Math.min(Math.round(element.position.y / gridSize) * gridSize, maxY)), }; }