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

256 lines
7.2 KiB
TypeScript

/**
* 🔄 Width를 컬럼 스팬으로 변환하는 마이그레이션 유틸리티
*
* 기존 픽셀 기반 width 값을 새로운 그리드 시스템의 컬럼 스팬으로 변환
*/
import { ColumnSpanPreset, COLUMN_SPAN_VALUES, getColumnSpanValue } from "@/lib/constants/columnSpans";
import { ComponentData, LayoutData } from "@/types/screen";
/**
* 픽셀 width를 가장 가까운 ColumnSpanPreset으로 변환
*
* @param width 픽셀 너비
* @param canvasWidth 캔버스 전체 너비 (기본: 1920px)
* @returns 가장 가까운 컬럼 스팬 프리셋
*/
export function convertWidthToColumnSpan(width: number, canvasWidth: number = 1920): ColumnSpanPreset {
if (width <= 0 || canvasWidth <= 0) {
return "half"; // 기본값
}
const percentage = (width / canvasWidth) * 100;
// 각 프리셋의 백분율 계산
const presetPercentages: Array<[ColumnSpanPreset, number]> = [
["full", 100],
["threeQuarters", 75],
["twoThirds", 67],
["half", 50],
["third", 33],
["quarter", 25],
["label", 25],
["input", 75],
["small", 17],
["medium", 33],
["large", 67],
];
// 가장 가까운 값 찾기
let closestPreset: ColumnSpanPreset = "half";
let minDiff = Infinity;
for (const [preset, presetPercentage] of presetPercentages) {
const diff = Math.abs(percentage - presetPercentage);
if (diff < minDiff) {
minDiff = diff;
closestPreset = preset;
}
}
return closestPreset;
}
/**
* Y 좌표를 기준으로 행 인덱스 계산
*
* @param components 컴포넌트 배열
* @param threshold 같은 행으로 간주할 Y 좌표 차이 (기본: 50px)
* @returns 행 인덱스가 추가된 컴포넌트 배열
*/
export function calculateRowIndices(components: ComponentData[], threshold: number = 50): ComponentData[] {
if (components.length === 0) return [];
// Y 좌표로 정렬
const sorted = [...components].sort((a, b) => a.position.y - b.position.y);
let currentRowIndex = 0;
let currentY = sorted[0]?.position.y ?? 0;
return sorted.map((component) => {
if (Math.abs(component.position.y - currentY) > threshold) {
currentRowIndex++;
currentY = component.position.y;
}
return {
...component,
gridRowIndex: currentRowIndex,
};
});
}
/**
* 같은 행 내에서 X 좌표로 시작 컬럼 계산
*
* @param components 컴포넌트 배열 (gridRowIndex 필요)
* @returns 시작 컬럼이 추가된 컴포넌트 배열
*/
export function calculateColumnStarts(components: ComponentData[]): ComponentData[] {
// 행별로 그룹화
const rowGroups = new Map<number, ComponentData[]>();
for (const component of components) {
const rowIndex = component.gridRowIndex ?? 0;
if (!rowGroups.has(rowIndex)) {
rowGroups.set(rowIndex, []);
}
rowGroups.get(rowIndex)!.push(component);
}
// 각 행 내에서 X 좌표로 정렬하고 시작 컬럼 계산
const result: ComponentData[] = [];
for (const [rowIndex, rowComponents] of rowGroups) {
// X 좌표로 정렬
const sorted = rowComponents.sort((a, b) => a.position.x - b.position.x);
let currentColumn = 1;
for (const component of sorted) {
const columnSpan = component.gridColumnSpan || "half";
const spanValue = getColumnSpanValue(columnSpan);
// 현재 컬럼이 12를 넘으면 다음 줄로 (실제로는 같은 행이지만 자동 줄바꿈)
if (currentColumn + spanValue > 13) {
currentColumn = 1;
}
result.push({
...component,
gridColumnStart: currentColumn,
});
// 다음 컴포넌트는 현재 컴포넌트 뒤에 배치
currentColumn += spanValue;
}
}
return result;
}
/**
* 컴포넌트 배열에서 width를 gridColumnSpan으로 일괄 변환
*
* @param components 컴포넌트 배열
* @param canvasWidth 캔버스 너비 (기본: 1920px)
* @returns gridColumnSpan이 추가된 컴포넌트 배열
*/
export function migrateComponentsToColumnSpan(
components: ComponentData[],
canvasWidth: number = 1920,
): ComponentData[] {
return components.map((component) => {
// 이미 gridColumnSpan이 있으면 유지
if (component.gridColumnSpan) {
return component;
}
// width를 컬럼 스팬으로 변환
const gridColumnSpan = convertWidthToColumnSpan(component.size.width, canvasWidth);
return {
...component,
gridColumnSpan,
gridRowIndex: component.gridRowIndex ?? 0, // 초기값
};
});
}
/**
* 전체 레이아웃 마이그레이션
*
* @param layout 기존 레이아웃 데이터
* @param canvasWidth 캔버스 너비 (기본: 1920px)
* @returns 새로운 그리드 시스템으로 변환된 레이아웃
*/
export function migrateLayoutToGridSystem(layout: LayoutData, canvasWidth: number = 1920): LayoutData {
// 1단계: width를 gridColumnSpan으로 변환
let migratedComponents = migrateComponentsToColumnSpan(layout.components, canvasWidth);
// 2단계: Y 좌표로 행 인덱스 계산
migratedComponents = calculateRowIndices(migratedComponents);
// 3단계: 같은 행 내에서 X 좌표로 gridColumnStart 계산
migratedComponents = calculateColumnStarts(migratedComponents);
return {
...layout,
components: migratedComponents,
};
}
/**
* 단일 컴포넌트 마이그레이션
*
* @param component 기존 컴포넌트
* @param canvasWidth 캔버스 너비
* @returns 마이그레이션된 컴포넌트
*/
export function migrateComponent(component: ComponentData, canvasWidth: number = 1920): ComponentData {
// 이미 그리드 속성이 있으면 그대로 반환
if (component.gridColumnSpan && component.gridRowIndex !== undefined) {
return component;
}
const gridColumnSpan = component.gridColumnSpan || convertWidthToColumnSpan(component.size.width, canvasWidth);
return {
...component,
gridColumnSpan,
gridRowIndex: component.gridRowIndex ?? 0,
gridColumnStart: component.gridColumnStart,
};
}
/**
* 마이그레이션 필요 여부 확인
*
* @param layout 레이아웃 데이터
* @returns 마이그레이션 필요 여부
*/
export function needsMigration(layout: LayoutData): boolean {
return layout.components.some((c) => !c.gridColumnSpan || c.gridRowIndex === undefined);
}
/**
* 안전한 마이그레이션 (에러 처리 포함)
*
* @param layout 레이아웃 데이터
* @param canvasWidth 캔버스 너비
* @returns 마이그레이션된 레이아웃 또는 원본 (실패 시)
*/
export function safeMigrateLayout(layout: LayoutData, canvasWidth: number = 1920): LayoutData {
try {
if (!needsMigration(layout)) {
return layout;
}
return migrateLayoutToGridSystem(layout, canvasWidth);
} catch (error) {
console.error("❌ 마이그레이션 실패:", error);
console.warn("⚠️ 원본 레이아웃 반환 - 수동 확인 필요");
return layout;
}
}
/**
* 백업 데이터 생성
*
* @param layout 레이아웃 데이터
* @returns JSON 문자열
*/
export function createLayoutBackup(layout: LayoutData): string {
return JSON.stringify(layout, null, 2);
}
/**
* 백업에서 복원
*
* @param backupJson JSON 문자열
* @returns 레이아웃 데이터
*/
export function restoreFromBackup(backupJson: string): LayoutData {
return JSON.parse(backupJson);
}