/** * 슬롯 기반 레이아웃 계산 유틸리티 * * 핵심 개념: * - 12컬럼 그리드를 12개의 슬롯(0-11)으로 나눔 * - 각 컴포넌트는 여러 슬롯을 차지 * - 드래그 위치를 슬롯 인덱스로 변환하여 정확한 배치 가능 */ import { ComponentData, GridSettings, Position } from "@/types/screen"; /** * 슬롯 맵 타입 정의 * rowIndex를 키로 하고, 각 행은 12개의 슬롯(0-11)을 가짐 * 각 슬롯은 컴포넌트 ID 또는 null(빈 슬롯) */ export type SlotMap = { [rowIndex: number]: { [slotIndex: number]: string | null; // 컴포넌트 ID 또는 null }; }; /** * 슬롯 점유 정보 */ export interface SlotOccupancy { componentId: string; startSlot: number; // 0-11 endSlot: number; // 0-11 columns: number; } /** * 그리드 정보 (슬롯 계산용) */ export interface GridInfo { columnWidth: number; rowHeight: number; gap: number; totalColumns: number; } /** * 캔버스 너비와 그리드 설정으로 GridInfo 생성 */ export function createGridInfo(canvasWidth: number, gridSettings: GridSettings): GridInfo { const { columns = 12, gap = 16 } = gridSettings; const columnWidth = (canvasWidth - gap * (columns - 1)) / columns; const rowHeight = 200; // 기본 행 높이 (Y 좌표 100px 이내를 같은 행으로 간주) return { columnWidth, rowHeight, gap, totalColumns: columns, }; } /** * 컴포넌트의 컬럼 수 가져오기 */ export function getComponentColumns(component: ComponentData): number { const anyComp = component as any; return anyComp.gridColumns || anyComp.columnSpan || 12; } /** * X 좌표를 슬롯 인덱스로 변환 (0-11) */ export function positionToSlot(x: number, gridInfo: GridInfo): number { const { columnWidth, gap } = gridInfo; const slotWidth = columnWidth + gap; // X 좌표를 슬롯 인덱스로 변환 let slot = Math.round(x / slotWidth); // 0-11 범위로 제한 slot = Math.max(0, Math.min(11, slot)); return slot; } /** * Y 좌표를 행 인덱스로 변환 */ export function positionToRow(y: number, gridInfo: GridInfo): number { const { rowHeight } = gridInfo; return Math.floor(y / rowHeight); } /** * 슬롯 인덱스를 X 좌표로 변환 */ export function slotToPosition(slot: number, gridInfo: GridInfo): number { const { columnWidth, gap } = gridInfo; const slotWidth = columnWidth + gap; return slot * slotWidth; } /** * 행 인덱스를 Y 좌표로 변환 */ export function rowToPosition(rowIndex: number, gridInfo: GridInfo): number { const { rowHeight } = gridInfo; return rowIndex * rowHeight; } /** * 컴포넌트 배열로부터 슬롯 맵 생성 */ export function buildSlotMap(components: ComponentData[], gridInfo: GridInfo): SlotMap { const slotMap: SlotMap = {}; console.log("🗺️ 슬롯 맵 생성 시작:", { componentCount: components.length, gridInfo, }); for (const component of components) { const columns = getComponentColumns(component); const x = component.position.x; const y = component.position.y; // 컴포넌트의 시작 슬롯 계산 const startSlot = positionToSlot(x, gridInfo); // 행 인덱스 계산 const rowIndex = positionToRow(y, gridInfo); // 차지하는 슬롯 수 = 컬럼 수 const endSlot = Math.min(11, startSlot + columns - 1); // 해당 행 초기화 if (!slotMap[rowIndex]) { slotMap[rowIndex] = {}; // 모든 슬롯을 null로 초기화 for (let i = 0; i < 12; i++) { slotMap[rowIndex][i] = null; } } // 슬롯 점유 표시 for (let slot = startSlot; slot <= endSlot; slot++) { slotMap[rowIndex][slot] = component.id; } console.log(`📍 컴포넌트 ${component.id}:`, { position: { x, y }, rowIndex, startSlot, endSlot, columns, slots: `${startSlot}-${endSlot}`, }); } // 각 행의 빈 슬롯 로그 Object.entries(slotMap).forEach(([rowIndex, slots]) => { const emptySlots = Object.entries(slots) .filter(([_, componentId]) => componentId === null) .map(([slotIndex, _]) => slotIndex); console.log(`📊 행 ${rowIndex} 상태:`, { occupied: 12 - emptySlots.length, empty: emptySlots.length, emptySlots, }); }); return slotMap; } /** * 특정 행의 슬롯 점유 정보 추출 */ export function getRowOccupancy(rowIndex: number, slotMap: SlotMap): SlotOccupancy[] { const row = slotMap[rowIndex]; if (!row) return []; const occupancies: SlotOccupancy[] = []; let currentComponent: string | null = null; let startSlot = -1; for (let slot = 0; slot < 12; slot++) { const componentId = row[slot]; if (componentId !== currentComponent) { // 이전 컴포넌트 정보 저장 if (currentComponent !== null && startSlot !== -1) { occupancies.push({ componentId: currentComponent, startSlot, endSlot: slot - 1, columns: slot - startSlot, }); } // 새 컴포넌트 시작 if (componentId !== null) { currentComponent = componentId; startSlot = slot; } else { currentComponent = null; startSlot = -1; } } } // 마지막 컴포넌트 처리 if (currentComponent !== null && startSlot !== -1) { occupancies.push({ componentId: currentComponent, startSlot, endSlot: 11, columns: 12 - startSlot, }); } return occupancies; } /** * 특정 슬롯 범위가 비어있는지 체크 */ export function areSlotsEmpty(startSlot: number, endSlot: number, rowIndex: number, slotMap: SlotMap): boolean { const row = slotMap[rowIndex]; if (!row) return true; // 행이 없으면 비어있음 for (let slot = startSlot; slot <= endSlot; slot++) { if (row[slot] !== null) { return false; } } return true; } /** * 특정 슬롯 범위를 차지하는 컴포넌트 ID 목록 */ export function getComponentsInSlots(startSlot: number, endSlot: number, rowIndex: number, slotMap: SlotMap): string[] { const row = slotMap[rowIndex]; if (!row) return []; const componentIds = new Set(); for (let slot = startSlot; slot <= endSlot; slot++) { const componentId = row[slot]; if (componentId !== null) { componentIds.add(componentId); } } return Array.from(componentIds); } /** * 행의 빈 슬롯 수 계산 */ export function countEmptySlots(rowIndex: number, slotMap: SlotMap): number { const row = slotMap[rowIndex]; if (!row) return 12; // 행이 없으면 모두 비어있음 let emptyCount = 0; for (let slot = 0; slot < 12; slot++) { if (row[slot] === null) { emptyCount++; } } return emptyCount; } /** * 컴포넌트가 차지하는 슬롯 범위 찾기 */ export function findComponentSlots( componentId: string, rowIndex: number, slotMap: SlotMap, ): { startSlot: number; endSlot: number; columns: number } | null { const row = slotMap[rowIndex]; if (!row) return null; let startSlot = -1; let endSlot = -1; for (let slot = 0; slot < 12; slot++) { if (row[slot] === componentId) { if (startSlot === -1) { startSlot = slot; } endSlot = slot; } } if (startSlot === -1) return null; return { startSlot, endSlot, columns: endSlot - startSlot + 1, }; }