/** * @module useLayerActions * @description 캔버스에서 컴포넌트의 레이어 순서, 잠금, 그룹화를 관리한다. * * - 레이어 순서: bringToFront / sendToBack / bringForward / sendBackward * zIndex를 직접 조작하며, 앞/뒤 한 단계 이동 시 전체 컴포넌트를 재정렬하여 * zIndex가 1부터 연속되도록 유지한다. * * - 잠금 관리: toggleLock / lockComponents / unlockComponents * 잠긴 컴포넌트는 이동/편집/복사에서 제외된다. * * - 그룹화: groupComponents / ungroupComponents * groupId 필드로 그룹을 식별하며, 그룹 해제 시 해당 groupId를 가진 * 모든 컴포넌트에서 groupId를 제거한다. */ import { useCallback } from "react"; import type { ComponentConfig } from "@/types/report"; import type { SetComponentsFn, ToastFunction } from "./internalTypes"; export interface LayerActions { bringToFront: () => void; sendToBack: () => void; bringForward: () => void; sendBackward: () => void; toggleLock: () => void; lockComponents: () => void; unlockComponents: () => void; groupComponents: () => void; ungroupComponents: () => void; } interface LayerDeps { components: ComponentConfig[]; selectedComponentId: string | null; selectedComponentIds: string[]; setComponents: SetComponentsFn; toast: ToastFunction; } export function useLayerActions({ components, selectedComponentId, selectedComponentIds, setComponents, toast, }: LayerDeps): LayerActions { /** * 단일/다중 선택 모두를 지원하는 대상 ID 배열 반환. * selectedComponentIds가 비어 있으면 selectedComponentId를 사용한다. */ const getTargetIds = useCallback((): string[] => { if (selectedComponentIds.length > 0) return selectedComponentIds; return selectedComponentId ? [selectedComponentId] : []; }, [selectedComponentId, selectedComponentIds]); /** 선택된 컴포넌트를 모든 컴포넌트 중 가장 앞으로 이동 */ const bringToFront = useCallback(() => { const ids = getTargetIds(); if (ids.length === 0) return; const maxZIndex = Math.max(...components.map((c) => c.zIndex)); setComponents((prev) => prev.map((c) => (ids.includes(c.id) ? { ...c, zIndex: maxZIndex + 1 } : c)), ); toast({ title: "레이어 변경", description: "맨 앞으로 이동했습니다." }); }, [getTargetIds, components, setComponents, toast]); /** 선택된 컴포넌트를 모든 컴포넌트 중 가장 뒤로 이동 (최소 zIndex = 1) */ const sendToBack = useCallback(() => { const ids = getTargetIds(); if (ids.length === 0) return; const minZIndex = Math.min(...components.map((c) => c.zIndex)); setComponents((prev) => prev.map((c) => (ids.includes(c.id) ? { ...c, zIndex: Math.max(1, minZIndex - 1) } : c)), ); toast({ title: "레이어 변경", description: "맨 뒤로 이동했습니다." }); }, [getTargetIds, components, setComponents, toast]); /** 선택된 컴포넌트를 한 단계 앞으로 이동. 전체 zIndex를 재정렬하여 연속성 유지. */ const bringForward = useCallback(() => { const ids = getTargetIds(); if (ids.length === 0) return; setComponents((prev) => { const sorted = [...prev].sort((a, b) => a.zIndex - b.zIndex); const reindexed = sorted.map((c, i) => ({ ...c, zIndex: i })); return reindexed.map((c) => ids.includes(c.id) ? { ...c, zIndex: Math.min(c.zIndex + 1, reindexed.length - 1) } : c, ); }); toast({ title: "레이어 변경", description: "한 단계 앞으로 이동했습니다." }); }, [getTargetIds, setComponents, toast]); /** 선택된 컴포넌트를 한 단계 뒤로 이동. 전체 zIndex를 재정렬하여 연속성 유지. */ const sendBackward = useCallback(() => { const ids = getTargetIds(); if (ids.length === 0) return; setComponents((prev) => { const sorted = [...prev].sort((a, b) => a.zIndex - b.zIndex); const reindexed = sorted.map((c, i) => ({ ...c, zIndex: i + 1 })); return reindexed.map((c) => ids.includes(c.id) ? { ...c, zIndex: Math.max(c.zIndex - 1, 1) } : c, ); }); toast({ title: "레이어 변경", description: "한 단계 뒤로 이동했습니다." }); }, [getTargetIds, setComponents, toast]); /** 선택된 컴포넌트의 잠금 상태를 토글한다. */ const toggleLock = useCallback(() => { const ids = getTargetIds(); if (ids.length === 0) return; const isCurrentlyLocked = components.find((c) => ids.includes(c.id))?.locked === true; setComponents((prev) => prev.map((c) => (ids.includes(c.id) ? { ...c, locked: !c.locked } : c)), ); toast({ title: isCurrentlyLocked ? "잠금 해제" : "잠금 설정", description: isCurrentlyLocked ? "선택된 컴포넌트의 잠금이 해제되었습니다." : "선택된 컴포넌트가 잠겼습니다.", }); }, [getTargetIds, components, setComponents, toast]); /** 선택된 컴포넌트를 잠근다. */ const lockComponents = useCallback(() => { const ids = getTargetIds(); if (ids.length === 0) return; setComponents((prev) => prev.map((c) => (ids.includes(c.id) ? { ...c, locked: true } : c))); toast({ title: "잠금 설정", description: "선택된 컴포넌트가 잠겼습니다." }); }, [getTargetIds, setComponents, toast]); /** 선택된 컴포넌트의 잠금을 해제한다. */ const unlockComponents = useCallback(() => { const ids = getTargetIds(); if (ids.length === 0) return; setComponents((prev) => prev.map((c) => (ids.includes(c.id) ? { ...c, locked: false } : c))); toast({ title: "잠금 해제", description: "선택된 컴포넌트의 잠금이 해제되었습니다." }); }, [getTargetIds, setComponents, toast]); /** 2개 이상 선택된 컴포넌트에 동일한 groupId를 부여하여 그룹으로 묶는다. */ const groupComponents = useCallback(() => { if (selectedComponentIds.length < 2) { toast({ title: "그룹화 불가", description: "2개 이상의 컴포넌트를 선택해야 합니다.", variant: "destructive" }); return; } const newGroupId = `group_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; setComponents((prev) => prev.map((c) => (selectedComponentIds.includes(c.id) ? { ...c, groupId: newGroupId } : c)), ); toast({ title: "그룹화 완료", description: `${selectedComponentIds.length}개의 컴포넌트가 그룹화되었습니다.` }); }, [selectedComponentIds, setComponents, toast]); /** * 선택된 컴포넌트가 속한 그룹 전체를 해제한다. * 같은 groupId를 가진 컴포넌트 모두에서 groupId가 제거된다. */ const ungroupComponents = useCallback(() => { const ids = getTargetIds(); if (ids.length === 0) { toast({ title: "그룹 해제 불가", description: "그룹을 해제할 컴포넌트를 선택해주세요.", variant: "destructive" }); return; } const groupIds = new Set(); components.forEach((c) => { if (ids.includes(c.id) && c.groupId) groupIds.add(c.groupId); }); if (groupIds.size === 0) { toast({ title: "그룹 해제 불가", description: "선택된 컴포넌트 중 그룹화된 것이 없습니다.", variant: "destructive" }); return; } setComponents((prev) => prev.map((c) => { if (c.groupId && groupIds.has(c.groupId)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { groupId, ...rest } = c; return rest as ComponentConfig; } return c; }), ); toast({ title: "그룹 해제 완료", description: `${groupIds.size}개의 그룹이 해제되었습니다.` }); }, [getTargetIds, components, setComponents, toast]); return { bringToFront, sendToBack, bringForward, sendBackward, toggleLock, lockComponents, unlockComponents, groupComponents, ungroupComponents, }; }