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