ERP-node/frontend/components/pop/designer/utils/gridUtils.ts

253 lines
7.2 KiB
TypeScript

// POP 그리드 유틸리티 (리플로우, 겹침 해결, 위치 계산)
import {
PopGridPosition,
GridMode,
GRID_BREAKPOINTS,
PopLayoutData,
} from "../types/pop-layout";
// ========================================
// 리플로우 (행 그룹 기반 자동 재배치)
// ========================================
/**
* 행 그룹 리플로우
*
* CSS Flexbox wrap 원리로 자동 재배치한다.
* 1. 같은 행의 컴포넌트를 한 묶음으로 처리
* 2. 최소 2x2칸 보장 (터치 가능한 최소 크기)
* 3. 한 줄에 안 들어가면 다음 줄로 줄바꿈 (숨김 없음)
* 4. 설계 너비의 50% 이상인 컴포넌트는 전체 너비 확장
* 5. 리플로우 후 겹침 해결
*/
export function convertAndResolvePositions(
components: Array<{ id: string; position: PopGridPosition }>,
targetMode: GridMode
): Array<{ id: string; position: PopGridPosition }> {
if (components.length === 0) return [];
const targetColumns = GRID_BREAKPOINTS[targetMode].columns;
const designColumns = GRID_BREAKPOINTS["tablet_landscape"].columns;
if (targetColumns >= designColumns) {
return components.map(c => ({ id: c.id, position: { ...c.position } }));
}
const ratio = targetColumns / designColumns;
const MIN_COL_SPAN = 2;
const MIN_ROW_SPAN = 2;
const rowGroups: Record<number, Array<{ id: string; position: PopGridPosition }>> = {};
components.forEach(comp => {
const r = comp.position.row;
if (!rowGroups[r]) rowGroups[r] = [];
rowGroups[r].push(comp);
});
const placed: Array<{ id: string; position: PopGridPosition }> = [];
let outputRow = 1;
const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b);
for (const rowKey of sortedRows) {
const group = rowGroups[rowKey].sort((a, b) => a.position.col - b.position.col);
let currentCol = 1;
let maxRowSpanInLine = 0;
for (const comp of group) {
const pos = comp.position;
const isMainContent = pos.colSpan >= designColumns * 0.5;
let scaledSpan = isMainContent
? targetColumns
: Math.max(MIN_COL_SPAN, Math.round(pos.colSpan * ratio));
scaledSpan = Math.min(scaledSpan, targetColumns);
const scaledRowSpan = Math.max(MIN_ROW_SPAN, pos.rowSpan);
if (currentCol + scaledSpan - 1 > targetColumns) {
outputRow += Math.max(1, maxRowSpanInLine);
currentCol = 1;
maxRowSpanInLine = 0;
}
placed.push({
id: comp.id,
position: {
col: currentCol,
row: outputRow,
colSpan: scaledSpan,
rowSpan: scaledRowSpan,
},
});
maxRowSpanInLine = Math.max(maxRowSpanInLine, scaledRowSpan);
currentCol += scaledSpan;
}
outputRow += Math.max(1, maxRowSpanInLine);
}
return resolveOverlaps(placed, targetColumns);
}
// ========================================
// 겹침 감지 및 해결
// ========================================
export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean {
const aColEnd = a.col + a.colSpan - 1;
const bColEnd = b.col + b.colSpan - 1;
const colOverlap = !(aColEnd < b.col || bColEnd < a.col);
const aRowEnd = a.row + a.rowSpan - 1;
const bRowEnd = b.row + b.rowSpan - 1;
const rowOverlap = !(aRowEnd < b.row || bRowEnd < a.row);
return colOverlap && rowOverlap;
}
export function resolveOverlaps(
positions: Array<{ id: string; position: PopGridPosition }>,
columns: number
): Array<{ id: string; position: PopGridPosition }> {
const sorted = [...positions].sort((a, b) =>
a.position.row - b.position.row || a.position.col - b.position.col
);
const resolved: Array<{ id: string; position: PopGridPosition }> = [];
sorted.forEach((item) => {
let { row, col, colSpan, rowSpan } = item.position;
if (col + colSpan - 1 > columns) {
colSpan = columns - col + 1;
}
let attempts = 0;
while (attempts < 100) {
const currentPos: PopGridPosition = { col, row, colSpan, rowSpan };
const hasOverlap = resolved.some(r => isOverlapping(currentPos, r.position));
if (!hasOverlap) break;
row++;
attempts++;
}
resolved.push({
id: item.id,
position: { col, row, colSpan, rowSpan },
});
});
return resolved;
}
// ========================================
// 자동 배치 (새 컴포넌트 드롭 시)
// ========================================
export function findNextEmptyPosition(
existingPositions: PopGridPosition[],
colSpan: number,
rowSpan: number,
columns: number
): PopGridPosition {
let row = 1;
let col = 1;
let attempts = 0;
while (attempts < 1000) {
const candidatePos: PopGridPosition = { col, row, colSpan, rowSpan };
if (col + colSpan - 1 > columns) {
col = 1;
row++;
continue;
}
const hasOverlap = existingPositions.some(pos => isOverlapping(candidatePos, pos));
if (!hasOverlap) return candidatePos;
col++;
if (col + colSpan - 1 > columns) {
col = 1;
row++;
}
attempts++;
}
return { col: 1, row: row + 1, colSpan, rowSpan };
}
// ========================================
// 유효 위치 계산
// ========================================
/**
* 컴포넌트의 유효 위치를 계산한다.
* 우선순위: 1. 오버라이드 → 2. 자동 재배치 → 3. 원본 위치
*/
function getEffectiveComponentPosition(
componentId: string,
layout: PopLayoutData,
mode: GridMode,
autoResolvedPositions?: Array<{ id: string; position: PopGridPosition }>
): PopGridPosition | null {
const component = layout.components[componentId];
if (!component) return null;
const override = layout.overrides?.[mode]?.positions?.[componentId];
if (override) {
return { ...component.position, ...override };
}
if (autoResolvedPositions) {
const autoResolved = autoResolvedPositions.find(p => p.id === componentId);
if (autoResolved) return autoResolved.position;
} else {
const componentsArray = Object.entries(layout.components).map(([id, comp]) => ({
id,
position: comp.position,
}));
const resolved = convertAndResolvePositions(componentsArray, mode);
const autoResolved = resolved.find(p => p.id === componentId);
if (autoResolved) return autoResolved.position;
}
return component.position;
}
/**
* 모든 컴포넌트의 유효 위치를 일괄 계산한다.
* 숨김 처리된 컴포넌트는 제외.
*/
export function getAllEffectivePositions(
layout: PopLayoutData,
mode: GridMode
): Map<string, PopGridPosition> {
const result = new Map<string, PopGridPosition>();
const hiddenIds = layout.overrides?.[mode]?.hidden || [];
const componentsArray = Object.entries(layout.components).map(([id, comp]) => ({
id,
position: comp.position,
}));
const autoResolvedPositions = convertAndResolvePositions(componentsArray, mode);
Object.keys(layout.components).forEach(componentId => {
if (hiddenIds.includes(componentId)) return;
const position = getEffectiveComponentPosition(
componentId, layout, mode, autoResolvedPositions
);
if (position) {
result.set(componentId, position);
}
});
return result;
}