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

201 lines
7.6 KiB
TypeScript
Raw Normal View History

/**
* @module useUIState
* @description UI .
* - //눈금자: 캔버스
* - (zoom): /
* - 가이드라인: 드래그
* - /펼치기: ··
* - 모달: 더블클릭 ID
*/
import { useState, useCallback } from "react";
import type { ComponentConfig } from "@/types/report";
import { MM_TO_PX } from "@/lib/report/constants";
/** 캔버스에 표시할 정렬 가이드선 좌표 (px) */
interface AlignmentGuides {
vertical: number[];
horizontal: number[];
}
/** 정렬 가이드 계산에 필요한 캔버스 치수 (mm 단위) */
interface CanvasDimensions {
canvasWidth: number;
canvasHeight: number;
}
export interface UIStateResult {
gridSize: number;
setGridSize: (size: number) => void;
showGrid: boolean;
setShowGrid: (show: boolean) => void;
snapToGrid: boolean;
setSnapToGrid: (snap: boolean) => void;
snapValueToGrid: (value: number) => number;
showRuler: boolean;
setShowRuler: (show: boolean) => void;
zoom: number;
setZoom: (zoom: number) => void;
fitTrigger: number;
fitToScreen: () => void;
alignmentGuides: AlignmentGuides;
calculateAlignmentGuides: (draggingId: string, x: number, y: number, width: number, height: number) => void;
clearAlignmentGuides: () => void;
isPageListCollapsed: boolean;
setIsPageListCollapsed: (v: boolean) => void;
isLeftPanelCollapsed: boolean;
setIsLeftPanelCollapsed: (v: boolean) => void;
isRightPanelCollapsed: boolean;
setIsRightPanelCollapsed: (v: boolean) => void;
componentModalTargetId: string | null;
openComponentModal: (componentId: string) => void;
closeComponentModal: () => void;
}
/** 드래그 중 정렬 가이드를 계산할 때 사용하는 오차 허용 범위 (px) */
const ALIGNMENT_THRESHOLD_PX = 5;
export function useUIState(
/** 정렬 가이드 계산에 필요한 현재 페이지 컴포넌트 목록 */
components: ComponentConfig[],
/** 정렬 가이드 계산에 필요한 캔버스 치수 */
dimensions: CanvasDimensions,
): UIStateResult {
const { canvasWidth, canvasHeight } = dimensions;
const [gridSize, setGridSize] = useState(10);
const [showGrid, setShowGrid] = useState(true);
const [snapToGrid, setSnapToGrid] = useState(true);
const [showRuler, setShowRuler] = useState(true);
const [zoom, setZoom] = useState(1);
const [fitTrigger, setFitTrigger] = useState(0);
const [alignmentGuides, setAlignmentGuides] = useState<AlignmentGuides>({ vertical: [], horizontal: [] });
const [isPageListCollapsed, setIsPageListCollapsed] = useState(false);
const [isLeftPanelCollapsed, setIsLeftPanelCollapsed] = useState(false);
const [isRightPanelCollapsed, setIsRightPanelCollapsed] = useState(false);
const [componentModalTargetId, setComponentModalTargetId] = useState<string | null>(null);
/** fitToScreen 호출 시 트리거 값을 증가시켜 Canvas가 화면 맞춤을 실행하게 한다. */
const fitToScreen = useCallback(() => setFitTrigger((n) => n + 1), []);
/** 인캔버스 설정 모달을 열고 대상 컴포넌트 ID를 등록한다. */
const openComponentModal = useCallback((componentId: string) => {
setComponentModalTargetId(componentId);
}, []);
/** 인캔버스 설정 모달을 닫는다. */
const closeComponentModal = useCallback(() => {
setComponentModalTargetId(null);
}, []);
/**
* value를 gridSize .
* .
*/
const snapValueToGrid = useCallback(
(value: number): number => {
if (!snapToGrid) return value;
return Math.round(value / gridSize) * gridSize;
},
[snapToGrid, gridSize],
);
/**
* /
* .
*
* @param draggingId ID ( )
* @param x x (px)
* @param y y (px)
* @param width (px)
* @param height (px)
*/
const calculateAlignmentGuides = useCallback(
(draggingId: string, x: number, y: number, width: number, height: number) => {
const verticalLines: number[] = [];
const horizontalLines: number[] = [];
const canvasWidthPx = canvasWidth * MM_TO_PX;
const canvasHeightPx = canvasHeight * MM_TO_PX;
const canvasCenterX = canvasWidthPx / 2;
const canvasCenterY = canvasHeightPx / 2;
const left = x;
const right = x + width;
const centerX = x + width / 2;
const top = y;
const bottom = y + height;
const centerY = y + height / 2;
// 캔버스 중앙선 체크
if (Math.abs(centerX - canvasCenterX) < ALIGNMENT_THRESHOLD_PX) verticalLines.push(canvasCenterX);
if (Math.abs(centerY - canvasCenterY) < ALIGNMENT_THRESHOLD_PX) horizontalLines.push(canvasCenterY);
// 다른 컴포넌트와 비교
components.forEach((comp) => {
if (comp.id === draggingId) return;
const cLeft = comp.x;
const cRight = comp.x + comp.width;
const cCenterX = comp.x + comp.width / 2;
const cTop = comp.y;
const cBottom = comp.y + comp.height;
const cCenterY = comp.y + comp.height / 2;
// 세로 정렬선 (left, right, centerX)
if (Math.abs(left - cLeft) < ALIGNMENT_THRESHOLD_PX) verticalLines.push(cLeft);
if (Math.abs(left - cRight) < ALIGNMENT_THRESHOLD_PX) verticalLines.push(cRight);
if (Math.abs(right - cLeft) < ALIGNMENT_THRESHOLD_PX) verticalLines.push(cLeft);
if (Math.abs(right - cRight) < ALIGNMENT_THRESHOLD_PX) verticalLines.push(cRight);
if (Math.abs(centerX - cCenterX) < ALIGNMENT_THRESHOLD_PX) verticalLines.push(cCenterX);
// 가로 정렬선 (top, bottom, centerY)
if (Math.abs(top - cTop) < ALIGNMENT_THRESHOLD_PX) horizontalLines.push(cTop);
if (Math.abs(top - cBottom) < ALIGNMENT_THRESHOLD_PX) horizontalLines.push(cBottom);
if (Math.abs(bottom - cTop) < ALIGNMENT_THRESHOLD_PX) horizontalLines.push(cTop);
if (Math.abs(bottom - cBottom) < ALIGNMENT_THRESHOLD_PX) horizontalLines.push(cBottom);
if (Math.abs(centerY - cCenterY) < ALIGNMENT_THRESHOLD_PX) horizontalLines.push(cCenterY);
});
setAlignmentGuides({
vertical: Array.from(new Set(verticalLines)),
horizontal: Array.from(new Set(horizontalLines)),
});
},
[components, canvasWidth, canvasHeight],
);
/** 드래그 종료 후 정렬 가이드라인을 초기화한다. */
const clearAlignmentGuides = useCallback(() => {
setAlignmentGuides({ vertical: [], horizontal: [] });
}, []);
return {
gridSize,
setGridSize,
showGrid,
setShowGrid,
snapToGrid,
setSnapToGrid,
snapValueToGrid,
showRuler,
setShowRuler,
zoom,
setZoom,
fitTrigger,
fitToScreen,
alignmentGuides,
calculateAlignmentGuides,
clearAlignmentGuides,
isPageListCollapsed,
setIsPageListCollapsed,
isLeftPanelCollapsed,
setIsLeftPanelCollapsed,
isRightPanelCollapsed,
setIsRightPanelCollapsed,
componentModalTargetId,
openComponentModal,
closeComponentModal,
};
}