/** * 화면 디자이너 정렬/배분/동일크기 유틸리티 * * 다중 선택된 컴포넌트에 대해 정렬, 균등 배분, 동일 크기 맞추기 기능을 제공합니다. */ import { ComponentData } from "@/types/screen"; // 정렬 모드 타입 export type AlignMode = "left" | "centerX" | "right" | "top" | "centerY" | "bottom"; // 배분 방향 타입 export type DistributeDirection = "horizontal" | "vertical"; // 크기 맞추기 모드 타입 export type MatchSizeMode = "width" | "height" | "both"; /** * 컴포넌트 정렬 * 선택된 컴포넌트들을 지정된 방향으로 정렬합니다. */ export function alignComponents( components: ComponentData[], selectedIds: string[], mode: AlignMode ): ComponentData[] { const selected = components.filter((c) => selectedIds.includes(c.id)); if (selected.length < 2) return components; let targetValue: number; switch (mode) { case "left": // 가장 왼쪽 x값으로 정렬 targetValue = Math.min(...selected.map((c) => c.position.x)); return components.map((c) => { if (!selectedIds.includes(c.id)) return c; return { ...c, position: { ...c.position, x: targetValue } }; }); case "right": // 가장 오른쪽 (x + width)로 정렬 targetValue = Math.max(...selected.map((c) => c.position.x + (c.size?.width || 100))); return components.map((c) => { if (!selectedIds.includes(c.id)) return c; const width = c.size?.width || 100; return { ...c, position: { ...c.position, x: targetValue - width } }; }); case "centerX": // 가로 중앙 정렬 (전체 범위의 중앙) { const minX = Math.min(...selected.map((c) => c.position.x)); const maxX = Math.max(...selected.map((c) => c.position.x + (c.size?.width || 100))); const centerX = (minX + maxX) / 2; return components.map((c) => { if (!selectedIds.includes(c.id)) return c; const width = c.size?.width || 100; return { ...c, position: { ...c.position, x: Math.round(centerX - width / 2) } }; }); } case "top": // 가장 위쪽 y값으로 정렬 targetValue = Math.min(...selected.map((c) => c.position.y)); return components.map((c) => { if (!selectedIds.includes(c.id)) return c; return { ...c, position: { ...c.position, y: targetValue } }; }); case "bottom": // 가장 아래쪽 (y + height)로 정렬 targetValue = Math.max(...selected.map((c) => c.position.y + (c.size?.height || 40))); return components.map((c) => { if (!selectedIds.includes(c.id)) return c; const height = c.size?.height || 40; return { ...c, position: { ...c.position, y: targetValue - height } }; }); case "centerY": // 세로 중앙 정렬 (전체 범위의 중앙) { const minY = Math.min(...selected.map((c) => c.position.y)); const maxY = Math.max(...selected.map((c) => c.position.y + (c.size?.height || 40))); const centerY = (minY + maxY) / 2; return components.map((c) => { if (!selectedIds.includes(c.id)) return c; const height = c.size?.height || 40; return { ...c, position: { ...c.position, y: Math.round(centerY - height / 2) } }; }); } default: return components; } } /** * 컴포넌트 균등 배분 * 선택된 컴포넌트들 간의 간격을 균등하게 분배합니다. * 최소 3개 이상의 컴포넌트가 필요합니다. */ export function distributeComponents( components: ComponentData[], selectedIds: string[], direction: DistributeDirection ): ComponentData[] { const selected = components.filter((c) => selectedIds.includes(c.id)); if (selected.length < 3) return components; if (direction === "horizontal") { // x 기준 정렬 const sorted = [...selected].sort((a, b) => a.position.x - b.position.x); const first = sorted[0]; const last = sorted[sorted.length - 1]; // 첫 번째 ~ 마지막 컴포넌트 사이의 총 공간 const totalSpace = last.position.x + (last.size?.width || 100) - first.position.x; // 컴포넌트들이 차지하는 총 너비 const totalComponentWidth = sorted.reduce((sum, c) => sum + (c.size?.width || 100), 0); // 균등 간격 const gap = (totalSpace - totalComponentWidth) / (sorted.length - 1); // ID -> 새 x 좌표 매핑 const newPositions = new Map(); let currentX = first.position.x; for (const comp of sorted) { newPositions.set(comp.id, Math.round(currentX)); currentX += (comp.size?.width || 100) + gap; } return components.map((c) => { if (!selectedIds.includes(c.id)) return c; const newX = newPositions.get(c.id); if (newX === undefined) return c; return { ...c, position: { ...c.position, x: newX } }; }); } else { // y 기준 정렬 const sorted = [...selected].sort((a, b) => a.position.y - b.position.y); const first = sorted[0]; const last = sorted[sorted.length - 1]; const totalSpace = last.position.y + (last.size?.height || 40) - first.position.y; const totalComponentHeight = sorted.reduce((sum, c) => sum + (c.size?.height || 40), 0); const gap = (totalSpace - totalComponentHeight) / (sorted.length - 1); const newPositions = new Map(); let currentY = first.position.y; for (const comp of sorted) { newPositions.set(comp.id, Math.round(currentY)); currentY += (comp.size?.height || 40) + gap; } return components.map((c) => { if (!selectedIds.includes(c.id)) return c; const newY = newPositions.get(c.id); if (newY === undefined) return c; return { ...c, position: { ...c.position, y: newY } }; }); } } /** * 컴포넌트 동일 크기 맞추기 * 선택된 컴포넌트들의 크기를 첫 번째 선택된 컴포넌트 기준으로 맞춥니다. */ export function matchComponentSize( components: ComponentData[], selectedIds: string[], mode: MatchSizeMode, referenceId?: string ): ComponentData[] { const selected = components.filter((c) => selectedIds.includes(c.id)); if (selected.length < 2) return components; // 기준 컴포넌트 (지정하지 않으면 첫 번째 선택된 컴포넌트) const reference = referenceId ? selected.find((c) => c.id === referenceId) || selected[0] : selected[0]; const refWidth = reference.size?.width || 100; const refHeight = reference.size?.height || 40; return components.map((c) => { if (!selectedIds.includes(c.id)) return c; const currentWidth = c.size?.width || 100; const currentHeight = c.size?.height || 40; let newWidth = currentWidth; let newHeight = currentHeight; if (mode === "width" || mode === "both") { newWidth = refWidth; } if (mode === "height" || mode === "both") { newHeight = refHeight; } return { ...c, size: { ...c.size, width: newWidth, height: newHeight, }, }; }); } /** * 컴포넌트 라벨 일괄 토글 * 모든 컴포넌트의 라벨 표시/숨기기를 토글합니다. * 숨겨진 라벨이 하나라도 있으면 모두 표시, 모두 표시되어 있으면 모두 숨기기 */ export function toggleAllLabels(components: ComponentData[], forceShow?: boolean): ComponentData[] { // 현재 라벨이 숨겨진(labelDisplay === false) 컴포넌트가 있는지 확인 const hasHiddenLabel = components.some( (c) => c.type === "widget" && (c.style as any)?.labelDisplay === false ); // forceShow가 지정되면 그 값 사용, 아니면 자동 판단 // 숨겨진 라벨이 있으면 모두 표시, 아니면 모두 숨기기 const shouldShow = forceShow !== undefined ? forceShow : hasHiddenLabel; return components.map((c) => { // 위젯 타입만 라벨 토글 대상 if (c.type !== "widget") return c; return { ...c, style: { ...(c.style || {}), labelDisplay: shouldShow, } as any, }; }); } /** * 컴포넌트 nudge (화살표 키 이동) * 선택된 컴포넌트를 지정 방향으로 이동합니다. */ export function nudgeComponents( components: ComponentData[], selectedIds: string[], direction: "up" | "down" | "left" | "right", distance: number = 1 // 기본 1px, Shift 누르면 10px ): ComponentData[] { const dx = direction === "left" ? -distance : direction === "right" ? distance : 0; const dy = direction === "up" ? -distance : direction === "down" ? distance : 0; return components.map((c) => { if (!selectedIds.includes(c.id)) return c; return { ...c, position: { ...c.position, x: Math.max(0, c.position.x + dx), y: Math.max(0, c.position.y + dy), }, }; }); }