import { Position, Size } from "@/types/screen"; import { GridSettings } from "@/types/screen-management"; export interface GridInfo { columnWidth: number; totalWidth: number; totalHeight: number; } /** * 격자 정보 계산 */ export function calculateGridInfo( containerWidth: number, containerHeight: number, gridSettings: GridSettings, ): GridInfo { const { gap, padding } = gridSettings; let { columns } = gridSettings; // 🔥 최소 컬럼 너비를 보장하기 위한 최대 컬럼 수 계산 const MIN_COLUMN_WIDTH = 30; // 최소 컬럼 너비 30px const availableWidth = containerWidth - padding * 2; const maxPossibleColumns = Math.floor((availableWidth + gap) / (MIN_COLUMN_WIDTH + gap)); // 설정된 컬럼 수가 너무 많으면 자동으로 제한 if (columns > maxPossibleColumns) { console.warn( `⚠️ 격자 컬럼 수가 너무 많습니다. ${columns}개 → ${maxPossibleColumns}개로 자동 조정됨 (최소 컬럼 너비: ${MIN_COLUMN_WIDTH}px)`, ); columns = Math.max(1, maxPossibleColumns); } // 격자 간격을 고려한 컬럼 너비 계산 const totalGaps = (columns - 1) * gap; const columnWidth = (availableWidth - totalGaps) / columns; return { columnWidth: Math.max(columnWidth, MIN_COLUMN_WIDTH), totalWidth: containerWidth, totalHeight: containerHeight, }; } /** * 위치를 격자에 맞춤 */ export function snapToGrid(position: Position, gridInfo: GridInfo, gridSettings: GridSettings): Position { if (!gridSettings.snapToGrid) { return position; } const { columnWidth } = gridInfo; const { gap, padding } = gridSettings; // 격자 셀 크기 (컬럼 너비 + 간격을 하나의 격자 단위로 계산) const cellWidth = columnWidth + gap; const cellHeight = 10; // 행 높이 10px 단위로 고정 // 패딩을 제외한 상대 위치 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); return { x: snappedX, y: snappedY, z: position.z, }; } /** * 크기를 격자에 맞춤 */ export function snapSizeToGrid(size: Size, gridInfo: GridInfo, gridSettings: GridSettings): Size { if (!gridSettings.snapToGrid) { return size; } const { columnWidth } = gridInfo; const { gap } = gridSettings; // 격자 단위로 너비 계산 // 컴포넌트가 차지하는 컬럼 수를 올바르게 계산 let gridColumns = 1; // 현재 너비에서 가장 가까운 격자 컬럼 수 찾기 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; } gridColumns = cols; } const snappedWidth = gridColumns * columnWidth + (gridColumns - 1) * gap; // 높이는 10px 단위로 스냅 const rowHeight = 10; const snappedHeight = Math.max(10, 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; } /** * gridColumns 속성을 기반으로 컴포넌트 크기 업데이트 */ 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); return { width: newWidth, height: component.size.height, // 높이는 유지 }; } /** * 컴포넌트의 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))); } /** * 격자 가이드라인 생성 */ 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 = 10; // 행 높이 10px 단위로 고정 // 세로 격자선 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); } return { verticalLines, horizontalLines, }; } /** * 컴포넌트가 격자 경계에 있는지 확인 */ export function isOnGridBoundary( position: Position, size: Size, gridInfo: GridInfo, gridSettings: GridSettings, tolerance: number = 5, ): boolean { 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; } /** * 그룹 내부 컴포넌트들을 격자에 맞게 정렬 */ export function alignGroupChildrenToGrid( children: any[], groupPosition: Position, gridInfo: GridInfo, gridSettings: GridSettings, ): any[] { if (!gridSettings.snapToGrid || children.length === 0) return children; 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 좌표는 10px 단위로 스냅 const rowHeight = 10; 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(10, 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: 10 * 4 }; } 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, }; 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; }