ERP-node/frontend/lib/utils/slotBasedLayout.ts

381 lines
10 KiB
TypeScript
Raw Normal View History

/**
* -
*
* 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;
}