305 lines
7.3 KiB
TypeScript
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,
|
||
|
|
};
|
||
|
|
}
|