199 lines
7.9 KiB
TypeScript
199 lines
7.9 KiB
TypeScript
/**
|
|
* @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<string>();
|
|
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,
|
|
};
|
|
}
|