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

305 lines
7.3 KiB
TypeScript

/**
* 슬롯 기반 레이아웃 계산 유틸리티
*
* 핵심 개념:
* - 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,
};
}