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

229 lines
7.9 KiB
TypeScript

/**
* @module usePageManager
* @description 리포트 레이아웃의 페이지 단위 CRUD 및 설정 변경을 담당한다.
* - 페이지 추가/삭제/복제/순서 변경
* - 현재 활성 페이지 전환 (selectPage)
* - 개별 페이지 설정(크기, 방향, 여백 등) 업데이트
* - 전체 페이지 공유 워터마크 설정
*
* 페이지 크기 변경 시 기존 컴포넌트 위치/크기를 비율에 따라 자동 재계산한다.
*/
import { useCallback } from "react";
import { v4 as uuidv4 } from "uuid";
import type { ReportLayoutConfig, ReportPage, WatermarkConfig } from "@/types/report";
import type { ToastFunction } from "./internalTypes";
import { generateComponentId } from "@/lib/report/constants";
export interface PageManagerActions {
addPage: (name?: string) => void;
deletePage: (pageId: string) => void;
duplicatePage: (pageId: string) => void;
reorderPages: (sourceIndex: number, targetIndex: number) => void;
selectPage: (pageId: string) => void;
updatePageSettings: (pageId: string, settings: Partial<ReportPage>) => void;
updateWatermark: (watermark: WatermarkConfig | undefined) => void;
}
interface PageManagerDeps {
layoutConfig: ReportLayoutConfig;
setLayoutConfig: React.Dispatch<React.SetStateAction<ReportLayoutConfig>>;
currentPageId: string | null;
setCurrentPageId: React.Dispatch<React.SetStateAction<string | null>>;
/** 페이지 전환 시 선택 상태를 초기화하는 콜백 */
clearSelection: () => void;
toast: ToastFunction;
}
/**
* 페이지 크기가 변경될 때 기존 컴포넌트의 위치/크기를 비율에 맞춰 재계산한다.
* 소수점 2자리까지만 유지하여 부동소수점 오차를 최소화한다.
*/
function recalculateComponentPositions(
components: ReportPage["components"],
oldWidth: number,
oldHeight: number,
newWidth: number,
newHeight: number,
): ReportPage["components"] {
if (oldWidth === newWidth && oldHeight === newHeight) return components;
const widthRatio = newWidth / oldWidth;
const heightRatio = newHeight / oldHeight;
return components.map((comp) => ({
...comp,
x: Math.round(comp.x * widthRatio * 100) / 100,
y: Math.round(comp.y * heightRatio * 100) / 100,
width: Math.round(comp.width * widthRatio * 100) / 100,
height: Math.round(comp.height * heightRatio * 100) / 100,
}));
}
export function usePageManager({
layoutConfig,
setLayoutConfig,
currentPageId,
setCurrentPageId,
clearSelection,
toast,
}: PageManagerDeps): PageManagerActions {
/** 새 페이지를 마지막에 추가하고 해당 페이지로 자동 전환한다. */
const addPage = useCallback(
(name?: string) => {
const newPageId = uuidv4();
const newPage: ReportPage = {
page_id: newPageId,
page_name: name ?? `페이지 ${layoutConfig.pages.length + 1}`,
page_order: layoutConfig.pages.length,
width: 210,
height: 297,
orientation: "portrait",
margins: { top: 20, bottom: 20, left: 20, right: 20 },
background_color: "#ffffff",
components: [],
};
setLayoutConfig((prev) => ({ pages: [...prev.pages, newPage] }));
setCurrentPageId(newPageId);
toast({ title: "페이지 추가", description: `${newPage.page_name}이(가) 추가되었습니다.` });
},
[layoutConfig.pages.length, setLayoutConfig, setCurrentPageId, toast],
);
/**
* 지정한 페이지를 삭제한다.
* 마지막 페이지는 삭제 불가. 현재 페이지 삭제 시 나머지 첫 번째 페이지로 이동한다.
*/
const deletePage = useCallback(
(pageId: string) => {
if (layoutConfig.pages.length <= 1) {
toast({ title: "삭제 불가", description: "최소 1개의 페이지는 필요합니다.", variant: "destructive" });
return;
}
const pageIndex = layoutConfig.pages.findIndex((p) => p.page_id === pageId);
if (pageIndex === -1) return;
setLayoutConfig((prev) => ({
pages: prev.pages
.filter((p) => p.page_id !== pageId)
.map((p, idx) => ({ ...p, page_order: idx })),
}));
if (currentPageId === pageId) {
const remaining = layoutConfig.pages.filter((p) => p.page_id !== pageId);
setCurrentPageId(remaining[0]?.page_id ?? null);
}
toast({ title: "페이지 삭제", description: "페이지가 삭제되었습니다." });
},
[layoutConfig.pages, currentPageId, setLayoutConfig, setCurrentPageId, toast],
);
/** 지정한 페이지를 복제하고 복제된 페이지로 이동한다. 컴포넌트에도 새 ID가 부여된다. */
const duplicatePage = useCallback(
(pageId: string) => {
const sourcePage = layoutConfig.pages.find((p) => p.page_id === pageId);
if (!sourcePage) return;
const newPageId = uuidv4();
const newPage: ReportPage = {
...sourcePage,
page_id: newPageId,
page_name: `${sourcePage.page_name} (복사)`,
page_order: layoutConfig.pages.length,
components: sourcePage.components.map((comp) => ({
...comp,
id: generateComponentId(),
})),
};
setLayoutConfig((prev) => ({ pages: [...prev.pages, newPage] }));
setCurrentPageId(newPageId);
toast({ title: "페이지 복제", description: `${newPage.page_name}이(가) 생성되었습니다.` });
},
[layoutConfig.pages, setLayoutConfig, setCurrentPageId, toast],
);
/** 드래그&드롭으로 페이지 순서를 변경한다. page_order는 인덱스와 동기화된다. */
const reorderPages = useCallback(
(sourceIndex: number, targetIndex: number) => {
if (sourceIndex === targetIndex) return;
const newPages = [...layoutConfig.pages];
const [movedPage] = newPages.splice(sourceIndex, 1);
newPages.splice(targetIndex, 0, movedPage);
newPages.forEach((page, idx) => { page.page_order = idx; });
setLayoutConfig({ pages: newPages });
},
[layoutConfig.pages, setLayoutConfig],
);
/** 지정한 페이지로 전환하고, 기존에 선택된 컴포넌트를 초기화한다. */
const selectPage = useCallback(
(pageId: string) => {
setCurrentPageId(pageId);
clearSelection();
},
[setCurrentPageId, clearSelection],
);
/**
* 페이지의 크기, 방향, 여백, 배경색 등을 업데이트한다.
* 페이지 크기가 변경될 경우 기존 컴포넌트 위치/크기를 비율에 따라 재계산한다.
*/
const updatePageSettings = useCallback(
(pageId: string, settings: Partial<ReportPage>) => {
setLayoutConfig((prev) => {
const targetPage = prev.pages.find((p) => p.page_id === pageId);
if (!targetPage) return prev;
const isWidthChanging = settings.width !== undefined && settings.width !== targetPage.width;
const isHeightChanging = settings.height !== undefined && settings.height !== targetPage.height;
const updatedComponents =
isWidthChanging || isHeightChanging
? recalculateComponentPositions(
targetPage.components,
targetPage.width,
targetPage.height,
settings.width ?? targetPage.width,
settings.height ?? targetPage.height,
)
: targetPage.components;
return {
...prev,
pages: prev.pages.map((page) =>
page.page_id === pageId ? { ...page, ...settings, components: updatedComponents } : page,
),
};
});
},
[setLayoutConfig],
);
/**
* 전체 페이지에 공유되는 워터마크를 설정한다.
* undefined를 전달하면 워터마크가 제거된다.
*/
const updateWatermark = useCallback(
(watermark: WatermarkConfig | undefined) => {
setLayoutConfig((prev) => ({ ...prev, watermark }));
},
[setLayoutConfig],
);
return {
addPage,
deletePage,
duplicatePage,
reorderPages,
selectPage,
updatePageSettings,
updateWatermark,
};
}