"use client"; import { createContext, useContext, useState, useCallback, ReactNode, useEffect } from "react"; import { ComponentConfig, ReportDetail, ReportLayout } from "@/types/report"; import { reportApi } from "@/lib/api/reportApi"; import { useToast } from "@/hooks/use-toast"; export interface ReportQuery { id: string; name: string; type: "MASTER" | "DETAIL"; sqlQuery: string; parameters: string[]; } // 템플릿 레이아웃 정의 interface TemplateLayout { components: ComponentConfig[]; queries: ReportQuery[]; } function getTemplateLayout(templateId: string): TemplateLayout | null { switch (templateId) { case "order": return { components: [ { id: `comp-${Date.now()}-1`, type: "label", x: 50, y: 30, width: 200, height: 40, fontSize: 24, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, defaultValue: "발주서", }, { id: `comp-${Date.now()}-2`, type: "text", x: 50, y: 80, width: 150, height: 30, fontSize: 14, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, }, { id: `comp-${Date.now()}-3`, type: "text", x: 220, y: 80, width: 150, height: 30, fontSize: 14, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, }, { id: `comp-${Date.now()}-4`, type: "text", x: 390, y: 80, width: 150, height: 30, fontSize: 14, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, }, { id: `comp-${Date.now()}-5`, type: "table", x: 50, y: 130, width: 500, height: 200, fontSize: 12, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, }, ], queries: [ { id: `query-${Date.now()}-1`, name: "발주 헤더", type: "MASTER", sqlQuery: "SELECT order_no, order_date, supplier_name FROM orders WHERE order_no = $1", parameters: ["$1"], }, { id: `query-${Date.now()}-2`, name: "발주 품목", type: "DETAIL", sqlQuery: "SELECT item_name, quantity, unit_price FROM order_items WHERE order_no = $1", parameters: ["$1"], }, ], }; case "invoice": return { components: [ { id: `comp-${Date.now()}-1`, type: "label", x: 50, y: 30, width: 200, height: 40, fontSize: 24, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, defaultValue: "청구서", }, { id: `comp-${Date.now()}-2`, type: "text", x: 50, y: 80, width: 150, height: 30, fontSize: 14, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, }, { id: `comp-${Date.now()}-3`, type: "text", x: 220, y: 80, width: 150, height: 30, fontSize: 14, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, }, { id: `comp-${Date.now()}-4`, type: "table", x: 50, y: 130, width: 500, height: 200, fontSize: 12, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, }, { id: `comp-${Date.now()}-5`, type: "label", x: 400, y: 350, width: 150, height: 30, fontSize: 16, fontColor: "#000000", backgroundColor: "#ffffcc", borderColor: "#000000", borderWidth: 1, zIndex: 1, defaultValue: "합계: 0원", }, ], queries: [ { id: `query-${Date.now()}-1`, name: "청구 헤더", type: "MASTER", sqlQuery: "SELECT invoice_no, invoice_date, customer_name FROM invoices WHERE invoice_no = $1", parameters: ["$1"], }, { id: `query-${Date.now()}-2`, name: "청구 항목", type: "DETAIL", sqlQuery: "SELECT description, quantity, unit_price, amount FROM invoice_items WHERE invoice_no = $1", parameters: ["$1"], }, ], }; case "basic": return { components: [ { id: `comp-${Date.now()}-1`, type: "label", x: 50, y: 30, width: 300, height: 40, fontSize: 20, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, defaultValue: "리포트 제목", }, { id: `comp-${Date.now()}-2`, type: "text", x: 50, y: 80, width: 500, height: 100, fontSize: 14, fontColor: "#000000", backgroundColor: "transparent", borderColor: "#cccccc", borderWidth: 0, zIndex: 1, defaultValue: "내용을 입력하세요", }, ], queries: [ { id: `query-${Date.now()}-1`, name: "기본 쿼리", type: "MASTER", sqlQuery: "SELECT * FROM table_name WHERE id = $1", parameters: ["$1"], }, ], }; default: return null; } } export interface QueryResult { queryId: string; fields: string[]; rows: Record[]; } interface ReportDesignerContextType { reportId: string; reportDetail: ReportDetail | null; layout: ReportLayout | null; components: ComponentConfig[]; selectedComponentId: string | null; isLoading: boolean; isSaving: boolean; // 쿼리 관리 queries: ReportQuery[]; setQueries: (queries: ReportQuery[]) => void; // 쿼리 실행 결과 queryResults: QueryResult[]; setQueryResult: (queryId: string, fields: string[], rows: Record[]) => void; getQueryResult: (queryId: string) => QueryResult | null; // 컴포넌트 관리 addComponent: (component: ComponentConfig) => void; updateComponent: (id: string, updates: Partial) => void; removeComponent: (id: string) => void; selectComponent: (id: string | null) => void; // 레이아웃 관리 updateLayout: (updates: Partial) => void; saveLayout: () => Promise; loadLayout: () => Promise; // 템플릿 적용 applyTemplate: (templateId: string) => void; // 캔버스 설정 canvasWidth: number; canvasHeight: number; pageOrientation: string; margins: { top: number; bottom: number; left: number; right: number; }; } const ReportDesignerContext = createContext(undefined); export function ReportDesignerProvider({ reportId, children }: { reportId: string; children: ReactNode }) { const [reportDetail, setReportDetail] = useState(null); const [layout, setLayout] = useState(null); const [components, setComponents] = useState([]); const [queries, setQueries] = useState([]); const [queryResults, setQueryResults] = useState([]); const [selectedComponentId, setSelectedComponentId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const { toast } = useToast(); // 캔버스 설정 (기본값) const [canvasWidth, setCanvasWidth] = useState(210); const [canvasHeight, setCanvasHeight] = useState(297); const [pageOrientation, setPageOrientation] = useState("portrait"); const [margins, setMargins] = useState({ top: 20, bottom: 20, left: 20, right: 20, }); // 리포트 및 레이아웃 로드 const loadLayout = useCallback(async () => { setIsLoading(true); try { // 'new'는 새 리포트 생성 모드 if (reportId === "new") { setIsLoading(false); return; } // 리포트 상세 조회 (쿼리 포함) const detailResponse = await reportApi.getReportById(reportId); if (detailResponse.success && detailResponse.data) { setReportDetail(detailResponse.data); // 쿼리 로드 if (detailResponse.data.queries && detailResponse.data.queries.length > 0) { const loadedQueries = detailResponse.data.queries.map((q) => ({ id: q.query_id, name: q.query_name, type: q.query_type, sqlQuery: q.sql_query, parameters: Array.isArray(q.parameters) ? q.parameters : [], })); setQueries(loadedQueries); } } // 레이아웃 조회 try { const layoutResponse = await reportApi.getLayout(reportId); if (layoutResponse.success && layoutResponse.data) { const layoutData = layoutResponse.data; setLayout(layoutData); setComponents(layoutData.components || []); setCanvasWidth(layoutData.canvas_width); setCanvasHeight(layoutData.canvas_height); setPageOrientation(layoutData.page_orientation); setMargins({ top: layoutData.margin_top, bottom: layoutData.margin_bottom, left: layoutData.margin_left, right: layoutData.margin_right, }); } } catch { // 레이아웃이 없으면 기본값 사용 console.log("레이아웃 없음, 기본값 사용"); } // 쿼리 조회는 이미 위에서 처리됨 (reportResponse.data.queries) } catch (error) { const errorMessage = error instanceof Error ? error.message : "리포트를 불러오는데 실패했습니다."; toast({ title: "오류", description: errorMessage, variant: "destructive", }); } finally { setIsLoading(false); } }, [reportId, toast]); // 초기 로드 useEffect(() => { loadLayout(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [reportId]); // 쿼리 결과 저장 const setQueryResult = useCallback((queryId: string, fields: string[], rows: Record[]) => { setQueryResults((prev) => { const existing = prev.find((r) => r.queryId === queryId); if (existing) { return prev.map((r) => (r.queryId === queryId ? { queryId, fields, rows } : r)); } return [...prev, { queryId, fields, rows }]; }); }, []); // 쿼리 결과 조회 const getQueryResult = useCallback( (queryId: string): QueryResult | null => { return queryResults.find((r) => r.queryId === queryId) || null; }, [queryResults], ); // 컴포넌트 추가 const addComponent = useCallback((component: ComponentConfig) => { setComponents((prev) => [...prev, component]); }, []); // 컴포넌트 업데이트 const updateComponent = useCallback((id: string, updates: Partial) => { setComponents((prev) => prev.map((comp) => (comp.id === id ? { ...comp, ...updates } : comp))); }, []); // 컴포넌트 삭제 const removeComponent = useCallback( (id: string) => { setComponents((prev) => prev.filter((comp) => comp.id !== id)); if (selectedComponentId === id) { setSelectedComponentId(null); } }, [selectedComponentId], ); // 컴포넌트 선택 const selectComponent = useCallback((id: string | null) => { setSelectedComponentId(id); }, []); // 레이아웃 업데이트 const updateLayout = useCallback((updates: Partial) => { setLayout((prev) => (prev ? { ...prev, ...updates } : null)); if (updates.canvas_width !== undefined) setCanvasWidth(updates.canvas_width); if (updates.canvas_height !== undefined) setCanvasHeight(updates.canvas_height); if (updates.page_orientation !== undefined) setPageOrientation(updates.page_orientation); if ( updates.margin_top !== undefined || updates.margin_bottom !== undefined || updates.margin_left !== undefined || updates.margin_right !== undefined ) { setMargins((prev) => ({ top: updates.margin_top ?? prev.top, bottom: updates.margin_bottom ?? prev.bottom, left: updates.margin_left ?? prev.left, right: updates.margin_right ?? prev.right, })); } }, []); // 레이아웃 저장 const saveLayout = useCallback(async () => { setIsSaving(true); try { let actualReportId = reportId; // 새 리포트인 경우 먼저 리포트 생성 if (reportId === "new") { const createResponse = await reportApi.createReport({ reportNameKor: "새 리포트", reportType: "BASIC", description: "새로 생성된 리포트입니다.", }); if (!createResponse.success || !createResponse.data) { throw new Error("리포트 생성에 실패했습니다."); } actualReportId = createResponse.data.reportId; // URL 업데이트 (페이지 리로드 없이) window.history.replaceState({}, "", `/admin/report/designer/${actualReportId}`); } // 레이아웃 저장 (쿼리 포함) await reportApi.saveLayout(actualReportId, { canvasWidth, canvasHeight, pageOrientation, marginTop: margins.top, marginBottom: margins.bottom, marginLeft: margins.left, marginRight: margins.right, components, queries, }); toast({ title: "성공", description: reportId === "new" ? "리포트가 생성되었습니다." : "레이아웃이 저장되었습니다.", }); // 새 리포트였다면 데이터 다시 로드 if (reportId === "new") { await loadLayout(); } } catch (error) { const errorMessage = error instanceof Error ? error.message : "저장에 실패했습니다."; toast({ title: "오류", description: errorMessage, variant: "destructive", }); } finally { setIsSaving(false); } }, [reportId, canvasWidth, canvasHeight, pageOrientation, margins, components, queries, toast, loadLayout]); // 템플릿 적용 const applyTemplate = useCallback( (templateId: string) => { const templates = getTemplateLayout(templateId); if (!templates) { toast({ title: "오류", description: "템플릿을 찾을 수 없습니다.", variant: "destructive", }); return; } // 기존 컴포넌트가 있으면 확인 if (components.length > 0) { if (!confirm("현재 레이아웃을 덮어씁니다. 계속하시겠습니까?")) { return; } } // 컴포넌트 배치 setComponents(templates.components); // 쿼리 설정 setQueries(templates.queries); toast({ title: "성공", description: "템플릿이 적용되었습니다.", }); }, [components.length, toast], ); const value: ReportDesignerContextType = { reportId, reportDetail, layout, components, queries, setQueries, queryResults, setQueryResult, getQueryResult, selectedComponentId, isLoading, isSaving, addComponent, updateComponent, removeComponent, selectComponent, updateLayout, saveLayout, loadLayout, applyTemplate, canvasWidth, canvasHeight, pageOrientation, margins, }; return {children}; } export function useReportDesigner() { const context = useContext(ReportDesignerContext); if (context === undefined) { throw new Error("useReportDesigner must be used within a ReportDesignerProvider"); } return context; }