2025-10-01 12:00:13 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
/**
|
|
|
|
|
* @module ReportDesignerContext
|
|
|
|
|
* @description 리포트 디자이너의 전역 상태를 제공하는 Context.
|
|
|
|
|
*
|
|
|
|
|
* 이 파일은 얇은 조합 레이어(thin orchestrator)로, 실제 로직은 각 훅에 위임한다.
|
|
|
|
|
*
|
|
|
|
|
* 책임 분배:
|
|
|
|
|
* - useSelectionState → 컴포넌트 선택 상태 (단일/다중)
|
|
|
|
|
* - usePageManager → 페이지 CRUD 및 설정 변경
|
|
|
|
|
* - useQueryManager → 쿼리 정의 및 실행 결과 캐시
|
|
|
|
|
* - useUIState → 그리드/줌/패널/정렬 가이드 등 UI 전용 상태
|
|
|
|
|
* - useHistoryManager → Undo/Redo 이력 관리
|
|
|
|
|
* - useClipboardActions → 복사/붙여넣기/복제/스타일 전달
|
|
|
|
|
* - useAlignmentActions → 정렬·균등 배치·크기 동일화
|
|
|
|
|
* - useLayerActions → 레이어 순서·잠금·그룹화
|
|
|
|
|
* - useLayoutIO → 서버 저장/로드 및 템플릿 적용
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
createContext,
|
|
|
|
|
useContext,
|
|
|
|
|
useState,
|
|
|
|
|
useCallback,
|
|
|
|
|
ReactNode,
|
|
|
|
|
useEffect,
|
|
|
|
|
useRef,
|
|
|
|
|
} from "react";
|
|
|
|
|
import {
|
|
|
|
|
ComponentConfig,
|
|
|
|
|
ReportDetail,
|
|
|
|
|
ReportLayout,
|
|
|
|
|
ReportPage,
|
|
|
|
|
ReportLayoutConfig,
|
|
|
|
|
WatermarkConfig,
|
|
|
|
|
} from "@/types/report";
|
2025-10-01 12:00:13 +09:00
|
|
|
import { useToast } from "@/hooks/use-toast";
|
2025-10-01 13:53:45 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// 각 역할별 훅
|
|
|
|
|
import { useSelectionState } from "./report-designer/useSelectionState";
|
|
|
|
|
import { usePageManager } from "./report-designer/usePageManager";
|
|
|
|
|
import { useQueryManager } from "./report-designer/useQueryManager";
|
|
|
|
|
import { useUIState } from "./report-designer/useUIState";
|
|
|
|
|
import { useHistoryManager } from "./report-designer/useHistoryManager";
|
|
|
|
|
import { useClipboardActions } from "./report-designer/useClipboardActions";
|
|
|
|
|
import { useAlignmentActions } from "./report-designer/useAlignmentActions";
|
|
|
|
|
import { useLayerActions } from "./report-designer/useLayerActions";
|
|
|
|
|
import { useLayoutIO } from "./report-designer/useLayoutIO";
|
2025-10-01 14:05:06 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// 공유 타입
|
|
|
|
|
export type { ReportQuery, QueryResult } from "./report-designer/types";
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────
|
|
|
|
|
// Context 타입 정의
|
|
|
|
|
// ─────────────────────────────────────────────
|
2025-10-01 13:53:45 +09:00
|
|
|
|
2025-10-01 12:00:13 +09:00
|
|
|
interface ReportDesignerContextType {
|
|
|
|
|
reportId: string;
|
|
|
|
|
reportDetail: ReportDetail | null;
|
|
|
|
|
layout: ReportLayout | null;
|
2025-10-02 13:44:16 +09:00
|
|
|
|
|
|
|
|
// 페이지 관리
|
2026-03-11 12:03:53 +09:00
|
|
|
layoutConfig: ReportLayoutConfig;
|
2025-10-02 13:44:16 +09:00
|
|
|
currentPageId: string | null;
|
|
|
|
|
currentPage: ReportPage | undefined;
|
|
|
|
|
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;
|
2026-03-11 12:03:53 +09:00
|
|
|
updateWatermark: (watermark: WatermarkConfig | undefined) => void;
|
2025-10-02 13:44:16 +09:00
|
|
|
|
|
|
|
|
// 컴포넌트 (현재 페이지)
|
2026-03-11 12:03:53 +09:00
|
|
|
components: ComponentConfig[];
|
2025-10-01 12:00:13 +09:00
|
|
|
selectedComponentId: string | null;
|
2026-03-11 12:03:53 +09:00
|
|
|
selectedComponentIds: string[];
|
2025-10-01 12:00:13 +09:00
|
|
|
isLoading: boolean;
|
|
|
|
|
isSaving: boolean;
|
|
|
|
|
addComponent: (component: ComponentConfig) => void;
|
|
|
|
|
updateComponent: (id: string, updates: Partial<ComponentConfig>) => void;
|
|
|
|
|
removeComponent: (id: string) => void;
|
2025-10-01 15:53:37 +09:00
|
|
|
selectComponent: (id: string | null, isMultiSelect?: boolean) => void;
|
2026-03-11 12:03:53 +09:00
|
|
|
selectMultipleComponents: (ids: string[]) => void;
|
2025-10-01 12:00:13 +09:00
|
|
|
|
|
|
|
|
// 레이아웃 관리
|
|
|
|
|
updateLayout: (updates: Partial<ReportLayout>) => void;
|
|
|
|
|
saveLayout: () => Promise<void>;
|
|
|
|
|
loadLayout: () => Promise<void>;
|
2026-03-11 12:03:53 +09:00
|
|
|
applyTemplate: (templateId: string) => Promise<void>;
|
2025-10-01 12:00:13 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// 캔버스 설정 (현재 페이지 기반)
|
2025-10-01 12:00:13 +09:00
|
|
|
canvasWidth: number;
|
|
|
|
|
canvasHeight: number;
|
|
|
|
|
pageOrientation: string;
|
2026-03-11 12:03:53 +09:00
|
|
|
margins: { top: number; bottom: number; left: number; right: number };
|
2025-10-01 15:32:35 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// UI 도구
|
2025-10-01 15:32:35 +09:00
|
|
|
gridSize: number;
|
|
|
|
|
setGridSize: (size: number) => void;
|
|
|
|
|
showGrid: boolean;
|
|
|
|
|
setShowGrid: (show: boolean) => void;
|
|
|
|
|
snapToGrid: boolean;
|
|
|
|
|
setSnapToGrid: (snap: boolean) => void;
|
|
|
|
|
snapValueToGrid: (value: number) => number;
|
2025-10-01 15:35:16 +09:00
|
|
|
alignmentGuides: { vertical: number[]; horizontal: number[] };
|
|
|
|
|
calculateAlignmentGuides: (draggingId: string, x: number, y: number, width: number, height: number) => void;
|
|
|
|
|
clearAlignmentGuides: () => void;
|
2025-10-01 15:53:37 +09:00
|
|
|
|
|
|
|
|
// 복사/붙여넣기
|
|
|
|
|
copyComponents: () => void;
|
|
|
|
|
pasteComponents: () => void;
|
2026-03-11 12:03:53 +09:00
|
|
|
duplicateComponents: () => void;
|
|
|
|
|
copyStyles: () => void;
|
|
|
|
|
pasteStyles: () => void;
|
|
|
|
|
duplicateAtPosition: (componentIds: string[], offsetX?: number, offsetY?: number) => string[];
|
|
|
|
|
fitSelectedToContent: () => void;
|
2025-10-01 15:53:37 +09:00
|
|
|
|
|
|
|
|
// Undo/Redo
|
|
|
|
|
undo: () => void;
|
|
|
|
|
redo: () => void;
|
|
|
|
|
canUndo: boolean;
|
|
|
|
|
canRedo: boolean;
|
2025-10-01 16:06:47 +09:00
|
|
|
|
|
|
|
|
// 정렬 기능
|
|
|
|
|
alignLeft: () => void;
|
|
|
|
|
alignRight: () => void;
|
|
|
|
|
alignTop: () => void;
|
|
|
|
|
alignBottom: () => void;
|
|
|
|
|
alignCenterHorizontal: () => void;
|
|
|
|
|
alignCenterVertical: () => void;
|
|
|
|
|
distributeHorizontal: () => void;
|
|
|
|
|
distributeVertical: () => void;
|
|
|
|
|
makeSameWidth: () => void;
|
|
|
|
|
makeSameHeight: () => void;
|
|
|
|
|
makeSameSize: () => void;
|
2025-10-01 16:17:41 +09:00
|
|
|
|
|
|
|
|
// 레이어 관리
|
|
|
|
|
bringToFront: () => void;
|
|
|
|
|
sendToBack: () => void;
|
|
|
|
|
bringForward: () => void;
|
|
|
|
|
sendBackward: () => void;
|
2025-10-01 16:23:20 +09:00
|
|
|
toggleLock: () => void;
|
|
|
|
|
lockComponents: () => void;
|
|
|
|
|
unlockComponents: () => void;
|
2025-10-01 16:27:05 +09:00
|
|
|
|
|
|
|
|
// 눈금자 표시
|
|
|
|
|
showRuler: boolean;
|
|
|
|
|
setShowRuler: (show: boolean) => void;
|
2025-10-01 16:33:25 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// 캔버스 줌
|
|
|
|
|
zoom: number;
|
|
|
|
|
setZoom: (zoom: number) => void;
|
|
|
|
|
fitTrigger: number;
|
|
|
|
|
fitToScreen: () => void;
|
|
|
|
|
|
2025-10-01 16:33:25 +09:00
|
|
|
// 그룹화
|
|
|
|
|
groupComponents: () => void;
|
|
|
|
|
ungroupComponents: () => void;
|
2025-12-23 14:34:49 +09:00
|
|
|
|
|
|
|
|
// 메뉴 연결
|
|
|
|
|
menuObjids: number[];
|
|
|
|
|
setMenuObjids: (menuObjids: number[]) => void;
|
|
|
|
|
saveLayoutWithMenus: (menuObjids: number[]) => Promise<void>;
|
2026-03-11 12:03:53 +09:00
|
|
|
|
|
|
|
|
// 쿼리 관리
|
|
|
|
|
queries: import("./report-designer/types").ReportQuery[];
|
|
|
|
|
setQueries: (queries: import("./report-designer/types").ReportQuery[]) => void;
|
|
|
|
|
queryResults: import("./report-designer/types").QueryResult[];
|
|
|
|
|
setQueryResult: (queryId: string, fields: string[], rows: Record<string, unknown>[]) => void;
|
|
|
|
|
getQueryResult: (queryId: string) => import("./report-designer/types").QueryResult | null;
|
|
|
|
|
|
|
|
|
|
// 인캔버스 설정 모달
|
|
|
|
|
componentModalTargetId: string | null;
|
|
|
|
|
openComponentModal: (componentId: string) => void;
|
|
|
|
|
closeComponentModal: () => void;
|
|
|
|
|
|
|
|
|
|
// 패널 접기/펼치기
|
|
|
|
|
isPageListCollapsed: boolean;
|
|
|
|
|
setIsPageListCollapsed: (v: boolean) => void;
|
|
|
|
|
isLeftPanelCollapsed: boolean;
|
|
|
|
|
setIsLeftPanelCollapsed: (v: boolean) => void;
|
|
|
|
|
isRightPanelCollapsed: boolean;
|
|
|
|
|
setIsRightPanelCollapsed: (v: boolean) => void;
|
2025-10-01 12:00:13 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ReportDesignerContext = createContext<ReportDesignerContextType | undefined>(undefined);
|
|
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// ─────────────────────────────────────────────
|
|
|
|
|
// Provider
|
|
|
|
|
// ─────────────────────────────────────────────
|
2025-12-23 15:12:21 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
export function ReportDesignerProvider({
|
|
|
|
|
reportId,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
reportId: string;
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
}) {
|
|
|
|
|
const { toast } = useToast();
|
2025-12-23 15:12:21 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// ── 공유 레이아웃 상태 (여러 훅이 공유) ──
|
|
|
|
|
const [layoutConfig, setLayoutConfig] = useState<ReportLayoutConfig>({ pages: [] });
|
|
|
|
|
const [currentPageId, setCurrentPageId] = useState<string | null>(null);
|
2025-10-01 12:00:13 +09:00
|
|
|
const [reportDetail, setReportDetail] = useState<ReportDetail | null>(null);
|
|
|
|
|
const [layout, setLayout] = useState<ReportLayout | null>(null);
|
2026-03-11 12:03:53 +09:00
|
|
|
const [menuObjids, setMenuObjids] = useState<number[]>([]);
|
2025-10-01 12:00:13 +09:00
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
2025-10-02 13:44:16 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// currentPageId를 ref로 유지하여 클로저 내에서도 최신 값을 읽을 수 있게 한다
|
2025-10-02 14:13:11 +09:00
|
|
|
const currentPageIdRef = useRef<string | null>(currentPageId);
|
2026-03-11 12:03:53 +09:00
|
|
|
useEffect(() => { currentPageIdRef.current = currentPageId; }, [currentPageId]);
|
|
|
|
|
|
|
|
|
|
// ── 파생 값 ──
|
|
|
|
|
const currentPage = layoutConfig.pages.find((p) => p.page_id === currentPageId);
|
|
|
|
|
const components: ComponentConfig[] = Array.isArray(currentPage?.components) ? currentPage.components : [];
|
2025-10-02 14:13:11 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// 현재 페이지의 컴포넌트를 업데이트하는 공유 헬퍼 (ref 기반으로 클로저 문제 회피)
|
2025-10-02 13:44:16 +09:00
|
|
|
const setComponents = useCallback(
|
|
|
|
|
(updater: ComponentConfig[] | ((prev: ComponentConfig[]) => ComponentConfig[])) => {
|
2025-10-02 14:13:11 +09:00
|
|
|
setLayoutConfig((prev) => {
|
|
|
|
|
const pageId = currentPageIdRef.current;
|
2026-03-11 12:03:53 +09:00
|
|
|
if (!pageId) return prev;
|
|
|
|
|
const pageIndex = prev.pages.findIndex((p) => p.page_id === pageId);
|
|
|
|
|
if (pageIndex === -1) return prev;
|
|
|
|
|
const page = prev.pages[pageIndex];
|
|
|
|
|
const newComponents = typeof updater === "function" ? updater(page.components) : updater;
|
2025-10-02 14:13:11 +09:00
|
|
|
const newPages = [...prev.pages];
|
2026-03-11 12:03:53 +09:00
|
|
|
newPages[pageIndex] = { ...page, components: newComponents };
|
|
|
|
|
return { ...prev, pages: newPages };
|
2025-10-02 14:13:11 +09:00
|
|
|
});
|
2025-10-02 13:44:16 +09:00
|
|
|
},
|
2025-10-02 14:13:11 +09:00
|
|
|
[], // ref를 사용하므로 의존성 배열 비움
|
2025-10-02 13:44:16 +09:00
|
|
|
);
|
|
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// 캔버스 치수 (현재 페이지 기반, 단위: mm)
|
|
|
|
|
const canvasWidth = currentPage?.width ?? 210;
|
|
|
|
|
const canvasHeight = currentPage?.height ?? 297;
|
|
|
|
|
const pageOrientation = currentPage?.orientation ?? "portrait";
|
|
|
|
|
const margins = currentPage?.margins ?? { top: 20, bottom: 20, left: 20, right: 20 };
|
2025-10-01 15:53:37 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// ── 각 역할별 훅 조합 ──
|
2025-10-01 16:17:41 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const selection = useSelectionState();
|
2025-10-01 16:17:41 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const pageManager = usePageManager({
|
|
|
|
|
layoutConfig,
|
|
|
|
|
setLayoutConfig,
|
|
|
|
|
currentPageId,
|
|
|
|
|
setCurrentPageId,
|
|
|
|
|
clearSelection: selection.clearSelection,
|
|
|
|
|
toast,
|
|
|
|
|
});
|
2025-10-02 13:44:16 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const queryManager = useQueryManager();
|
2025-10-02 13:44:16 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const uiState = useUIState(components, { canvasWidth, canvasHeight });
|
2025-10-02 13:44:16 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const historyManager = useHistoryManager(components, setComponents, isLoading, toast);
|
2025-12-23 15:12:21 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const clipboard = useClipboardActions({
|
|
|
|
|
components,
|
|
|
|
|
selectedComponentId: selection.selectedComponentId,
|
|
|
|
|
selectedComponentIds: selection.selectedComponentIds,
|
|
|
|
|
setComponents,
|
|
|
|
|
currentPage,
|
|
|
|
|
currentPageId,
|
|
|
|
|
setLayoutConfig,
|
|
|
|
|
snapToGrid: uiState.snapToGrid,
|
|
|
|
|
gridSize: uiState.gridSize,
|
|
|
|
|
toast,
|
|
|
|
|
});
|
2025-12-23 15:12:21 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const alignment = useAlignmentActions({
|
|
|
|
|
components,
|
|
|
|
|
selectedComponentIds: selection.selectedComponentIds,
|
|
|
|
|
setComponents,
|
|
|
|
|
toast,
|
|
|
|
|
});
|
2025-10-01 12:00:13 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const layer = useLayerActions({
|
|
|
|
|
components,
|
|
|
|
|
selectedComponentId: selection.selectedComponentId,
|
|
|
|
|
selectedComponentIds: selection.selectedComponentIds,
|
|
|
|
|
setComponents,
|
|
|
|
|
toast,
|
|
|
|
|
});
|
2025-10-01 12:00:13 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
const layoutIO = useLayoutIO({
|
|
|
|
|
reportId,
|
|
|
|
|
layoutConfig,
|
|
|
|
|
queries: queryManager.queries,
|
|
|
|
|
menuObjids,
|
|
|
|
|
setMenuObjids,
|
|
|
|
|
setIsLoading,
|
|
|
|
|
setIsSaving,
|
|
|
|
|
setReportDetail,
|
|
|
|
|
setLayout,
|
|
|
|
|
setLayoutConfig,
|
|
|
|
|
setCurrentPageId,
|
|
|
|
|
setQueries: queryManager.setQueries,
|
|
|
|
|
setComponents,
|
|
|
|
|
currentPageIdRef,
|
|
|
|
|
toast,
|
|
|
|
|
});
|
2025-10-01 12:00:13 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// ── 초기 로드 ──
|
2025-10-01 12:00:13 +09:00
|
|
|
useEffect(() => {
|
2026-03-11 12:03:53 +09:00
|
|
|
layoutIO.loadLayout();
|
2025-10-01 14:23:00 +09:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [reportId]);
|
2025-10-01 12:00:13 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// ── 초기 히스토리 설정 (레이아웃 로드 완료 시) ──
|
2025-10-01 15:53:37 +09:00
|
|
|
useEffect(() => {
|
2026-03-11 12:03:53 +09:00
|
|
|
if (!isLoading && components.length > 0) {
|
|
|
|
|
historyManager.initHistory(components);
|
2025-10-01 15:53:37 +09:00
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-03-11 12:03:53 +09:00
|
|
|
}, [isLoading]);
|
2025-10-01 15:53:37 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// ── 컴포넌트 관리 (현재 페이지 직접 조작) ──
|
2025-10-01 15:53:37 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
/** 현재 페이지에 새 컴포넌트를 추가한다. */
|
2025-10-02 13:44:16 +09:00
|
|
|
const addComponent = useCallback(
|
2026-03-11 12:03:53 +09:00
|
|
|
(component: ComponentConfig) => setComponents((prev) => [...prev, component]),
|
2025-10-13 19:15:52 +09:00
|
|
|
[setComponents],
|
2025-10-02 13:44:16 +09:00
|
|
|
);
|
2025-10-01 12:00:13 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
/** 현재 페이지에서 특정 컴포넌트의 속성을 업데이트한다. */
|
2025-10-02 13:44:16 +09:00
|
|
|
const updateComponent = useCallback(
|
|
|
|
|
(id: string, updates: Partial<ComponentConfig>) => {
|
|
|
|
|
if (!currentPageId) return;
|
2025-10-13 19:15:52 +09:00
|
|
|
setLayoutConfig((prev) => ({
|
|
|
|
|
pages: prev.pages.map((page) =>
|
|
|
|
|
page.page_id === currentPageId
|
2026-03-11 12:03:53 +09:00
|
|
|
? { ...page, components: page.components.map((c) => (c.id === id ? { ...c, ...updates } : c)) }
|
2025-10-13 19:15:52 +09:00
|
|
|
: page,
|
|
|
|
|
),
|
|
|
|
|
}));
|
2025-10-02 13:44:16 +09:00
|
|
|
},
|
2025-10-13 19:15:52 +09:00
|
|
|
[currentPageId],
|
2025-10-02 13:44:16 +09:00
|
|
|
);
|
2025-10-01 12:00:13 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
/** 현재 페이지에서 특정 컴포넌트를 삭제하고 선택 상태를 정리한다. */
|
2025-10-01 12:00:13 +09:00
|
|
|
const removeComponent = useCallback(
|
|
|
|
|
(id: string) => {
|
2025-10-02 13:44:16 +09:00
|
|
|
if (!currentPageId) return;
|
|
|
|
|
setLayoutConfig((prev) => ({
|
|
|
|
|
pages: prev.pages.map((page) =>
|
|
|
|
|
page.page_id === currentPageId
|
2026-03-11 12:03:53 +09:00
|
|
|
? { ...page, components: page.components.filter((c) => c.id !== id) }
|
2025-10-02 13:44:16 +09:00
|
|
|
: page,
|
|
|
|
|
),
|
|
|
|
|
}));
|
2026-03-11 12:03:53 +09:00
|
|
|
if (selection.selectedComponentId === id) selection.setSelectedComponentId(null);
|
|
|
|
|
selection.setSelectedComponentIds((prev) => prev.filter((compId) => compId !== id));
|
2025-10-01 12:00:13 +09:00
|
|
|
},
|
2026-03-11 12:03:53 +09:00
|
|
|
[currentPageId, selection],
|
2025-10-01 12:00:13 +09:00
|
|
|
);
|
|
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
/**
|
|
|
|
|
* 레이아웃 메타데이터(canvas_width, page_orientation 등)를 업데이트한다.
|
|
|
|
|
* 현재 페이지의 페이지 설정과 동기화된다.
|
|
|
|
|
*/
|
2025-10-02 13:44:16 +09:00
|
|
|
const updateLayout = useCallback(
|
|
|
|
|
(updates: Partial<ReportLayout>) => {
|
|
|
|
|
setLayout((prev) => (prev ? { ...prev, ...updates } : null));
|
|
|
|
|
if (!currentPageId) return;
|
|
|
|
|
|
|
|
|
|
const pageUpdates: Partial<ReportPage> = {};
|
|
|
|
|
if (updates.canvas_width !== undefined) pageUpdates.width = updates.canvas_width;
|
|
|
|
|
if (updates.canvas_height !== undefined) pageUpdates.height = updates.canvas_height;
|
|
|
|
|
if (updates.page_orientation !== undefined)
|
|
|
|
|
pageUpdates.orientation = updates.page_orientation as "portrait" | "landscape";
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
updates.margin_top !== undefined ||
|
|
|
|
|
updates.margin_bottom !== undefined ||
|
|
|
|
|
updates.margin_left !== undefined ||
|
|
|
|
|
updates.margin_right !== undefined
|
|
|
|
|
) {
|
|
|
|
|
pageUpdates.margins = {
|
|
|
|
|
top: updates.margin_top ?? currentPage?.margins.top ?? 20,
|
|
|
|
|
bottom: updates.margin_bottom ?? currentPage?.margins.bottom ?? 20,
|
|
|
|
|
left: updates.margin_left ?? currentPage?.margins.left ?? 20,
|
|
|
|
|
right: updates.margin_right ?? currentPage?.margins.right ?? 20,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Object.keys(pageUpdates).length > 0) {
|
2026-03-11 12:03:53 +09:00
|
|
|
pageManager.updatePageSettings(currentPageId, pageUpdates);
|
2025-12-23 14:34:49 +09:00
|
|
|
}
|
|
|
|
|
},
|
2026-03-11 12:03:53 +09:00
|
|
|
[currentPageId, currentPage, pageManager, setLayout],
|
2025-12-23 14:34:49 +09:00
|
|
|
);
|
2025-10-01 12:00:13 +09:00
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
// ─────────────────────────────────────────────
|
|
|
|
|
// Context 값 조합
|
|
|
|
|
// ─────────────────────────────────────────────
|
2025-10-01 14:05:06 +09:00
|
|
|
|
2025-10-01 12:00:13 +09:00
|
|
|
const value: ReportDesignerContextType = {
|
|
|
|
|
reportId,
|
|
|
|
|
reportDetail,
|
|
|
|
|
layout,
|
2025-10-02 13:44:16 +09:00
|
|
|
|
|
|
|
|
// 페이지 관리
|
|
|
|
|
layoutConfig,
|
|
|
|
|
currentPageId,
|
|
|
|
|
currentPage,
|
2026-03-11 12:03:53 +09:00
|
|
|
...pageManager,
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 상태 및 관리
|
2025-10-01 12:00:13 +09:00
|
|
|
components,
|
|
|
|
|
isLoading,
|
|
|
|
|
isSaving,
|
|
|
|
|
addComponent,
|
|
|
|
|
updateComponent,
|
|
|
|
|
removeComponent,
|
|
|
|
|
updateLayout,
|
2026-03-11 12:03:53 +09:00
|
|
|
|
|
|
|
|
// 선택 상태
|
|
|
|
|
selectedComponentId: selection.selectedComponentId,
|
|
|
|
|
selectedComponentIds: selection.selectedComponentIds,
|
|
|
|
|
selectComponent: selection.selectComponent,
|
|
|
|
|
selectMultipleComponents: selection.selectMultipleComponents,
|
|
|
|
|
|
|
|
|
|
// 레이아웃 I/O
|
|
|
|
|
saveLayout: layoutIO.saveLayout,
|
|
|
|
|
loadLayout: layoutIO.loadLayout,
|
|
|
|
|
applyTemplate: layoutIO.applyTemplate,
|
|
|
|
|
saveLayoutWithMenus: layoutIO.saveLayoutWithMenus,
|
|
|
|
|
|
|
|
|
|
// 캔버스 설정
|
2025-10-01 12:00:13 +09:00
|
|
|
canvasWidth,
|
|
|
|
|
canvasHeight,
|
|
|
|
|
pageOrientation,
|
|
|
|
|
margins,
|
2026-03-11 12:03:53 +09:00
|
|
|
|
|
|
|
|
// UI 상태
|
|
|
|
|
...uiState,
|
|
|
|
|
|
2025-10-01 15:53:37 +09:00
|
|
|
// 복사/붙여넣기
|
2026-03-11 12:03:53 +09:00
|
|
|
...clipboard,
|
|
|
|
|
|
2025-10-01 15:53:37 +09:00
|
|
|
// Undo/Redo
|
2026-03-11 12:03:53 +09:00
|
|
|
undo: historyManager.undo,
|
|
|
|
|
redo: historyManager.redo,
|
|
|
|
|
canUndo: historyManager.canUndo,
|
|
|
|
|
canRedo: historyManager.canRedo,
|
|
|
|
|
|
|
|
|
|
// 정렬
|
|
|
|
|
...alignment,
|
|
|
|
|
|
|
|
|
|
// 레이어/잠금/그룹
|
|
|
|
|
...layer,
|
|
|
|
|
|
2025-12-23 14:34:49 +09:00
|
|
|
// 메뉴 연결
|
|
|
|
|
menuObjids,
|
|
|
|
|
setMenuObjids,
|
2026-03-11 12:03:53 +09:00
|
|
|
|
|
|
|
|
// 쿼리 관리
|
|
|
|
|
...queryManager,
|
2025-10-01 12:00:13 +09:00
|
|
|
};
|
|
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
return (
|
|
|
|
|
<ReportDesignerContext.Provider value={value}>
|
|
|
|
|
{children}
|
|
|
|
|
</ReportDesignerContext.Provider>
|
|
|
|
|
);
|
2025-10-01 12:00:13 +09:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 12:03:53 +09:00
|
|
|
/**
|
|
|
|
|
* 리포트 디자이너 Context를 소비하는 훅.
|
|
|
|
|
* ReportDesignerProvider 내부에서만 사용 가능하다.
|
|
|
|
|
*/
|
2025-10-01 12:00:13 +09:00
|
|
|
export function useReportDesigner() {
|
|
|
|
|
const context = useContext(ReportDesignerContext);
|
|
|
|
|
if (context === undefined) {
|
|
|
|
|
throw new Error("useReportDesigner must be used within a ReportDesignerProvider");
|
|
|
|
|
}
|
|
|
|
|
return context;
|
|
|
|
|
}
|