캔버스 작동방식 수정 중간커밋
This commit is contained in:
parent
ec4d8f9b94
commit
41dee6956d
|
|
@ -44,6 +44,13 @@ import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
|
|||
import { initializeComponents } from "@/lib/registry/components";
|
||||
import { ScreenFileAPI } from "@/lib/api/screenFile";
|
||||
import { safeMigrateLayout, needsMigration } from "@/lib/utils/widthToColumnSpan";
|
||||
import {
|
||||
detectDropZones,
|
||||
applyDropZone,
|
||||
adjustLayoutOnColumnChange,
|
||||
DropZone,
|
||||
DetectedDropZones,
|
||||
} from "@/lib/utils/slotBasedLayout";
|
||||
|
||||
import StyleEditor from "./StyleEditor";
|
||||
import { RealtimePreview } from "./RealtimePreviewDynamic";
|
||||
|
|
@ -190,6 +197,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
currentPosition: { x: 0, y: 0, z: 1 },
|
||||
grabOffset: { x: 0, y: 0 },
|
||||
justFinishedDrag: false, // 드래그 종료 직후 클릭 방지용
|
||||
detectedDropZones: null as DetectedDropZones | null, // 슬롯 기반 드롭존
|
||||
originalLayoutSnapshot: [] as ComponentData[], // 드래그 시작 시점의 원본 레이아웃 (크기 복구용)
|
||||
});
|
||||
|
||||
// Pan 모드 상태 (스페이스바 + 드래그)
|
||||
|
|
@ -581,7 +590,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}
|
||||
}
|
||||
|
||||
// gridColumns 변경 시 크기를 격자에 맞게 자동 조정
|
||||
// gridColumns 변경 시 크기를 격자에 맞게 자동 조정 + 슬롯 기반 레이아웃 조정
|
||||
if (path === "gridColumns" && layout.gridSettings?.snapToGrid && newComp.type !== "group") {
|
||||
const currentGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||
columns: layout.gridSettings.columns,
|
||||
|
|
@ -702,13 +711,37 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
return newComp;
|
||||
});
|
||||
|
||||
const newLayout = { ...layout, components: updatedComponents };
|
||||
// gridColumns 변경 시 슬롯 기반 레이아웃 조정 적용
|
||||
let finalComponents = updatedComponents;
|
||||
if (path === "gridColumns" && canvasRef.current) {
|
||||
console.log("🎯 gridColumns 변경 감지 - 슬롯 기반 레이아웃 조정 시작:", {
|
||||
componentId,
|
||||
newColumns: value,
|
||||
});
|
||||
|
||||
const canvasWidth = canvasRef.current.getBoundingClientRect().width;
|
||||
finalComponents = adjustLayoutOnColumnChange(
|
||||
componentId,
|
||||
value,
|
||||
updatedComponents,
|
||||
canvasWidth,
|
||||
layout.gridSettings || { columns: 12, gap: 16, padding: 0, snapToGrid: false },
|
||||
2, // minColumns
|
||||
);
|
||||
|
||||
console.log("✅ 슬롯 기반 레이아웃 조정 완료:", {
|
||||
originalCount: updatedComponents.length,
|
||||
finalCount: finalComponents.length,
|
||||
});
|
||||
}
|
||||
|
||||
const newLayout = { ...layout, components: finalComponents };
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
|
||||
// selectedComponent가 업데이트된 컴포넌트와 같다면 selectedComponent도 업데이트
|
||||
if (selectedComponent && selectedComponent.id === componentId) {
|
||||
const updatedSelectedComponent = updatedComponents.find((c) => c.id === componentId);
|
||||
const updatedSelectedComponent = finalComponents.find((c) => c.id === componentId);
|
||||
if (updatedSelectedComponent) {
|
||||
console.log("🔄 selectedComponent 동기화:", {
|
||||
componentId,
|
||||
|
|
@ -744,7 +777,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
});
|
||||
}
|
||||
},
|
||||
[layout, gridInfo, saveToHistory],
|
||||
[layout, gridInfo, saveToHistory, selectedComponent, screenResolution, canvasRef],
|
||||
);
|
||||
|
||||
// 컴포넌트 시스템 초기화
|
||||
|
|
@ -2797,12 +2830,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
y: relativeMouseY - component.position.y,
|
||||
},
|
||||
justFinishedDrag: false,
|
||||
detectedDropZones: null, // 슬롯 기반 드롭존 초기화
|
||||
originalLayoutSnapshot: JSON.parse(JSON.stringify(layout.components)), // 원본 레이아웃 스냅샷 저장
|
||||
});
|
||||
},
|
||||
[groupState.selectedComponents, layout.components, dragState.justFinishedDrag],
|
||||
);
|
||||
|
||||
// 드래그 중 위치 업데이트 (성능 최적화 + 실시간 업데이트)
|
||||
// 드래그 중 위치 업데이트 (슬롯 기반 시스템)
|
||||
const updateDragPosition = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!dragState.isDragging || !dragState.draggedComponent || !canvasRef.current) return;
|
||||
|
|
@ -2819,39 +2854,105 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
z: (dragState.draggedComponent.position as Position).z || 1,
|
||||
};
|
||||
|
||||
// 드래그 상태 업데이트
|
||||
console.log("🔥 ScreenDesigner updateDragPosition:", {
|
||||
// 슬롯 기반 드롭존 감지 (원본 레이아웃 스냅샷 사용)
|
||||
const canvasWidth = rect.width;
|
||||
const detectedDropZones = detectDropZones(
|
||||
newPosition,
|
||||
dragState.draggedComponent,
|
||||
dragState.originalLayoutSnapshot.length > 0 ? dragState.originalLayoutSnapshot : layout.components,
|
||||
canvasWidth,
|
||||
layout.gridSettings || { columns: 12, gap: 16, padding: 0, snapToGrid: false },
|
||||
2, // minColumns
|
||||
);
|
||||
|
||||
console.log("🔥 슬롯 기반 드롭존 감지:", {
|
||||
draggedComponentId: dragState.draggedComponent.id,
|
||||
oldPosition: dragState.currentPosition,
|
||||
newPosition: newPosition,
|
||||
position: newPosition,
|
||||
detectedDropZones: {
|
||||
horizontal: detectedDropZones.horizontal?.id || "없음",
|
||||
vertical: detectedDropZones.vertical?.id || "없음",
|
||||
best: detectedDropZones.best?.id || "없음",
|
||||
},
|
||||
});
|
||||
|
||||
setDragState((prev) => {
|
||||
const newState = {
|
||||
...prev,
|
||||
currentPosition: { ...newPosition }, // 새로운 객체 생성
|
||||
};
|
||||
console.log("🔄 ScreenDesigner dragState 업데이트:", {
|
||||
prevPosition: prev.currentPosition,
|
||||
newPosition: newState.currentPosition,
|
||||
stateChanged:
|
||||
prev.currentPosition.x !== newState.currentPosition.x ||
|
||||
prev.currentPosition.y !== newState.currentPosition.y,
|
||||
});
|
||||
return newState;
|
||||
});
|
||||
|
||||
// 성능 최적화: 드래그 중에는 상태 업데이트만 하고,
|
||||
// 실제 레이아웃 업데이트는 endDrag에서 처리
|
||||
// 속성 패널에서는 dragState.currentPosition을 참조하여 실시간 표시
|
||||
// 드래그 상태 업데이트
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
currentPosition: { ...newPosition },
|
||||
detectedDropZones,
|
||||
}));
|
||||
},
|
||||
[dragState.isDragging, dragState.draggedComponent, dragState.grabOffset],
|
||||
[
|
||||
dragState.isDragging,
|
||||
dragState.draggedComponent,
|
||||
dragState.grabOffset,
|
||||
dragState.originalLayoutSnapshot,
|
||||
layout.components,
|
||||
layout.gridSettings,
|
||||
],
|
||||
);
|
||||
|
||||
// 드래그 종료
|
||||
// 드래그 종료 (슬롯 기반 시스템)
|
||||
const endDrag = useCallback(() => {
|
||||
if (dragState.isDragging && dragState.draggedComponent) {
|
||||
// 주 드래그 컴포넌트의 최종 위치 계산
|
||||
console.log("🎯 드래그 종료 - 슬롯 기반 배치:", {
|
||||
draggedComponentId: dragState.draggedComponent.id,
|
||||
detectedDropZones: dragState.detectedDropZones,
|
||||
});
|
||||
|
||||
// 최적의 드롭존이 있으면 슬롯 기반 배치
|
||||
if (dragState.detectedDropZones?.best) {
|
||||
const bestDropZone = dragState.detectedDropZones.best;
|
||||
|
||||
console.log("✅ 슬롯 기반 배치 적용:", {
|
||||
dropZoneId: bestDropZone.id,
|
||||
slot: bestDropZone.slot,
|
||||
rowIndex: bestDropZone.rowIndex,
|
||||
strategy: bestDropZone.placementCheck.strategy,
|
||||
});
|
||||
|
||||
// 슬롯 기반 레이아웃 조정 적용
|
||||
const updatedComponents = applyDropZone(bestDropZone, dragState.draggedComponent, layout.components);
|
||||
|
||||
const newLayout = { ...layout, components: updatedComponents };
|
||||
setLayout(newLayout);
|
||||
|
||||
// 선택된 컴포넌트 업데이트
|
||||
if (selectedComponent && selectedComponent.id === dragState.draggedComponent.id) {
|
||||
const updatedSelectedComponent = updatedComponents.find((c) => c.id === selectedComponent.id);
|
||||
if (updatedSelectedComponent) {
|
||||
setSelectedComponent(updatedSelectedComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// 히스토리에 저장
|
||||
saveToHistory(newLayout);
|
||||
|
||||
// 드래그 상태 초기화
|
||||
setDragState({
|
||||
isDragging: false,
|
||||
draggedComponent: null,
|
||||
draggedComponents: [],
|
||||
originalPosition: { x: 0, y: 0, z: 1 },
|
||||
currentPosition: { x: 0, y: 0, z: 1 },
|
||||
grabOffset: { x: 0, y: 0 },
|
||||
justFinishedDrag: true,
|
||||
detectedDropZones: null,
|
||||
originalLayoutSnapshot: [], // 원본 스냅샷 초기화
|
||||
});
|
||||
|
||||
// 짧은 시간 후 justFinishedDrag 플래그 해제
|
||||
setTimeout(() => {
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
justFinishedDrag: false,
|
||||
}));
|
||||
}, 100);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 드롭존이 없으면 기존 격자 스냅 로직 사용 (폴백)
|
||||
const draggedComponent = layout.components.find((c) => c.id === dragState.draggedComponent);
|
||||
let finalPosition = dragState.currentPosition;
|
||||
|
||||
|
|
@ -2882,7 +2983,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
},
|
||||
);
|
||||
|
||||
console.log("🎯 격자 스냅 적용됨:", {
|
||||
console.log("🎯 격자 스냅 적용됨 (폴백):", {
|
||||
resolution: `${screenResolution.width}x${screenResolution.height}`,
|
||||
originalPosition: dragState.currentPosition,
|
||||
snappedPosition: finalPosition,
|
||||
|
|
@ -3008,6 +3109,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
currentPosition: { x: 0, y: 0, z: 1 },
|
||||
grabOffset: { x: 0, y: 0 },
|
||||
justFinishedDrag: true,
|
||||
detectedDropZones: null,
|
||||
originalLayoutSnapshot: [], // 원본 스냅샷 초기화
|
||||
});
|
||||
|
||||
// 짧은 시간 후 justFinishedDrag 플래그 해제
|
||||
|
|
@ -3017,7 +3120,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
justFinishedDrag: false,
|
||||
}));
|
||||
}, 100);
|
||||
}, [dragState, layout, gridInfo, saveToHistory]);
|
||||
}, [dragState, layout, gridInfo, saveToHistory, selectedComponent, saveToHistory, screenResolution]);
|
||||
|
||||
// 드래그 선택 시작
|
||||
const startSelectionDrag = useCallback(
|
||||
|
|
@ -4058,6 +4161,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
};
|
||||
}
|
||||
}
|
||||
} else if (dragState.isDragging && dragState.detectedDropZones?.best?.adjustment) {
|
||||
// 드래그 중이지만 이 컴포넌트는 드래그되지 않는 경우
|
||||
// adjustment에서 이 컴포넌트가 영향받는지 체크
|
||||
const adjustment = dragState.detectedDropZones.best.adjustment;
|
||||
const adjustedComp = adjustment.adjustedComponents.find((c) => c.id === component.id);
|
||||
|
||||
if (adjustedComp) {
|
||||
// 이 컴포넌트가 조정되었으면 실시간 미리보기 적용
|
||||
const resizeInfo = adjustment.resizedComponents.find((r) => r.id === component.id);
|
||||
|
||||
displayComponent = {
|
||||
...adjustedComp,
|
||||
style: {
|
||||
...component.style,
|
||||
transition: "all 0.2s ease-out", // 부드러운 애니메이션
|
||||
opacity: 0.9, // 약간 투명하게 (미리보기임을 표시)
|
||||
},
|
||||
};
|
||||
|
||||
if (resizeInfo) {
|
||||
console.log(`🎨 실시간 미리보기: ${component.id}`, {
|
||||
oldColumns: resizeInfo.oldColumns,
|
||||
newColumns: resizeInfo.newColumns,
|
||||
oldWidth: component.size.width,
|
||||
newWidth: displayComponent.size.width,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 파일 상태도 key에 포함하여 실시간 리렌더링
|
||||
|
|
|
|||
|
|
@ -0,0 +1,897 @@
|
|||
/**
|
||||
* 슬롯 기반 레이아웃 조정 로직
|
||||
*
|
||||
* 핵심 기능:
|
||||
* 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 },
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
/**
|
||||
* 슬롯 기반 레이아웃 시스템 - 메인 인터페이스
|
||||
*
|
||||
* ScreenDesigner에서 사용하는 주요 함수들을 제공
|
||||
*/
|
||||
|
||||
import { ComponentData, GridSettings, Position } from "@/types/screen";
|
||||
import {
|
||||
SlotMap,
|
||||
GridInfo,
|
||||
createGridInfo,
|
||||
buildSlotMap,
|
||||
positionToSlot,
|
||||
positionToRow,
|
||||
getComponentColumns,
|
||||
slotToPosition,
|
||||
rowToPosition,
|
||||
} from "./slotCalculations";
|
||||
import { PlacementCheck, LayoutAdjustment, canPlaceInSlot, calculateSlotPlacement } from "./slotAdjustment";
|
||||
|
||||
/**
|
||||
* 드롭존 정보
|
||||
*/
|
||||
export interface DropZone {
|
||||
id: string;
|
||||
slot: number; // 시작 슬롯 (0-11)
|
||||
endSlot: number; // 종료 슬롯 (0-11)
|
||||
rowIndex: number;
|
||||
position: Position;
|
||||
width: number;
|
||||
height: number;
|
||||
placementCheck: PlacementCheck;
|
||||
adjustment?: LayoutAdjustment; // 배치 시 조정 정보
|
||||
}
|
||||
|
||||
/**
|
||||
* 드래그 중 감지된 드롭존 목록
|
||||
*/
|
||||
export interface DetectedDropZones {
|
||||
horizontal: DropZone | null; // 같은 행의 드롭존
|
||||
vertical: DropZone | null; // 다음 행의 드롭존
|
||||
best: DropZone | null; // 가장 적합한 드롭존
|
||||
}
|
||||
|
||||
/**
|
||||
* 드래그 위치에서 가장 가까운 슬롯 찾기
|
||||
*/
|
||||
export function findNearestSlot(
|
||||
dragPosition: Position,
|
||||
canvasWidth: number,
|
||||
gridSettings: GridSettings,
|
||||
): { slot: number; rowIndex: number; gridInfo: GridInfo } {
|
||||
const gridInfo = createGridInfo(canvasWidth, gridSettings);
|
||||
const slot = positionToSlot(dragPosition.x, gridInfo);
|
||||
const rowIndex = positionToRow(dragPosition.y, gridInfo);
|
||||
|
||||
console.log("🎯 가장 가까운 슬롯:", {
|
||||
dragPosition,
|
||||
slot,
|
||||
rowIndex,
|
||||
});
|
||||
|
||||
return { slot, rowIndex, gridInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
* 드래그 중 드롭존 감지
|
||||
*/
|
||||
export function detectDropZones(
|
||||
dragPosition: Position,
|
||||
draggedComponent: ComponentData,
|
||||
allComponents: ComponentData[],
|
||||
canvasWidth: number,
|
||||
gridSettings: GridSettings,
|
||||
minColumns: number = 2,
|
||||
): DetectedDropZones {
|
||||
console.log("🔍 드롭존 감지 시작:", {
|
||||
dragPosition,
|
||||
draggedComponentId: draggedComponent.id,
|
||||
draggedColumns: getComponentColumns(draggedComponent),
|
||||
});
|
||||
|
||||
// 드래그 중인 컴포넌트를 제외한 컴포넌트들
|
||||
const otherComponents = allComponents.filter((c) => c.id !== draggedComponent.id);
|
||||
|
||||
// 그리드 정보 생성
|
||||
const gridInfo = createGridInfo(canvasWidth, gridSettings);
|
||||
|
||||
// 슬롯 맵 생성
|
||||
const slotMap = buildSlotMap(otherComponents, gridInfo);
|
||||
|
||||
// 드래그 위치에서 가장 가까운 슬롯
|
||||
const { slot: targetSlot, rowIndex } = findNearestSlot(dragPosition, canvasWidth, gridSettings);
|
||||
|
||||
const draggedColumns = getComponentColumns(draggedComponent);
|
||||
|
||||
// 수평 드롭존 (같은 행)
|
||||
const horizontalDropZone = createDropZone(
|
||||
targetSlot,
|
||||
draggedColumns,
|
||||
draggedComponent.id,
|
||||
rowIndex,
|
||||
slotMap,
|
||||
otherComponents,
|
||||
gridInfo,
|
||||
gridSettings,
|
||||
minColumns,
|
||||
);
|
||||
|
||||
// 수직 드롭존 (다음 행)
|
||||
const verticalDropZone = createDropZone(
|
||||
targetSlot,
|
||||
draggedColumns,
|
||||
draggedComponent.id,
|
||||
rowIndex + 1,
|
||||
slotMap,
|
||||
otherComponents,
|
||||
gridInfo,
|
||||
gridSettings,
|
||||
minColumns,
|
||||
);
|
||||
|
||||
// 최적의 드롭존 선택
|
||||
const best = selectBestDropZone(horizontalDropZone, verticalDropZone);
|
||||
|
||||
console.log("✅ 드롭존 감지 완료:", {
|
||||
horizontal: horizontalDropZone ? "있음" : "없음",
|
||||
vertical: verticalDropZone ? "있음" : "없음",
|
||||
best: best?.id || "없음",
|
||||
});
|
||||
|
||||
return {
|
||||
horizontal: horizontalDropZone,
|
||||
vertical: verticalDropZone,
|
||||
best,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 드롭존 생성
|
||||
*/
|
||||
function createDropZone(
|
||||
targetSlot: number,
|
||||
draggedColumns: number,
|
||||
draggedComponentId: string,
|
||||
rowIndex: number,
|
||||
slotMap: SlotMap,
|
||||
allComponents: ComponentData[],
|
||||
gridInfo: GridInfo,
|
||||
gridSettings: GridSettings,
|
||||
minColumns: number,
|
||||
): DropZone | null {
|
||||
// 슬롯 범위 체크
|
||||
const endSlot = Math.min(11, targetSlot + draggedColumns - 1);
|
||||
|
||||
// 배치 가능 여부 체크
|
||||
const placementCheck = canPlaceInSlot(targetSlot, draggedColumns, rowIndex, slotMap, allComponents, minColumns);
|
||||
|
||||
if (!placementCheck.canPlace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 레이아웃 조정 계산
|
||||
const adjustment = calculateSlotPlacement(
|
||||
targetSlot,
|
||||
draggedColumns,
|
||||
draggedComponentId,
|
||||
rowIndex,
|
||||
slotMap,
|
||||
allComponents,
|
||||
gridInfo,
|
||||
gridSettings,
|
||||
minColumns,
|
||||
);
|
||||
|
||||
if (!adjustment.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 드롭존 위치 및 크기 계산
|
||||
const x = slotToPosition(targetSlot, gridInfo);
|
||||
const y = rowToPosition(rowIndex, gridInfo);
|
||||
const width = draggedColumns * gridInfo.columnWidth + (draggedColumns - 1) * gridInfo.gap;
|
||||
const height = 100; // 기본 높이
|
||||
|
||||
return {
|
||||
id: `dropzone-${rowIndex}-${targetSlot}`,
|
||||
slot: targetSlot,
|
||||
endSlot,
|
||||
rowIndex,
|
||||
position: { x, y, z: 0 },
|
||||
width,
|
||||
height,
|
||||
placementCheck,
|
||||
adjustment,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 최적의 드롭존 선택
|
||||
*/
|
||||
function selectBestDropZone(horizontal: DropZone | null, vertical: DropZone | null): DropZone | null {
|
||||
// 수평 드롭존 우선 (같은 행에 배치)
|
||||
if (horizontal) {
|
||||
// 빈 공간에 배치 가능하면 최우선
|
||||
if (horizontal.placementCheck.strategy === "EMPTY_SPACE") {
|
||||
console.log("🏆 최적 드롭존: 수평 (빈 공간)");
|
||||
return horizontal;
|
||||
}
|
||||
|
||||
// 축소로 배치 가능하면 그 다음 우선
|
||||
if (horizontal.placementCheck.strategy === "SHRINK_COMPONENTS") {
|
||||
console.log("🏆 최적 드롭존: 수평 (축소)");
|
||||
return horizontal;
|
||||
}
|
||||
}
|
||||
|
||||
// 수직 드롭존 (다음 행으로)
|
||||
if (vertical) {
|
||||
console.log("🏆 최적 드롭존: 수직 (다음 행)");
|
||||
return vertical;
|
||||
}
|
||||
|
||||
// 수평 드롭존 (이동 전략)
|
||||
if (horizontal) {
|
||||
console.log("🏆 최적 드롭존: 수평 (이동)");
|
||||
return horizontal;
|
||||
}
|
||||
|
||||
console.log("❌ 적합한 드롭존 없음");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 드롭존 적용 (실제 레이아웃 업데이트)
|
||||
*/
|
||||
export function applyDropZone(
|
||||
dropZone: DropZone,
|
||||
draggedComponent: ComponentData,
|
||||
allComponents: ComponentData[],
|
||||
): ComponentData[] {
|
||||
console.log("🎯 드롭존 적용:", {
|
||||
dropZoneId: dropZone.id,
|
||||
strategy: dropZone.placementCheck.strategy,
|
||||
draggedComponentId: draggedComponent.id,
|
||||
});
|
||||
|
||||
if (!dropZone.adjustment) {
|
||||
console.error("❌ 조정 정보 없음");
|
||||
return allComponents;
|
||||
}
|
||||
|
||||
const { adjustment } = dropZone;
|
||||
|
||||
// adjustment.adjustedComponents는 드래그 중인 컴포넌트를 제외한 다른 컴포넌트들만 포함
|
||||
// 따라서 드래그된 컴포넌트를 추가해야 함
|
||||
let updatedComponents = [...adjustment.adjustedComponents];
|
||||
|
||||
// 드래그된 컴포넌트가 이미 있는지 확인
|
||||
const draggedIndex = updatedComponents.findIndex((c) => c.id === draggedComponent.id);
|
||||
|
||||
const draggedColumns = getComponentColumns(draggedComponent);
|
||||
const updatedDraggedComponent = {
|
||||
...draggedComponent,
|
||||
position: adjustment.placement,
|
||||
gridColumns: draggedColumns,
|
||||
columnSpan: draggedColumns,
|
||||
} as ComponentData;
|
||||
|
||||
if (draggedIndex !== -1) {
|
||||
// 이미 있으면 업데이트
|
||||
updatedComponents[draggedIndex] = updatedDraggedComponent;
|
||||
console.log("✅ 드래그 컴포넌트 업데이트 (기존):", {
|
||||
id: draggedComponent.id,
|
||||
position: adjustment.placement,
|
||||
columns: draggedColumns,
|
||||
});
|
||||
} else {
|
||||
// 없으면 추가
|
||||
updatedComponents.push(updatedDraggedComponent);
|
||||
console.log("✅ 드래그 컴포넌트 추가 (신규):", {
|
||||
id: draggedComponent.id,
|
||||
position: adjustment.placement,
|
||||
columns: draggedColumns,
|
||||
totalComponents: updatedComponents.length,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ 드롭존 적용 완료:", {
|
||||
totalComponents: updatedComponents.length,
|
||||
resized: adjustment.resizedComponents.length,
|
||||
moved: adjustment.movedComponents.length,
|
||||
});
|
||||
|
||||
return updatedComponents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 속성 패널에서 컬럼 수 변경 시 레이아웃 조정
|
||||
*/
|
||||
export function adjustLayoutOnColumnChange(
|
||||
targetComponentId: string,
|
||||
newColumns: number,
|
||||
allComponents: ComponentData[],
|
||||
canvasWidth: number,
|
||||
gridSettings: GridSettings,
|
||||
minColumns: number = 2,
|
||||
): ComponentData[] {
|
||||
console.log("🔧 컬럼 수 변경 레이아웃 조정:", {
|
||||
targetComponentId,
|
||||
newColumns,
|
||||
});
|
||||
|
||||
// 대상 컴포넌트 찾기
|
||||
const targetComponent = allComponents.find((c) => c.id === targetComponentId);
|
||||
if (!targetComponent) {
|
||||
console.error("❌ 대상 컴포넌트 없음");
|
||||
return allComponents;
|
||||
}
|
||||
|
||||
// 그리드 정보 생성
|
||||
const gridInfo = createGridInfo(canvasWidth, gridSettings);
|
||||
|
||||
// 현재 위치의 슬롯 계산
|
||||
const targetSlot = positionToSlot(targetComponent.position.x, gridInfo);
|
||||
const rowIndex = positionToRow(targetComponent.position.y, gridInfo);
|
||||
|
||||
// 다른 컴포넌트들로 슬롯 맵 생성
|
||||
const otherComponents = allComponents.filter((c) => c.id !== targetComponentId);
|
||||
const slotMap = buildSlotMap(otherComponents, gridInfo);
|
||||
|
||||
// 레이아웃 조정 계산
|
||||
const adjustment = calculateSlotPlacement(
|
||||
targetSlot,
|
||||
newColumns,
|
||||
targetComponentId,
|
||||
rowIndex,
|
||||
slotMap,
|
||||
allComponents,
|
||||
gridInfo,
|
||||
gridSettings,
|
||||
minColumns,
|
||||
);
|
||||
|
||||
if (!adjustment.success) {
|
||||
console.error("❌ 레이아웃 조정 실패");
|
||||
return allComponents;
|
||||
}
|
||||
|
||||
// 대상 컴포넌트 업데이트
|
||||
let updatedComponents = [...adjustment.adjustedComponents];
|
||||
const targetIndex = updatedComponents.findIndex((c) => c.id === targetComponentId);
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
const newWidth = newColumns * gridInfo.columnWidth + (newColumns - 1) * gridInfo.gap;
|
||||
|
||||
updatedComponents[targetIndex] = {
|
||||
...updatedComponents[targetIndex],
|
||||
gridColumns: newColumns,
|
||||
columnSpan: newColumns,
|
||||
size: {
|
||||
...updatedComponents[targetIndex].size,
|
||||
width: newWidth,
|
||||
},
|
||||
} as ComponentData;
|
||||
|
||||
console.log("✅ 대상 컴포넌트 업데이트:", {
|
||||
id: targetComponentId,
|
||||
newColumns,
|
||||
newWidth,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ 레이아웃 조정 완료:", {
|
||||
resized: adjustment.resizedComponents.length,
|
||||
moved: adjustment.movedComponents.length,
|
||||
});
|
||||
|
||||
return updatedComponents;
|
||||
}
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* 슬롯 기반 레이아웃 계산 유틸리티
|
||||
*
|
||||
* 핵심 개념:
|
||||
* - 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<string>();
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue