/** * 슬롯 기반 레이아웃 조정 로직 * * 핵심 기능: * 1. 특정 슬롯에 컴포넌트 배치 가능 여부 체크 * 2. 배치 시 다른 컴포넌트들을 자동으로 조정 * 3. 조정 우선순위: 빈 공간 활용 → 컴포넌트 축소 → 아래로 이동 */ import { ComponentData, GridSettings, Position } from "@/types/screen"; import { SlotMap, GridInfo, getComponentColumns, areSlotsEmpty, getComponentsInSlots, countEmptySlots, findComponentSlots, slotToPosition, positionToSlot, rowToPosition, buildSlotMap, positionToRow, } from "./slotCalculations"; import { calculateWidthFromColumns } from "./gridUtils"; /** * 배치 가능 여부 체크 결과 */ export interface PlacementCheck { canPlace: boolean; strategy: "EMPTY_SPACE" | "SHRINK_COMPONENTS" | "MOVE_DOWN" | "IMPOSSIBLE"; reason?: string; affectedComponents: string[]; // 영향받는 컴포넌트 ID requiredSpace: number; // 필요한 슬롯 수 availableSpace: number; // 사용 가능한 빈 슬롯 수 } /** * 레이아웃 조정 결과 */ export interface LayoutAdjustment { success: boolean; adjustedComponents: ComponentData[]; resizedComponents: Array<{ id: string; oldColumns: number; newColumns: number; oldSlots: number[]; newSlots: number[]; }>; movedComponents: Array<{ id: string; oldRow: number; newRow: number; oldPosition: Position; newPosition: Position; }>; placement: Position; // 드래그된 컴포넌트의 최종 배치 위치 } /** * 특정 슬롯에 컴포넌트 배치 가능 여부 체크 */ export function canPlaceInSlot( targetSlot: number, columns: number, rowIndex: number, slotMap: SlotMap, allComponents: ComponentData[], minColumns: number = 2, ): PlacementCheck { const endSlot = Math.min(11, targetSlot + columns - 1); console.log("🔍 배치 가능 여부 체크:", { targetSlot, endSlot, columns, rowIndex, }); // 슬롯이 비어있는지 체크 const isEmpty = areSlotsEmpty(targetSlot, endSlot, rowIndex, slotMap); if (isEmpty) { return { canPlace: true, strategy: "EMPTY_SPACE", affectedComponents: [], requiredSpace: columns, availableSpace: columns, }; } // 겹치는 컴포넌트 찾기 const affectedComponents = getComponentsInSlots(targetSlot, endSlot, rowIndex, slotMap); // 빈 슬롯 수 계산 const emptySlots = countEmptySlots(rowIndex, slotMap); console.log("📊 행 분석:", { emptySlots, affectedComponents, requiredSpace: columns, }); // 컴포넌트를 축소해서 공간 확보 가능한지 먼저 체크 const canShrink = checkIfCanShrink(affectedComponents, columns, rowIndex, slotMap, allComponents, minColumns); console.log("✂️ 축소 가능 여부:", { possible: canShrink.possible, availableSpace: canShrink.availableSpace, emptySlots, totalAvailable: emptySlots + canShrink.availableSpace, required: columns, }); // 빈 공간 + 축소 가능 공간이 충분하면 축소 전략 사용 if (emptySlots + canShrink.availableSpace >= columns) { return { canPlace: true, strategy: "SHRINK_COMPONENTS", affectedComponents, requiredSpace: columns, availableSpace: emptySlots + canShrink.availableSpace, }; } // 축소로도 불가능하면 아래로 이동 return { canPlace: true, strategy: "MOVE_DOWN", affectedComponents, requiredSpace: columns, availableSpace: emptySlots, }; } /** * 컴포넌트들을 축소할 수 있는지 체크 */ function checkIfCanShrink( componentIds: string[], requiredSpace: number, rowIndex: number, slotMap: SlotMap, allComponents: ComponentData[], minColumns: number, ): { possible: boolean; availableSpace: number } { let totalShrinkable = 0; for (const componentId of componentIds) { const component = allComponents.find((c) => c.id === componentId); if (!component) continue; const currentColumns = getComponentColumns(component); const shrinkable = Math.max(0, currentColumns - minColumns); totalShrinkable += shrinkable; } console.log("✂️ 축소 가능 공간:", { componentIds, totalShrinkable, requiredSpace, possible: totalShrinkable >= requiredSpace, }); return { possible: totalShrinkable >= requiredSpace, availableSpace: totalShrinkable, }; } /** * 슬롯에 컴포넌트 배치 시 레이아웃 조정 계산 */ export function calculateSlotPlacement( targetSlot: number, draggedColumns: number, draggedComponentId: string, rowIndex: number, slotMap: SlotMap, allComponents: ComponentData[], gridInfo: GridInfo, gridSettings: GridSettings, minColumns: number = 2, ): LayoutAdjustment { const endSlot = Math.min(11, targetSlot + draggedColumns - 1); console.log("🎯 레이아웃 조정 계산 시작:", { targetSlot, endSlot, draggedColumns, rowIndex, }); // 배치 가능 여부 체크 const check = canPlaceInSlot(targetSlot, draggedColumns, rowIndex, slotMap, allComponents, minColumns); if (!check.canPlace) { return { success: false, adjustedComponents: allComponents, resizedComponents: [], movedComponents: [], placement: { x: 0, y: 0, z: 1 }, }; } // 전략별 조정 수행 switch (check.strategy) { case "EMPTY_SPACE": return placeInEmptySpace(targetSlot, draggedColumns, draggedComponentId, rowIndex, allComponents, gridInfo); case "SHRINK_COMPONENTS": return placeWithShrinking( targetSlot, draggedColumns, draggedComponentId, rowIndex, slotMap, allComponents, gridInfo, gridSettings, minColumns, ); case "MOVE_DOWN": return placeWithMovingDown( targetSlot, draggedColumns, draggedComponentId, rowIndex, slotMap, allComponents, gridInfo, gridSettings, ); default: return { success: false, adjustedComponents: allComponents, resizedComponents: [], movedComponents: [], placement: { x: 0, y: 0, z: 1 }, }; } } /** * 전략 1: 빈 공간에 배치 (조정 불필요) */ function placeInEmptySpace( targetSlot: number, draggedColumns: number, draggedComponentId: string, rowIndex: number, allComponents: ComponentData[], gridInfo: GridInfo, ): LayoutAdjustment { console.log("✅ 빈 공간에 배치 (조정 불필요)"); const x = slotToPosition(targetSlot, gridInfo); const y = rowToPosition(rowIndex, gridInfo); return { success: true, adjustedComponents: allComponents, resizedComponents: [], movedComponents: [], placement: { x, y, z: 1 }, }; } /** * 전략 2: 컴포넌트 축소하여 배치 */ function placeWithShrinking( targetSlot: number, draggedColumns: number, draggedComponentId: string, rowIndex: number, slotMap: SlotMap, allComponents: ComponentData[], gridInfo: GridInfo, gridSettings: GridSettings, minColumns: number, ): LayoutAdjustment { console.log("✂️ 컴포넌트 축소하여 배치"); const { columnWidth, gap } = gridInfo; const endSlot = Math.min(11, targetSlot + draggedColumns - 1); // 겹치는 컴포넌트들 (드롭 위치에 직접 겹치는 컴포넌트) const directlyAffected = getComponentsInSlots(targetSlot, endSlot, rowIndex, slotMap); // 같은 행의 모든 컴포넌트들 (왼쪽/오른쪽 모두) const allComponentsInRow = allComponents.filter((c) => { const compRow = positionToRow(c.position.y, gridInfo); return compRow === rowIndex && c.id !== draggedComponentId; }); let adjustedComponents = [...allComponents]; const resizedComponents: LayoutAdjustment["resizedComponents"] = []; // 같은 행의 모든 컴포넌트를 슬롯 순서대로 정렬 const componentsWithSlots = allComponentsInRow .map((c) => { const slots = findComponentSlots(c.id, rowIndex, slotMap); return { component: c, startSlot: slots?.startSlot ?? 0, columns: getComponentColumns(c), }; }) .sort((a, b) => a.startSlot - b.startSlot); // targetSlot 이전/이후 컴포넌트 분리 // 컴포넌트의 끝이 targetSlot 이전이면 beforeTarget const beforeTarget = componentsWithSlots.filter((c) => c.startSlot + c.columns - 1 < targetSlot); // 컴포넌트의 시작이 targetSlot 이후거나, targetSlot과 겹치면 atOrAfterTarget const atOrAfterTarget = componentsWithSlots.filter((c) => c.startSlot + c.columns - 1 >= targetSlot); // 이전 컴포넌트들의 총 컬럼 수 const beforeColumns = beforeTarget.reduce((sum, c) => sum + c.columns, 0); // 이후 컴포넌트들의 총 컬럼 수 const afterColumns = atOrAfterTarget.reduce((sum, c) => sum + c.columns, 0); // 필요한 총 컬럼: 이전 + 드래그 + 이후 const totalNeeded = beforeColumns + draggedColumns + afterColumns; console.log("📊 레이아웃 분석:", { targetSlot, beforeColumns, draggedColumns, afterColumns, totalNeeded, spaceNeeded: totalNeeded - 12, before: beforeTarget.map((c) => `${c.component.id}:${c.columns}`), after: atOrAfterTarget.map((c) => `${c.component.id}:${c.columns}`), }); // atOrAfterTarget 컴포넌트들 중 targetSlot과 겹치는 컴포넌트가 있는지 체크 const overlappingComponents = atOrAfterTarget.filter((c) => { const endSlot = c.startSlot + c.columns - 1; const targetEndSlot = Math.min(11, targetSlot + draggedColumns - 1); // 겹침 조건: 컴포넌트 범위와 목표 범위가 겹치는지 return !(endSlot < targetSlot || c.startSlot > targetEndSlot); }); console.log("🔍 겹침 컴포넌트:", { overlapping: overlappingComponents.map((c) => `${c.component.id}:${c.startSlot}-${c.startSlot + c.columns - 1}`), }); // 실제로 겹치는 슬롯 수 계산 let overlapSlots = 0; if (overlappingComponents.length > 0) { const targetEndSlot = Math.min(11, targetSlot + draggedColumns - 1); // 각 겹치는 컴포넌트의 겹침 범위를 계산하여 최대값 사용 // (여러 컴포넌트와 겹칠 경우, 전체 겹침 범위) const allOverlapSlots = new Set(); for (const compInfo of overlappingComponents) { const compEndSlot = compInfo.startSlot + compInfo.columns - 1; // 겹치는 슬롯 범위 const overlapStart = Math.max(compInfo.startSlot, targetSlot); const overlapEnd = Math.min(compEndSlot, targetEndSlot); for (let slot = overlapStart; slot <= overlapEnd; slot++) { allOverlapSlots.add(slot); } } overlapSlots = allOverlapSlots.size; } // 필요한 공간 = 실제 겹치는 슬롯 수 let spaceNeeded = overlapSlots; // 12컬럼 초과 체크 (겹침과는 별개로 전체 공간 부족) if (totalNeeded > 12) { spaceNeeded = Math.max(spaceNeeded, totalNeeded - 12); } console.log("📊 공간 분석:", { hasOverlap: overlappingComponents.length > 0, overlapSlots, draggedColumns, totalNeeded, spaceNeeded, overlapping: overlappingComponents.map( (c) => `${c.component.id}:슬롯${c.startSlot}-${c.startSlot + c.columns - 1}`, ), }); // 필요한 만큼 축소 if (spaceNeeded > 0) { // atOrAfterTarget 컴포넌트들을 축소 for (const compInfo of atOrAfterTarget) { if (spaceNeeded <= 0) break; const oldColumns = compInfo.columns; const maxShrink = oldColumns - minColumns; if (maxShrink > 0) { const shrinkAmount = Math.min(spaceNeeded, maxShrink); const newColumns = oldColumns - shrinkAmount; const newWidth = newColumns * columnWidth + (newColumns - 1) * gap; const componentIndex = adjustedComponents.findIndex((c) => c.id === compInfo.component.id); if (componentIndex !== -1) { adjustedComponents[componentIndex] = { ...adjustedComponents[componentIndex], gridColumns: newColumns, columnSpan: newColumns, size: { ...adjustedComponents[componentIndex].size, width: newWidth, }, } as ComponentData; resizedComponents.push({ id: compInfo.component.id, oldColumns, newColumns, oldSlots: Array.from({ length: oldColumns }, (_, i) => compInfo.startSlot + i), newSlots: Array.from({ length: newColumns }, (_, i) => compInfo.startSlot + i), }); spaceNeeded -= shrinkAmount; console.log(`✂️ ${compInfo.component.id} 축소:`, { oldColumns, newColumns, shrinkAmount, remainingNeed: spaceNeeded, }); } } } } console.log("📋 초기 축소 완료:", { totalResized: resizedComponents.length, resizedComponents: resizedComponents.map((r) => ({ id: r.id, oldColumns: r.oldColumns, newColumns: r.newColumns, })), }); // 배치 위치 계산 let x = slotToPosition(targetSlot, gridInfo); let y = rowToPosition(rowIndex, gridInfo); // 드래그될 컴포넌트가 차지할 슬롯 범위 (예약) const reservedStartSlot = targetSlot; const reservedEndSlot = Math.min(11, targetSlot + draggedColumns - 1); console.log("🎯 드래그 컴포넌트 슬롯 예약:", { targetSlot, draggedColumns, reservedSlots: `${reservedStartSlot}-${reservedEndSlot}`, currentSpaceNeeded: spaceNeeded, }); // 같은 행의 모든 컴포넌트들의 X 위치를 재계산하여 슬롯에 맞게 정렬 // 드래그될 컴포넌트의 슬롯은 건너뛰기 const componentsInRowSorted = adjustedComponents .filter((c) => { const compRow = positionToRow(c.position.y, gridInfo); return compRow === rowIndex && c.id !== draggedComponentId; }) .sort((a, b) => a.position.x - b.position.x); let currentSlot = 0; const movedToNextRow: string[] = []; // 다음 행으로 이동한 컴포넌트들 for (const comp of componentsInRowSorted) { const compIndex = adjustedComponents.findIndex((c) => c.id === comp.id); if (compIndex !== -1) { let compColumns = getComponentColumns(adjustedComponents[compIndex]); // 현재 슬롯이 예약된 범위와 겹치는지 체크 const wouldOverlap = currentSlot <= reservedEndSlot && currentSlot + compColumns - 1 >= reservedStartSlot; if (wouldOverlap) { // 예약된 슬롯을 건너뛰고 그 다음부터 배치 currentSlot = reservedEndSlot + 1; console.log(`⏭️ ${comp.id} 예약된 슬롯 건너뛰기, 새 시작 슬롯: ${currentSlot}`); } // 화면 밖으로 나가는지 체크 (12컬럼 초과) if (currentSlot + compColumns > 12) { const overflow = currentSlot + compColumns - 12; const oldColumns = compColumns; const maxShrink = compColumns - minColumns; // 추가 축소 가능하면 축소 if (maxShrink >= overflow) { compColumns -= overflow; const newWidth = compColumns * columnWidth + (compColumns - 1) * gap; adjustedComponents[compIndex] = { ...adjustedComponents[compIndex], gridColumns: compColumns, columnSpan: compColumns, size: { ...adjustedComponents[compIndex].size, width: newWidth, }, } as ComponentData; resizedComponents.push({ id: comp.id, oldColumns, newColumns: compColumns, oldSlots: [], newSlots: [], }); console.log(`✂️ ${comp.id} 추가 축소 (화면 경계):`, { oldColumns, newColumns: compColumns, overflow, }); } else { // 축소로도 안되면 다음 행으로 이동 console.log(`⚠️ ${comp.id} 화면 밖으로 나감! (슬롯 ${currentSlot}, 컬럼 ${compColumns})`); const newY = rowToPosition(rowIndex + 1, gridInfo); adjustedComponents[compIndex] = { ...adjustedComponents[compIndex], position: { x: 0, y: newY, z: adjustedComponents[compIndex].position.z, }, } as ComponentData; movedToNextRow.push(comp.id); console.log(`⬇️ ${comp.id} 다음 행으로 이동 (y: ${newY})`); continue; } } const newX = slotToPosition(currentSlot, gridInfo); adjustedComponents[compIndex] = { ...adjustedComponents[compIndex], position: { ...adjustedComponents[compIndex].position, x: newX, }, } as ComponentData; console.log(`📍 ${comp.id} 위치 재조정:`, { oldX: comp.position.x, newX, slot: currentSlot, columns: compColumns, wasOverlapping: wouldOverlap, }); currentSlot += compColumns; } } // 오른쪽 축소 효과: resizedComponents 중 targetSlot 오른쪽에 있는 컴포넌트들을 오른쪽으로 밀기 for (const resized of resizedComponents) { const compIndex = adjustedComponents.findIndex((c) => c.id === resized.id); if (compIndex !== -1) { const comp = adjustedComponents[compIndex]; const compSlot = positionToSlot(comp.position.x, gridInfo); // targetSlot 오른쪽에 있고 축소된 컴포넌트만 처리 if (compSlot >= targetSlot && resized.oldColumns > resized.newColumns) { const shrinkAmount = resized.oldColumns - resized.newColumns; const newX = comp.position.x + shrinkAmount * (columnWidth + gap); adjustedComponents[compIndex] = { ...adjustedComponents[compIndex], position: { ...adjustedComponents[compIndex].position, x: newX, }, } as ComponentData; console.log(`➡️ ${resized.id} 오른쪽으로 이동:`, { oldX: comp.position.x, newX, shrinkAmount, oldColumns: resized.oldColumns, newColumns: resized.newColumns, }); } } } // 축소된 컴포넌트 복구 로직 // 조건: // 1. 축소된 컴포넌트가 있고 // 2. 컴포넌트가 다음 행으로 이동했거나 // 3. 드래그 컴포넌트가 현재 행에서 이미 축소된 컴포넌트와 겹치지 않는 위치에 배치되는 경우 if (resizedComponents.length > 0) { console.log("🔍 축소된 컴포넌트 복구 가능 여부 확인:", { movedToNextRow: movedToNextRow.length, resizedComponents: resizedComponents.map((r) => r.id), targetSlot, draggedColumns, }); // 🔧 중요: 업데이트된 컴포넌트 기준으로 새로운 슬롯맵 생성 const updatedSlotMap = buildSlotMap(adjustedComponents, gridInfo); // 드래그 컴포넌트가 차지할 슬롯 범위 const draggedStartSlot = targetSlot; const draggedEndSlot = Math.min(11, targetSlot + draggedColumns - 1); // 축소된 컴포넌트들이 차지하는 슬롯 범위 (원본 크기 기준) let canRestoreAll = true; const restoredSlotRanges: Array<{ id: string; startSlot: number; endSlot: number }> = []; for (const resizeInfo of resizedComponents) { const comp = adjustedComponents.find((c) => c.id === resizeInfo.id); if (!comp || movedToNextRow.includes(resizeInfo.id)) continue; // 현재 위치를 슬롯으로 변환 (업데이트된 슬롯맵 사용) const compSlots = findComponentSlots(resizeInfo.id, rowIndex, updatedSlotMap); if (!compSlots) { console.log(`⚠️ ${resizeInfo.id}의 슬롯을 찾을 수 없음`); canRestoreAll = false; break; } // 원본 크기로 복구했을 때의 슬롯 범위 계산 const currentStartSlot = compSlots.startSlot; const restoredEndSlot = currentStartSlot + resizeInfo.oldColumns - 1; console.log(`📍 ${resizeInfo.id} 슬롯 정보:`, { currentSlots: `${currentStartSlot}-${compSlots.endSlot} (${compSlots.columns}컬럼)`, restoredSlots: `${currentStartSlot}-${restoredEndSlot} (${resizeInfo.oldColumns}컬럼)`, }); // 드래그 컴포넌트와 겹치는지 확인 const wouldOverlapWithDragged = !(restoredEndSlot < draggedStartSlot || currentStartSlot > draggedEndSlot); if (wouldOverlapWithDragged) { console.log(`⚠️ ${resizeInfo.id} 복구 시 드래그 컴포넌트와 겹침:`, { restoredRange: `${currentStartSlot}-${restoredEndSlot}`, draggedRange: `${draggedStartSlot}-${draggedEndSlot}`, }); canRestoreAll = false; break; } restoredSlotRanges.push({ id: resizeInfo.id, startSlot: currentStartSlot, endSlot: restoredEndSlot, }); } // 복구된 컴포넌트들끼리도 겹치는지 확인 if (canRestoreAll) { for (let i = 0; i < restoredSlotRanges.length; i++) { for (let j = i + 1; j < restoredSlotRanges.length; j++) { const range1 = restoredSlotRanges[i]; const range2 = restoredSlotRanges[j]; const overlap = !(range1.endSlot < range2.startSlot || range1.startSlot > range2.endSlot); if (overlap) { console.log(`⚠️ 복구 시 컴포넌트끼리 겹침: ${range1.id} vs ${range2.id}`); canRestoreAll = false; break; } } if (!canRestoreAll) break; } } // 총 컬럼 수 체크 (12컬럼 초과 방지) if (canRestoreAll) { let totalColumnsInRow = draggedColumns; const componentsInCurrentRow = adjustedComponents.filter((c) => { const compRow = positionToRow(c.position.y, gridInfo); return compRow === rowIndex && c.id !== draggedComponentId && !movedToNextRow.includes(c.id); }); for (const comp of componentsInCurrentRow) { const resizeInfo = resizedComponents.find((r) => r.id === comp.id); if (resizeInfo) { totalColumnsInRow += resizeInfo.oldColumns; // 원본 크기 } else { totalColumnsInRow += getComponentColumns(comp); // 현재 크기 } } if (totalColumnsInRow > 12) { console.log(`⚠️ 복구하면 12컬럼 초과: ${totalColumnsInRow}`); canRestoreAll = false; } } // 복구 가능하면 복구 실행 if (canRestoreAll) { console.log("✅ 공간이 충분하여 축소된 컴포넌트 복구"); for (const resizeInfo of resizedComponents) { if (!movedToNextRow.includes(resizeInfo.id)) { const compIndex = adjustedComponents.findIndex((c) => c.id === resizeInfo.id); if (compIndex !== -1) { const originalColumns = resizeInfo.oldColumns; const originalWidth = calculateWidthFromColumns(originalColumns, gridInfo, gridSettings); adjustedComponents[compIndex] = { ...adjustedComponents[compIndex], gridColumns: originalColumns, columnSpan: originalColumns, size: { ...adjustedComponents[compIndex].size, width: originalWidth, }, } as ComponentData; console.log(`🔄 ${resizeInfo.id} 원래 크기로 복구:`, { from: resizeInfo.newColumns, to: originalColumns, }); } } } // 복구 후 위치 재조정 (슬롯 기반) // 드래그 컴포넌트는 targetSlot에 배치하고, 나머지는 슬롯 순서대로 배치 console.log("🔄 복구 후 슬롯 기반 재정렬 시작:", { targetSlot, draggedColumns, restoredComponents: resizedComponents.map((r) => `${r.id}:${r.oldColumns}컬럼`), }); // 1. 현재 행의 모든 컴포넌트를 슬롯 기준으로 수집 const componentsInRow = adjustedComponents.filter((c) => { const compRow = positionToRow(c.position.y, gridInfo); return compRow === rowIndex && c.id !== draggedComponentId; }); // 2. 각 컴포넌트의 시작 슬롯 계산 (업데이트된 슬롯맵 기준) const updatedSlotMap2 = buildSlotMap(adjustedComponents, gridInfo); const componentsForReposition = componentsInRow .map((c) => { const slots = findComponentSlots(c.id, rowIndex, updatedSlotMap2); return { component: c, startSlot: slots?.startSlot ?? 0, columns: getComponentColumns(c), }; }) .sort((a, b) => a.startSlot - b.startSlot); // 3. 드래그 컴포넌트를 targetSlot에 삽입 const draggedSlotInfo = { component: null as any, // 임시 startSlot: targetSlot, columns: draggedColumns, isDragged: true, }; // 4. 슬롯 순서대로 전체 배열 구성 const allSlotsSorted = [...componentsForReposition, draggedSlotInfo].sort((a, b) => a.startSlot - b.startSlot); console.log("📋 슬롯 순서:", { components: allSlotsSorted.map((s) => s.isDragged ? `[드래그:${s.startSlot}슬롯,${s.columns}컬럼]` : `${s.component.id}:${s.startSlot}슬롯,${s.columns}컬럼`, ), }); // 5. 슬롯 순서대로 X 좌표 재배치 let currentSlot = 0; for (const slotInfo of allSlotsSorted) { const posX = slotToPosition(currentSlot, gridInfo); const compColumns = slotInfo.columns; const compWidth = calculateWidthFromColumns(compColumns, gridInfo, gridSettings); if (slotInfo.isDragged) { // 드래그 컴포넌트 x = posX; console.log(` 📍 드래그 컴포넌트: 슬롯${currentSlot}, x=${posX}, ${compColumns}컬럼`); } else { // 일반 컴포넌트 const compIndex = adjustedComponents.findIndex((c) => c.id === slotInfo.component.id); if (compIndex !== -1) { adjustedComponents[compIndex] = { ...adjustedComponents[compIndex], position: { ...adjustedComponents[compIndex].position, x: posX, }, } as ComponentData; console.log(` 📍 ${slotInfo.component.id}: 슬롯${currentSlot}, x=${posX}, ${compColumns}컬럼`); } } currentSlot += compColumns; } console.log("📐 복구 후 위치 재조정 완료", { finalDraggedX: x, finalDraggedSlot: targetSlot }); } else { console.log("⚠️ 복구 조건 불만족, 축소 상태 유지"); } // 복구 여부와 관계없이 resizedComponents 초기화 resizedComponents.length = 0; } return { success: true, adjustedComponents, resizedComponents, movedComponents: [], placement: { x, y, z: 1 }, }; } /** * 전략 3: 겹치는 컴포넌트를 아래로 이동 */ function placeWithMovingDown( targetSlot: number, draggedColumns: number, draggedComponentId: string, rowIndex: number, slotMap: SlotMap, allComponents: ComponentData[], gridInfo: GridInfo, gridSettings: GridSettings, ): LayoutAdjustment { console.log("⬇️ 겹치는 컴포넌트를 아래로 이동"); const endSlot = Math.min(11, targetSlot + draggedColumns - 1); // 겹치는 컴포넌트들 const affectedComponentIds = getComponentsInSlots(targetSlot, endSlot, rowIndex, slotMap); let adjustedComponents = [...allComponents]; const movedComponents: LayoutAdjustment["movedComponents"] = []; // 다음 행으로 이동 const newRowIndex = rowIndex + 1; const newY = rowToPosition(newRowIndex, gridInfo); for (const componentId of affectedComponentIds) { const component = allComponents.find((c) => c.id === componentId); if (!component) continue; const componentIndex = adjustedComponents.findIndex((c) => c.id === componentId); if (componentIndex !== -1) { const oldPosition = adjustedComponents[componentIndex].position; const newPosition = { ...oldPosition, y: newY }; adjustedComponents[componentIndex] = { ...adjustedComponents[componentIndex], position: newPosition, }; movedComponents.push({ id: componentId, oldRow: rowIndex, newRow: newRowIndex, oldPosition, newPosition, }); console.log(`⬇️ ${componentId} 이동:`, { from: `행 ${rowIndex}`, to: `행 ${newRowIndex}`, }); } } // 배치 위치 계산 const x = slotToPosition(targetSlot, gridInfo); const y = rowToPosition(rowIndex, gridInfo); return { success: true, adjustedComponents, resizedComponents: [], movedComponents, placement: { x, y, z: 1 }, }; }