ERP-node/frontend/contexts/report-designer/useLayerActions.ts

199 lines
7.9 KiB
TypeScript
Raw Normal View History

/**
* @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,
};
}