그리드 박스 기반 스냅 시스템 구현

This commit is contained in:
dohyeons 2025-10-22 16:58:07 +09:00
parent 7c3a2dff4c
commit 41c763c019
3 changed files with 56 additions and 37 deletions

View File

@ -4,7 +4,7 @@ import React, { useState, useCallback, useRef, useEffect } from "react";
import dynamic from "next/dynamic";
import { DashboardElement, QueryResult, Position } from "./types";
import { ChartRenderer } from "./charts/ChartRenderer";
import { GRID_CONFIG, magneticSnap } from "./gridUtils";
import { GRID_CONFIG, magneticSnap, snapSizeToGrid } from "./gridUtils";
// 위젯 동적 임포트
const WeatherWidget = dynamic(() => import("@/components/dashboard/widgets/WeatherWidget"), {
@ -378,9 +378,9 @@ export function CanvasElement({
const snappedX = magneticSnap(newX, verticalGuidelines);
const snappedY = magneticSnap(newY, horizontalGuidelines);
// 크기는 12px 단위로 스냅
const snappedWidth = Math.round(newWidth / 12) * 12;
const snappedHeight = Math.round(newHeight / 12) * 12;
// 크기는 그리드 박스 단위로 스냅
const snappedWidth = snapSizeToGrid(newWidth, canvasWidth || 1560);
const snappedHeight = snapSizeToGrid(newHeight, canvasWidth || 1560);
// 스냅 후 경계 체크
const finalSnappedX = Math.max(0, Math.min(snappedX, canvasWidth - snappedWidth));

View File

@ -7,7 +7,14 @@ import { DashboardTopMenu } from "./DashboardTopMenu";
import { ElementConfigSidebar } from "./ElementConfigSidebar";
import { DashboardSaveModal } from "./DashboardSaveModal";
import { DashboardElement, ElementType, ElementSubtype } from "./types";
import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize, calculateGridConfig } from "./gridUtils";
import {
GRID_CONFIG,
snapToGrid,
snapSizeToGrid,
calculateCellSize,
calculateGridConfig,
calculateBoxSize,
} from "./gridUtils";
import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector";
import { DashboardProvider } from "@/contexts/DashboardContext";
import { useMenu } from "@/contexts/MenuContext";
@ -89,8 +96,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
// 그리드에 스냅 (X, Y, 너비, 높이 모두)
const snappedX = snapToGrid(scaledX, newCellSize);
const snappedY = snapToGrid(el.position.y, newCellSize);
const snappedWidth = snapSizeToGrid(scaledWidth, 2, newCellSize);
const snappedHeight = snapSizeToGrid(el.size.height, 2, newCellSize);
const snappedWidth = snapSizeToGrid(scaledWidth, newConfig.width);
const snappedHeight = snapSizeToGrid(el.size.height, newConfig.width);
return {
...el,
@ -215,22 +222,25 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
return;
}
// 기본 크기 설정 (서브그리드 기준)
const gridConfig = calculateGridConfig(canvasConfig.width);
const subGridSize = gridConfig.SUB_GRID_SIZE;
// 기본 크기 설정 (그리드 박스 단위)
const boxSize = calculateBoxSize(canvasConfig.width);
// 서브그리드 기준 기본 크기 (픽셀)
let defaultWidth = subGridSize * 10; // 기본 위젯: 서브그리드 10칸
let defaultHeight = subGridSize * 10; // 기본 위젯: 서브그리드 10칸
// 그리드 박스 단위 기본 크기
let boxesWidth = 3; // 기본 위젯: 박스 3개
let boxesHeight = 3; // 기본 위젯: 박스 3개
if (type === "chart") {
defaultWidth = subGridSize * 20; // 차트: 서브그리드 20칸
defaultHeight = subGridSize * 15; // 차트: 서브그리드 15칸
boxesWidth = 4; // 차트: 박스 4개
boxesHeight = 3; // 차트: 박스 3개
} else if (type === "widget" && subtype === "calendar") {
defaultWidth = subGridSize * 10; // 달력: 서브그리드 10칸
defaultHeight = subGridSize * 15; // 달력: 서브그리드 15칸
boxesWidth = 3; // 달력: 박스 3개
boxesHeight = 4; // 달력: 박스 4개
}
// 박스 개수를 픽셀로 변환 (마지막 간격 제거)
const defaultWidth = boxesWidth * boxSize + (boxesWidth - 1) * GRID_CONFIG.GRID_BOX_GAP;
const defaultHeight = boxesHeight * boxSize + (boxesHeight - 1) * GRID_CONFIG.GRID_BOX_GAP;
// 크기 유효성 검사
if (isNaN(defaultWidth) || isNaN(defaultHeight) || defaultWidth <= 0 || defaultHeight <= 0) {
// console.error("Invalid size calculated:", {

View File

@ -54,9 +54,11 @@ export function calculateGridConfig(canvasWidth: number) {
/**
* (gap )
* @param canvasWidth -
*/
export const getCellWithGap = () => {
return GRID_CONFIG.CELL_SIZE + GRID_CONFIG.GAP;
export const getCellWithGap = (canvasWidth: number = 1560) => {
const boxSize = calculateBoxSize(canvasWidth);
return boxSize + GRID_CONFIG.GRID_BOX_GAP;
};
/**
@ -70,14 +72,14 @@ export const getCanvasWidth = () => {
/**
* ( )
* @param value -
* @param subGridSize - (, 기본값: cellSize/3 43px)
* @param subGridSize - ()
* @returns
*/
export const snapToGrid = (value: number, subGridSize?: number): number => {
// 서브 그리드 크기가 지정되지 않으면 기본 그리드 크기의 1/3 사용 (3x3 서브그리드)
const snapSize = subGridSize ?? Math.floor(GRID_CONFIG.CELL_SIZE / 3);
// 서브 그리드 크기가 지정되지 않으면 기본 박스 크기 사용
const snapSize = subGridSize ?? calculateBoxSize(1560);
// 서브 그리드 단위로 스냅
// 그리드 단위로 스냅
const gridIndex = Math.round(value / snapSize);
return gridIndex * snapSize;
};
@ -88,8 +90,9 @@ export const snapToGrid = (value: number, subGridSize?: number): number => {
* @param cellSize - ()
* @returns ( , )
*/
export const snapToGridWithThreshold = (value: number, cellSize: number = GRID_CONFIG.CELL_SIZE): number => {
const snapped = snapToGrid(value, cellSize);
export const snapToGridWithThreshold = (value: number, cellSize?: number): number => {
const snapSize = cellSize ?? calculateBoxSize(1560);
const snapped = snapToGrid(value, snapSize);
const distance = Math.abs(value - snapped);
return distance <= GRID_CONFIG.SNAP_THRESHOLD ? snapped : value;
@ -102,15 +105,7 @@ export const snapToGridWithThreshold = (value: number, cellSize: number = GRID_C
* @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;
};
// 기존 snapSizeToGrid 제거 - 새 버전은 269번 줄에 있음
/**
*
@ -142,9 +137,10 @@ export const snapBoundsToGrid = (bounds: GridBounds, canvasWidth?: number, canva
let snappedX = snapToGrid(bounds.position.x);
let snappedY = snapToGrid(bounds.position.y);
// 크기 스냅
const snappedWidth = snapSizeToGrid(bounds.size.width);
const snappedHeight = snapSizeToGrid(bounds.size.height);
// 크기 스냅 (canvasWidth 기본값 1560)
const width = canvasWidth || 1560;
const snappedWidth = snapSizeToGrid(bounds.size.width, width);
const snappedHeight = snapSizeToGrid(bounds.size.height, width);
// 캔버스 경계 체크
if (canvasWidth) {
@ -264,3 +260,16 @@ export function magneticSnap(value: number, guidelines: number[]): number {
const { nearest } = findNearestGuideline(value, guidelines);
return nearest; // 거리 체크 없이 무조건 스냅
}
// 크기를 그리드 박스 단위로 스냅 (박스 크기의 배수로만 가능)
export function snapSizeToGrid(size: number, canvasWidth: number): number {
const boxSize = calculateBoxSize(canvasWidth);
const cellSize = boxSize + GRID_CONFIG.GRID_BOX_GAP; // 박스 + 간격
// 최소 1개 박스 크기
const minBoxes = 1;
const boxes = Math.max(minBoxes, Math.round(size / cellSize));
// 박스 개수에서 마지막 간격 제거
return boxes * boxSize + (boxes - 1) * GRID_CONFIG.GRID_BOX_GAP;
}