달력과 투두리스트 합침, 배경색변경가능, 위젯끼리 밀어내는 기능과 세밀한 그리드 추가, 범용위젯 복구
This commit is contained in:
162
frontend/components/admin/dashboard/collisionUtils.ts
Normal file
162
frontend/components/admin/dashboard/collisionUtils.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 대시보드 위젯 충돌 감지 및 자동 재배치 유틸리티
|
||||
*/
|
||||
|
||||
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)),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user