/** * @module useAlignmentActions * @description 선택된 컴포넌트들의 위치/크기를 일괄 조정하는 정렬 기능을 제공한다. * - 엣지 정렬: 좌/우/상/하 기준으로 정렬 * - 중앙 정렬: 가로/세로 중앙으로 정렬 * - 균등 배치: 컴포넌트 간 간격을 동일하게 분배 (최소 3개 필요) * - 크기 동일화: 첫 번째 선택 컴포넌트 기준으로 너비/높이/둘 다 통일 * * 모든 함수는 2개 이상(균등 배치는 3개 이상) 선택된 경우에만 동작한다. */ import { useCallback } from "react"; import type { ComponentConfig } from "@/types/report"; import type { SetComponentsFn, ToastFunction } from "./internalTypes"; export interface AlignmentActions { alignLeft: () => void; alignRight: () => void; alignTop: () => void; alignBottom: () => void; alignCenterHorizontal: () => void; alignCenterVertical: () => void; distributeHorizontal: () => void; distributeVertical: () => void; makeSameWidth: () => void; makeSameHeight: () => void; makeSameSize: () => void; } interface AlignmentDeps { components: ComponentConfig[]; selectedComponentIds: string[]; setComponents: SetComponentsFn; toast: ToastFunction; } /** 선택된 컴포넌트를 업데이트 맵에서 찾아 교체하는 공통 헬퍼 */ function applyUpdates( prev: ComponentConfig[], updates: ComponentConfig[], ): ComponentConfig[] { const updateMap = new Map(updates.map((u) => [u.id, u])); return prev.map((c) => updateMap.get(c.id) ?? c); } export function useAlignmentActions({ components, selectedComponentIds, setComponents, toast, }: AlignmentDeps): AlignmentActions { /** 선택된 컴포넌트 배열 반환 (정렬 함수 내부 공통 사용) */ const getSelected = useCallback( () => components.filter((c) => selectedComponentIds.includes(c.id)), [components, selectedComponentIds], ); /** 모든 선택 컴포넌트의 x를 가장 왼쪽 x 기준으로 정렬 */ const alignLeft = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const minX = Math.min(...selected.map((c) => c.x)); setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, x: minX })))); toast({ title: "정렬 완료", description: "왼쪽 정렬되었습니다." }); }, [getSelected, setComponents, toast]); /** 모든 선택 컴포넌트의 오른쪽 엣지를 가장 오른쪽 엣지 기준으로 정렬 */ const alignRight = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const maxRight = Math.max(...selected.map((c) => c.x + c.width)); setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, x: maxRight - c.width })))); toast({ title: "정렬 완료", description: "오른쪽 정렬되었습니다." }); }, [getSelected, setComponents, toast]); /** 모든 선택 컴포넌트의 y를 가장 위쪽 y 기준으로 정렬 */ const alignTop = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const minY = Math.min(...selected.map((c) => c.y)); setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, y: minY })))); toast({ title: "정렬 완료", description: "위쪽 정렬되었습니다." }); }, [getSelected, setComponents, toast]); /** 모든 선택 컴포넌트의 아래쪽 엣지를 가장 아래쪽 엣지 기준으로 정렬 */ const alignBottom = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const maxBottom = Math.max(...selected.map((c) => c.y + c.height)); setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, y: maxBottom - c.height })))); toast({ title: "정렬 완료", description: "아래쪽 정렬되었습니다." }); }, [getSelected, setComponents, toast]); /** 선택 컴포넌트들의 수평 중앙을 전체 범위의 중앙으로 정렬 */ const alignCenterHorizontal = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const minX = Math.min(...selected.map((c) => c.x)); const maxRight = Math.max(...selected.map((c) => c.x + c.width)); const centerX = (minX + maxRight) / 2; setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, x: centerX - c.width / 2 })))); toast({ title: "정렬 완료", description: "가로 중앙 정렬되었습니다." }); }, [getSelected, setComponents, toast]); /** 선택 컴포넌트들의 수직 중앙을 전체 범위의 중앙으로 정렬 */ const alignCenterVertical = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const minY = Math.min(...selected.map((c) => c.y)); const maxBottom = Math.max(...selected.map((c) => c.y + c.height)); const centerY = (minY + maxBottom) / 2; setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, y: centerY - c.height / 2 })))); toast({ title: "정렬 완료", description: "세로 중앙 정렬되었습니다." }); }, [getSelected, setComponents, toast]); /** 3개 이상 선택된 컴포넌트를 가로 방향으로 균등하게 배치 */ const distributeHorizontal = useCallback(() => { const selected = getSelected(); if (selected.length < 3) return; const sorted = [...selected].sort((a, b) => a.x - b.x); const totalWidth = sorted.reduce((sum, c) => sum + c.width, 0); const span = sorted[sorted.length - 1].x + sorted[sorted.length - 1].width - sorted[0].x; const gap = (span - totalWidth) / (sorted.length - 1); let currentX = sorted[0].x; const updates = sorted.map((c) => { const updated = { ...c, x: currentX }; currentX += c.width + gap; return updated; }); setComponents((prev) => applyUpdates(prev, updates)); toast({ title: "정렬 완료", description: "가로 균등 배치되었습니다." }); }, [getSelected, setComponents, toast]); /** 3개 이상 선택된 컴포넌트를 세로 방향으로 균등하게 배치 */ const distributeVertical = useCallback(() => { const selected = getSelected(); if (selected.length < 3) return; const sorted = [...selected].sort((a, b) => a.y - b.y); const totalHeight = sorted.reduce((sum, c) => sum + c.height, 0); const span = sorted[sorted.length - 1].y + sorted[sorted.length - 1].height - sorted[0].y; const gap = (span - totalHeight) / (sorted.length - 1); let currentY = sorted[0].y; const updates = sorted.map((c) => { const updated = { ...c, y: currentY }; currentY += c.height + gap; return updated; }); setComponents((prev) => applyUpdates(prev, updates)); toast({ title: "정렬 완료", description: "세로 균등 배치되었습니다." }); }, [getSelected, setComponents, toast]); /** 선택된 모든 컴포넌트의 너비를 첫 번째 컴포넌트 너비로 통일 */ const makeSameWidth = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const targetWidth = selected[0].width; setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, width: targetWidth })))); toast({ title: "크기 조정 완료", description: "같은 너비로 조정되었습니다." }); }, [getSelected, setComponents, toast]); /** 선택된 모든 컴포넌트의 높이를 첫 번째 컴포넌트 높이로 통일 */ const makeSameHeight = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const targetHeight = selected[0].height; setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, height: targetHeight })))); toast({ title: "크기 조정 완료", description: "같은 높이로 조정되었습니다." }); }, [getSelected, setComponents, toast]); /** 선택된 모든 컴포넌트의 너비/높이를 첫 번째 컴포넌트 크기로 통일 */ const makeSameSize = useCallback(() => { const selected = getSelected(); if (selected.length < 2) return; const { width: targetWidth, height: targetHeight } = selected[0]; setComponents((prev) => applyUpdates(prev, selected.map((c) => ({ ...c, width: targetWidth, height: targetHeight })))); toast({ title: "크기 조정 완료", description: "같은 크기로 조정되었습니다." }); }, [getSelected, setComponents, toast]); return { alignLeft, alignRight, alignTop, alignBottom, alignCenterHorizontal, alignCenterVertical, distributeHorizontal, distributeVertical, makeSameWidth, makeSameHeight, makeSameSize, }; }