898 lines
28 KiB
TypeScript
898 lines
28 KiB
TypeScript
/**
|
|
* 슬롯 기반 레이아웃 조정 로직
|
|
*
|
|
* 핵심 기능:
|
|
* 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<number>();
|
|
|
|
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 },
|
|
};
|
|
}
|