381 lines
10 KiB
TypeScript
381 lines
10 KiB
TypeScript
|
|
/**
|
||
|
|
* 슬롯 기반 레이아웃 시스템 - 메인 인터페이스
|
||
|
|
*
|
||
|
|
* 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;
|
||
|
|
}
|