170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
/**
|
||
* 대시보드 그리드 시스템 유틸리티
|
||
* - 12 컬럼 그리드 시스템
|
||
* - 정사각형 셀 (가로 = 세로)
|
||
* - 스냅 기능
|
||
*/
|
||
|
||
// 그리드 설정 (고정 크기)
|
||
export const GRID_CONFIG = {
|
||
COLUMNS: 12,
|
||
CELL_SIZE: 132, // 고정 셀 크기
|
||
GAP: 8, // 셀 간격
|
||
SNAP_THRESHOLD: 15, // 스냅 임계값 (px)
|
||
ELEMENT_PADDING: 4, // 요소 주위 여백 (px)
|
||
CANVAS_WIDTH: 1682, // 고정 캔버스 너비 (실제 측정값)
|
||
// 계산식: (132 + 8) × 12 - 8 = 1672px (그리드)
|
||
// 추가 여백 10px 포함 = 1682px
|
||
} as const;
|
||
|
||
/**
|
||
* 실제 그리드 셀 크기 계산 (gap 포함)
|
||
*/
|
||
export const getCellWithGap = () => {
|
||
return GRID_CONFIG.CELL_SIZE + GRID_CONFIG.GAP;
|
||
};
|
||
|
||
/**
|
||
* 전체 캔버스 너비 계산
|
||
*/
|
||
export const getCanvasWidth = () => {
|
||
const cellWithGap = getCellWithGap();
|
||
return GRID_CONFIG.COLUMNS * cellWithGap - GRID_CONFIG.GAP;
|
||
};
|
||
|
||
/**
|
||
* 좌표를 가장 가까운 그리드 포인트로 스냅 (여백 포함)
|
||
* @param value - 스냅할 좌표값
|
||
* @param cellSize - 셀 크기 (선택사항, 기본값은 GRID_CONFIG.CELL_SIZE)
|
||
* @returns 스냅된 좌표값 (여백 포함)
|
||
*/
|
||
export const snapToGrid = (value: number, cellSize: number = GRID_CONFIG.CELL_SIZE): number => {
|
||
const cellWithGap = cellSize + GRID_CONFIG.GAP;
|
||
const gridIndex = Math.round(value / cellWithGap);
|
||
return gridIndex * cellWithGap + GRID_CONFIG.ELEMENT_PADDING;
|
||
};
|
||
|
||
/**
|
||
* 좌표를 그리드에 스냅 (임계값 적용)
|
||
* @param value - 현재 좌표값
|
||
* @param cellSize - 셀 크기 (선택사항)
|
||
* @returns 스냅된 좌표값 (임계값 내에 있으면 스냅, 아니면 원래 값)
|
||
*/
|
||
export const snapToGridWithThreshold = (value: number, cellSize: number = GRID_CONFIG.CELL_SIZE): number => {
|
||
const snapped = snapToGrid(value, cellSize);
|
||
const distance = Math.abs(value - snapped);
|
||
|
||
return distance <= GRID_CONFIG.SNAP_THRESHOLD ? snapped : value;
|
||
};
|
||
|
||
/**
|
||
* 크기를 그리드 단위로 스냅
|
||
* @param size - 스냅할 크기
|
||
* @param minCells - 최소 셀 개수 (기본값: 2)
|
||
* @param cellSize - 셀 크기 (선택사항)
|
||
* @returns 스냅된 크기
|
||
*/
|
||
export const snapSizeToGrid = (
|
||
size: number,
|
||
minCells: number = 2,
|
||
cellSize: number = GRID_CONFIG.CELL_SIZE,
|
||
): number => {
|
||
const cellWithGap = cellSize + GRID_CONFIG.GAP;
|
||
const cells = Math.max(minCells, Math.round(size / cellWithGap));
|
||
return cells * cellWithGap - GRID_CONFIG.GAP;
|
||
};
|
||
|
||
/**
|
||
* 위치와 크기를 모두 그리드에 스냅
|
||
*/
|
||
export interface GridPosition {
|
||
x: number;
|
||
y: number;
|
||
}
|
||
|
||
export interface GridSize {
|
||
width: number;
|
||
height: number;
|
||
}
|
||
|
||
export interface GridBounds {
|
||
position: GridPosition;
|
||
size: GridSize;
|
||
}
|
||
|
||
/**
|
||
* 요소의 위치와 크기를 그리드에 맞춰 조정
|
||
* @param bounds - 현재 위치와 크기
|
||
* @param canvasWidth - 캔버스 너비 (경계 체크용)
|
||
* @param canvasHeight - 캔버스 높이 (경계 체크용)
|
||
* @returns 그리드에 스냅된 위치와 크기
|
||
*/
|
||
export const snapBoundsToGrid = (bounds: GridBounds, canvasWidth?: number, canvasHeight?: number): GridBounds => {
|
||
// 위치 스냅
|
||
let snappedX = snapToGrid(bounds.position.x);
|
||
let snappedY = snapToGrid(bounds.position.y);
|
||
|
||
// 크기 스냅
|
||
const snappedWidth = snapSizeToGrid(bounds.size.width);
|
||
const snappedHeight = snapSizeToGrid(bounds.size.height);
|
||
|
||
// 캔버스 경계 체크
|
||
if (canvasWidth) {
|
||
snappedX = Math.min(snappedX, canvasWidth - snappedWidth);
|
||
}
|
||
if (canvasHeight) {
|
||
snappedY = Math.min(snappedY, canvasHeight - snappedHeight);
|
||
}
|
||
|
||
// 음수 방지
|
||
snappedX = Math.max(0, snappedX);
|
||
snappedY = Math.max(0, snappedY);
|
||
|
||
return {
|
||
position: { x: snappedX, y: snappedY },
|
||
size: { width: snappedWidth, height: snappedHeight },
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 좌표가 어느 그리드 셀에 속하는지 계산
|
||
* @param value - 좌표값
|
||
* @returns 그리드 인덱스 (0부터 시작)
|
||
*/
|
||
export const getGridIndex = (value: number): number => {
|
||
const cellWithGap = getCellWithGap();
|
||
return Math.floor(value / cellWithGap);
|
||
};
|
||
|
||
/**
|
||
* 그리드 인덱스를 좌표로 변환
|
||
* @param index - 그리드 인덱스
|
||
* @returns 좌표값
|
||
*/
|
||
export const gridIndexToCoordinate = (index: number): number => {
|
||
const cellWithGap = getCellWithGap();
|
||
return index * cellWithGap;
|
||
};
|
||
|
||
/**
|
||
* 스냅 가이드라인 표시용 좌표 계산
|
||
* @param value - 현재 좌표
|
||
* @returns 가장 가까운 그리드 라인들의 좌표 배열
|
||
*/
|
||
export const getNearbyGridLines = (value: number): number[] => {
|
||
const snapped = snapToGrid(value);
|
||
const cellWithGap = getCellWithGap();
|
||
|
||
return [snapped - cellWithGap, snapped, snapped + cellWithGap].filter((line) => line >= 0);
|
||
};
|
||
|
||
/**
|
||
* 위치가 스냅 임계값 내에 있는지 확인
|
||
* @param value - 현재 값
|
||
* @param snapValue - 스냅할 값
|
||
* @returns 임계값 내에 있으면 true
|
||
*/
|
||
export const isWithinSnapThreshold = (value: number, snapValue: number): boolean => {
|
||
return Math.abs(value - snapValue) <= GRID_CONFIG.SNAP_THRESHOLD;
|
||
};
|