256 lines
7.2 KiB
TypeScript
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);
|
|
}
|