ERP-node/frontend/lib/utils/gridUtils.ts

390 lines
11 KiB
TypeScript
Raw Normal View History

2025-10-13 18:37:18 +09:00
import { Position, Size } from "@/types/screen";
import { GridSettings } from "@/types/screen-management";
2025-09-02 11:16:40 +09:00
2025-10-13 18:37:18 +09:00
export interface GridInfo {
columnWidth: number;
totalWidth: number;
totalHeight: number;
2025-09-02 11:16:40 +09:00
}
/**
2025-10-13 18:37:18 +09:00
*
2025-09-02 11:16:40 +09:00
*/
2025-10-13 18:37:18 +09:00
export function calculateGridInfo(
containerWidth: number,
containerHeight: number,
gridSettings: GridSettings,
): GridInfo {
const { columns, gap, padding } = gridSettings;
// 사용 가능한 너비 계산 (패딩 제외)
const availableWidth = containerWidth - padding * 2;
// 격자 간격을 고려한 컬럼 너비 계산
const totalGaps = (columns - 1) * gap;
const columnWidth = (availableWidth - totalGaps) / columns;
return {
columnWidth: Math.max(columnWidth, 20), // 최소 20px로 줄여서 더 많은 컬럼 표시
totalWidth: containerWidth,
totalHeight: containerHeight,
};
2025-09-02 11:16:40 +09:00
}
/**
2025-10-13 18:37:18 +09:00
*
2025-09-02 11:16:40 +09:00
*/
2025-10-13 18:37:18 +09:00
export function snapToGrid(position: Position, gridInfo: GridInfo, gridSettings: GridSettings): Position {
if (!gridSettings.snapToGrid) {
return position;
2025-09-02 11:16:40 +09:00
}
2025-10-13 18:37:18 +09:00
const { columnWidth } = gridInfo;
const { gap, padding } = gridSettings;
// 격자 셀 크기 (컬럼 너비 + 간격을 하나의 격자 단위로 계산)
const cellWidth = columnWidth + gap;
const cellHeight = Math.max(40, gap * 2); // 행 높이를 더 크게 설정
// 패딩을 제외한 상대 위치
const relativeX = position.x - padding;
const relativeY = position.y - padding;
// 격자 기준으로 위치 계산 (가장 가까운 격자점으로 스냅)
const gridX = Math.round(relativeX / cellWidth);
const gridY = Math.round(relativeY / cellHeight);
// 실제 픽셀 위치로 변환
const snappedX = Math.max(padding, padding + gridX * cellWidth);
const snappedY = Math.max(padding, padding + gridY * cellHeight);
2025-09-02 11:16:40 +09:00
return {
2025-10-13 18:37:18 +09:00
x: snappedX,
y: snappedY,
z: position.z,
2025-09-02 11:16:40 +09:00
};
}
/**
2025-10-13 18:37:18 +09:00
*
2025-09-02 11:16:40 +09:00
*/
2025-10-13 18:37:18 +09:00
export function snapSizeToGrid(size: Size, gridInfo: GridInfo, gridSettings: GridSettings): Size {
if (!gridSettings.snapToGrid) {
return size;
}
2025-09-02 11:16:40 +09:00
2025-10-13 18:37:18 +09:00
const { columnWidth } = gridInfo;
const { gap } = gridSettings;
2025-09-02 16:18:38 +09:00
2025-10-13 18:37:18 +09:00
// 격자 단위로 너비 계산
// 컴포넌트가 차지하는 컬럼 수를 올바르게 계산
let gridColumns = 1;
2025-10-13 18:37:18 +09:00
// 현재 너비에서 가장 가까운 격자 컬럼 수 찾기
for (let cols = 1; cols <= gridSettings.columns; cols++) {
const targetWidth = cols * columnWidth + (cols - 1) * gap;
if (size.width <= targetWidth + (columnWidth + gap) / 2) {
gridColumns = cols;
break;
2025-09-02 16:18:38 +09:00
}
2025-10-13 18:37:18 +09:00
gridColumns = cols;
2025-09-02 16:18:38 +09:00
}
2025-10-13 18:37:18 +09:00
const snappedWidth = gridColumns * columnWidth + (gridColumns - 1) * gap;
// 높이는 동적 행 높이 단위로 스냅
const rowHeight = Math.max(20, gap);
const snappedHeight = Math.max(40, Math.round(size.height / rowHeight) * rowHeight);
console.log(
`📏 크기 스냅: ${size.width}px → ${snappedWidth}px (${gridColumns}컬럼, 컬럼너비:${columnWidth}px, 간격:${gap}px)`,
);
return {
width: Math.max(columnWidth, snappedWidth),
height: snappedHeight,
};
}
/**
*
*/
export function calculateWidthFromColumns(columns: number, gridInfo: GridInfo, gridSettings: GridSettings): number {
const { columnWidth } = gridInfo;
const { gap } = gridSettings;
return columns * columnWidth + (columns - 1) * gap;
2025-09-02 11:16:40 +09:00
}
2025-09-03 11:32:09 +09:00
/**
2025-10-13 18:37:18 +09:00
* gridColumns
2025-09-03 11:32:09 +09:00
*/
2025-10-13 18:37:18 +09:00
export function updateSizeFromGridColumns(
component: { gridColumns?: number; size: Size },
gridInfo: GridInfo,
gridSettings: GridSettings,
): Size {
if (!component.gridColumns || component.gridColumns < 1) {
return component.size;
}
const newWidth = calculateWidthFromColumns(component.gridColumns, gridInfo, gridSettings);
2025-09-03 11:32:09 +09:00
return {
2025-10-13 18:37:18 +09:00
width: newWidth,
height: component.size.height, // 높이는 유지
2025-09-03 11:32:09 +09:00
};
}
/**
2025-10-13 18:37:18 +09:00
* gridColumns를
*/
export function adjustGridColumnsFromSize(
component: { size: Size },
gridInfo: GridInfo,
gridSettings: GridSettings,
): number {
const columns = calculateColumnsFromWidth(component.size.width, gridInfo, gridSettings);
return Math.min(Math.max(1, columns), gridSettings.columns); // 1-12 범위로 제한
}
/**
*
*/
export function calculateColumnsFromWidth(width: number, gridInfo: GridInfo, gridSettings: GridSettings): number {
const { columnWidth } = gridInfo;
const { gap } = gridSettings;
return Math.max(1, Math.round((width + gap) / (columnWidth + gap)));
}
/**
*
2025-09-02 11:16:40 +09:00
*/
2025-10-13 18:37:18 +09:00
export function generateGridLines(
containerWidth: number,
containerHeight: number,
gridSettings: GridSettings,
): {
verticalLines: number[];
horizontalLines: number[];
} {
const { columns, gap, padding } = gridSettings;
const gridInfo = calculateGridInfo(containerWidth, containerHeight, gridSettings);
const { columnWidth } = gridInfo;
// 격자 셀 크기 (스냅 로직과 동일하게)
const cellWidth = columnWidth + gap;
const cellHeight = Math.max(40, gap * 2);
// 세로 격자선
const verticalLines: number[] = [];
for (let i = 0; i <= columns; i++) {
const x = padding + i * cellWidth;
if (x <= containerWidth) {
verticalLines.push(x);
}
}
// 가로 격자선
const horizontalLines: number[] = [];
for (let y = padding; y < containerHeight; y += cellHeight) {
horizontalLines.push(y);
}
2025-09-02 11:16:40 +09:00
return {
2025-10-13 18:37:18 +09:00
verticalLines,
horizontalLines,
2025-09-02 11:16:40 +09:00
};
}
/**
2025-10-13 18:37:18 +09:00
*
2025-09-02 11:16:40 +09:00
*/
2025-10-13 18:37:18 +09:00
export function isOnGridBoundary(
position: Position,
size: Size,
gridInfo: GridInfo,
gridSettings: GridSettings,
tolerance: number = 5,
2025-09-02 11:16:40 +09:00
): boolean {
2025-10-13 18:37:18 +09:00
const snappedPos = snapToGrid(position, gridInfo, gridSettings);
const snappedSize = snapSizeToGrid(size, gridInfo, gridSettings);
const positionMatch =
Math.abs(position.x - snappedPos.x) <= tolerance && Math.abs(position.y - snappedPos.y) <= tolerance;
const sizeMatch =
Math.abs(size.width - snappedSize.width) <= tolerance && Math.abs(size.height - snappedSize.height) <= tolerance;
return positionMatch && sizeMatch;
2025-09-03 11:32:09 +09:00
}
/**
2025-10-13 18:37:18 +09:00
*
2025-09-03 11:32:09 +09:00
*/
2025-10-13 18:37:18 +09:00
export function alignGroupChildrenToGrid(
children: any[],
groupPosition: Position,
gridInfo: GridInfo,
gridSettings: GridSettings,
): any[] {
if (!gridSettings.snapToGrid || children.length === 0) return children;
2025-09-03 11:32:09 +09:00
2025-10-13 18:37:18 +09:00
console.log("🔧 alignGroupChildrenToGrid 시작:", {
childrenCount: children.length,
groupPosition,
gridInfo,
gridSettings,
});
return children.map((child, index) => {
console.log(`📐 자식 ${index + 1} 처리 중:`, {
childId: child.id,
originalPosition: child.position,
originalSize: child.size,
});
const { columnWidth } = gridInfo;
const { gap } = gridSettings;
// 그룹 내부 패딩 고려한 격자 정렬
const padding = 16;
const effectiveX = child.position.x - padding;
const columnIndex = Math.round(effectiveX / (columnWidth + gap));
const snappedX = padding + columnIndex * (columnWidth + gap);
// Y 좌표는 동적 행 높이 단위로 스냅
const rowHeight = Math.max(20, gap);
const effectiveY = child.position.y - padding;
const rowIndex = Math.round(effectiveY / rowHeight);
const snappedY = padding + rowIndex * rowHeight;
// 크기는 외부 격자와 동일하게 스냅 (columnWidth + gap 사용)
const fullColumnWidth = columnWidth + gap; // 외부 격자와 동일한 크기
const widthInColumns = Math.max(1, Math.round(child.size.width / fullColumnWidth));
const snappedWidth = widthInColumns * fullColumnWidth - gap; // gap 제거하여 실제 컴포넌트 크기
const snappedHeight = Math.max(40, Math.round(child.size.height / rowHeight) * rowHeight);
const snappedChild = {
...child,
position: {
x: Math.max(padding, snappedX), // 패딩만큼 최소 여백 확보
y: Math.max(padding, snappedY),
z: child.position.z || 1,
},
size: {
width: snappedWidth,
height: snappedHeight,
},
};
console.log(`✅ 자식 ${index + 1} 격자 정렬 완료:`, {
childId: child.id,
calculation: {
effectiveX,
effectiveY,
columnIndex,
rowIndex,
widthInColumns,
originalX: child.position.x,
snappedX: snappedChild.position.x,
padding,
},
snappedPosition: snappedChild.position,
snappedSize: snappedChild.size,
deltaX: snappedChild.position.x - child.position.x,
deltaY: snappedChild.position.y - child.position.y,
});
return snappedChild;
});
}
/**
*
*/
export function calculateOptimalGroupSize(
children: Array<{ position: Position; size: Size }>,
gridInfo: GridInfo,
gridSettings: GridSettings,
): Size {
if (children.length === 0) {
return { width: gridInfo.columnWidth * 2, height: 40 * 2 };
}
console.log("📏 calculateOptimalGroupSize 시작:", {
childrenCount: children.length,
children: children.map((c) => ({ pos: c.position, size: c.size })),
});
// 모든 자식 컴포넌트를 포함하는 최소 경계 계산
const bounds = children.reduce(
(acc, child) => ({
minX: Math.min(acc.minX, child.position.x),
minY: Math.min(acc.minY, child.position.y),
maxX: Math.max(acc.maxX, child.position.x + child.size.width),
maxY: Math.max(acc.maxY, child.position.y + child.size.height),
}),
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
);
console.log("📐 경계 계산:", bounds);
const contentWidth = bounds.maxX - bounds.minX;
const contentHeight = bounds.maxY - bounds.minY;
// 그룹은 격자 스냅 없이 컨텐츠에 맞는 자연스러운 크기
const padding = 16; // 그룹 내부 여백
const groupSize = {
width: contentWidth + padding * 2,
height: contentHeight + padding * 2,
};
2025-10-13 18:37:18 +09:00
console.log("✅ 자연스러운 그룹 크기:", {
contentSize: { width: contentWidth, height: contentHeight },
withPadding: groupSize,
strategy: "그룹은 격자 스냅 없이, 내부 컴포넌트만 격자에 맞춤",
});
return groupSize;
}
/**
*
*/
export function normalizeGroupChildPositions(children: any[], gridSettings: GridSettings): any[] {
if (!gridSettings.snapToGrid || children.length === 0) return children;
console.log("🔄 normalizeGroupChildPositions 시작:", {
childrenCount: children.length,
originalPositions: children.map((c) => ({ id: c.id, pos: c.position })),
});
// 모든 자식의 최소 위치 찾기
const minX = Math.min(...children.map((child) => child.position.x));
const minY = Math.min(...children.map((child) => child.position.y));
console.log("📍 최소 위치:", { minX, minY });
// 그룹 내에서 시작점을 패딩만큼 떨어뜨림 (자연스러운 여백)
const padding = 16;
const startX = padding;
const startY = padding;
const normalizedChildren = children.map((child) => ({
...child,
position: {
x: child.position.x - minX + startX,
y: child.position.y - minY + startY,
z: child.position.z || 1,
},
}));
console.log("✅ 정규화 완료:", {
normalizedPositions: normalizedChildren.map((c) => ({ id: c.id, pos: c.position })),
});
return normalizedChildren;
2025-09-03 11:32:09 +09:00
}