import { PopGridPosition, GridMode, GRID_BREAKPOINTS } from "../types/pop-layout"; // ======================================== // 그리드 위치 변환 // ======================================== /** * 12칸 기준 위치를 다른 모드로 변환 */ export function convertPositionToMode( position: PopGridPosition, targetMode: GridMode ): PopGridPosition { const sourceColumns = 12; const targetColumns = GRID_BREAKPOINTS[targetMode].columns; // 같은 칸 수면 그대로 반환 if (sourceColumns === targetColumns) { return position; } const ratio = targetColumns / sourceColumns; // 열 위치 변환 let newCol = Math.max(1, Math.ceil((position.col - 1) * ratio) + 1); let newColSpan = Math.max(1, Math.round(position.colSpan * ratio)); // 범위 초과 방지 if (newCol > targetColumns) { newCol = 1; } if (newCol + newColSpan - 1 > targetColumns) { newColSpan = targetColumns - newCol + 1; } return { col: newCol, row: position.row, colSpan: Math.max(1, newColSpan), rowSpan: position.rowSpan, }; } // ======================================== // 겹침 감지 및 해결 // ======================================== /** * 두 위치가 겹치는지 확인 */ export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean { // 열 겹침 체크 const aColEnd = a.col + a.colSpan - 1; const bColEnd = b.col + b.colSpan - 1; const colOverlap = !(aColEnd < b.col || bColEnd < a.col); // 행 겹침 체크 const aRowEnd = a.row + a.rowSpan - 1; const bRowEnd = b.row + b.rowSpan - 1; const rowOverlap = !(aRowEnd < b.row || bRowEnd < a.row); return colOverlap && rowOverlap; } /** * 겹침 해결 (아래로 밀기) */ export function resolveOverlaps( positions: Array<{ id: string; position: PopGridPosition }>, columns: number ): Array<{ id: string; position: PopGridPosition }> { // row, col 순으로 정렬 const sorted = [...positions].sort((a, b) => a.position.row - b.position.row || a.position.col - b.position.col ); const resolved: Array<{ id: string; position: PopGridPosition }> = []; sorted.forEach((item) => { let { row, col, colSpan, rowSpan } = item.position; // 열이 범위를 초과하면 조정 if (col + colSpan - 1 > columns) { colSpan = columns - col + 1; } // 기존 배치와 겹치면 아래로 이동 let attempts = 0; const maxAttempts = 100; while (attempts < maxAttempts) { const currentPos: PopGridPosition = { col, row, colSpan, rowSpan }; const hasOverlap = resolved.some(r => isOverlapping(currentPos, r.position)); if (!hasOverlap) break; row++; attempts++; } resolved.push({ id: item.id, position: { col, row, colSpan, rowSpan }, }); }); return resolved; } // ======================================== // 좌표 변환 // ======================================== /** * 마우스 좌표 → 그리드 좌표 변환 */ export function mouseToGridPosition( mouseX: number, mouseY: number, canvasRect: DOMRect, columns: number, rowHeight: number, gap: number, padding: number ): { col: number; row: number } { // 캔버스 내 상대 위치 const relX = mouseX - canvasRect.left - padding; const relY = mouseY - canvasRect.top - padding; // 칸 너비 계산 const totalGap = gap * (columns - 1); const colWidth = (canvasRect.width - padding * 2 - totalGap) / columns; // 그리드 좌표 계산 (1부터 시작) const col = Math.max(1, Math.min(columns, Math.floor(relX / (colWidth + gap)) + 1)); const row = Math.max(1, Math.floor(relY / (rowHeight + gap)) + 1); return { col, row }; } /** * 그리드 좌표 → 픽셀 좌표 변환 */ export function gridToPixelPosition( col: number, row: number, colSpan: number, rowSpan: number, canvasWidth: number, columns: number, rowHeight: number, gap: number, padding: number ): { x: number; y: number; width: number; height: number } { const totalGap = gap * (columns - 1); const colWidth = (canvasWidth - padding * 2 - totalGap) / columns; return { x: padding + (col - 1) * (colWidth + gap), y: padding + (row - 1) * (rowHeight + gap), width: colWidth * colSpan + gap * (colSpan - 1), height: rowHeight * rowSpan + gap * (rowSpan - 1), }; } // ======================================== // 위치 검증 // ======================================== /** * 위치가 그리드 범위 내에 있는지 확인 */ export function isValidPosition( position: PopGridPosition, columns: number ): boolean { return ( position.col >= 1 && position.row >= 1 && position.colSpan >= 1 && position.rowSpan >= 1 && position.col + position.colSpan - 1 <= columns ); } /** * 위치를 그리드 범위 내로 조정 */ export function clampPosition( position: PopGridPosition, columns: number ): PopGridPosition { let { col, row, colSpan, rowSpan } = position; // 최소값 보장 col = Math.max(1, col); row = Math.max(1, row); colSpan = Math.max(1, colSpan); rowSpan = Math.max(1, rowSpan); // 열 범위 초과 방지 if (col + colSpan - 1 > columns) { if (col > columns) { col = 1; } colSpan = columns - col + 1; } return { col, row, colSpan, rowSpan }; } // ======================================== // 자동 배치 // ======================================== /** * 다음 빈 위치 찾기 */ export function findNextEmptyPosition( existingPositions: PopGridPosition[], colSpan: number, rowSpan: number, columns: number ): PopGridPosition { let row = 1; let col = 1; const maxAttempts = 1000; let attempts = 0; while (attempts < maxAttempts) { const candidatePos: PopGridPosition = { col, row, colSpan, rowSpan }; // 범위 체크 if (col + colSpan - 1 > columns) { col = 1; row++; continue; } // 겹침 체크 const hasOverlap = existingPositions.some(pos => isOverlapping(candidatePos, pos) ); if (!hasOverlap) { return candidatePos; } // 다음 위치로 이동 col++; if (col + colSpan - 1 > columns) { col = 1; row++; } attempts++; } // 실패 시 마지막 행에 배치 return { col: 1, row: row + 1, colSpan, rowSpan }; } /** * 컴포넌트들을 자동으로 배치 */ export function autoLayoutComponents( components: Array<{ id: string; colSpan: number; rowSpan: number }>, columns: number ): Array<{ id: string; position: PopGridPosition }> { const result: Array<{ id: string; position: PopGridPosition }> = []; let currentRow = 1; let currentCol = 1; components.forEach(comp => { // 현재 행에 공간이 부족하면 다음 행으로 if (currentCol + comp.colSpan - 1 > columns) { currentRow++; currentCol = 1; } result.push({ id: comp.id, position: { col: currentCol, row: currentRow, colSpan: comp.colSpan, rowSpan: comp.rowSpan, }, }); currentCol += comp.colSpan; }); return result; }