229 lines
7.9 KiB
TypeScript
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,
|
|
};
|
|
}
|