201 lines
7.6 KiB
TypeScript
201 lines
7.6 KiB
TypeScript
/**
|
|
* @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,
|
|
};
|
|
}
|