250 lines
7.7 KiB
TypeScript
250 lines
7.7 KiB
TypeScript
|
|
"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";
|
||
|
|
|
||
|
|
interface ReportDesignerContextType {
|
||
|
|
reportId: string;
|
||
|
|
reportDetail: ReportDetail | null;
|
||
|
|
layout: ReportLayout | null;
|
||
|
|
components: ComponentConfig[];
|
||
|
|
selectedComponentId: string | null;
|
||
|
|
isLoading: boolean;
|
||
|
|
isSaving: boolean;
|
||
|
|
|
||
|
|
// 컴포넌트 관리
|
||
|
|
addComponent: (component: ComponentConfig) => void;
|
||
|
|
updateComponent: (id: string, updates: Partial<ComponentConfig>) => void;
|
||
|
|
removeComponent: (id: string) => void;
|
||
|
|
selectComponent: (id: string | null) => void;
|
||
|
|
|
||
|
|
// 레이아웃 관리
|
||
|
|
updateLayout: (updates: Partial<ReportLayout>) => void;
|
||
|
|
saveLayout: () => Promise<void>;
|
||
|
|
loadLayout: () => Promise<void>;
|
||
|
|
|
||
|
|
// 캔버스 설정
|
||
|
|
canvasWidth: number;
|
||
|
|
canvasHeight: number;
|
||
|
|
pageOrientation: string;
|
||
|
|
margins: {
|
||
|
|
top: number;
|
||
|
|
bottom: number;
|
||
|
|
left: number;
|
||
|
|
right: number;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const ReportDesignerContext = createContext<ReportDesignerContextType | undefined>(undefined);
|
||
|
|
|
||
|
|
export function ReportDesignerProvider({ reportId, children }: { reportId: string; children: ReactNode }) {
|
||
|
|
const [reportDetail, setReportDetail] = useState<ReportDetail | null>(null);
|
||
|
|
const [layout, setLayout] = useState<ReportLayout | null>(null);
|
||
|
|
const [components, setComponents] = useState<ComponentConfig[]>([]);
|
||
|
|
const [selectedComponentId, setSelectedComponentId] = useState<string | null>(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);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 레이아웃 조회
|
||
|
|
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 (layoutError) {
|
||
|
|
// 레이아웃이 없으면 기본값 사용
|
||
|
|
console.log("레이아웃 없음, 기본값 사용");
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
toast({
|
||
|
|
title: "오류",
|
||
|
|
description: error.message || "리포트를 불러오는데 실패했습니다.",
|
||
|
|
variant: "destructive",
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
}, [reportId, toast]);
|
||
|
|
|
||
|
|
// 초기 로드
|
||
|
|
useEffect(() => {
|
||
|
|
loadLayout();
|
||
|
|
}, [loadLayout]);
|
||
|
|
|
||
|
|
// 컴포넌트 추가
|
||
|
|
const addComponent = useCallback((component: ComponentConfig) => {
|
||
|
|
setComponents((prev) => [...prev, component]);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 컴포넌트 업데이트
|
||
|
|
const updateComponent = useCallback((id: string, updates: Partial<ComponentConfig>) => {
|
||
|
|
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<ReportLayout>) => {
|
||
|
|
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,
|
||
|
|
});
|
||
|
|
|
||
|
|
toast({
|
||
|
|
title: "성공",
|
||
|
|
description: reportId === "new" ? "리포트가 생성되었습니다." : "레이아웃이 저장되었습니다.",
|
||
|
|
});
|
||
|
|
|
||
|
|
// 새 리포트였다면 데이터 다시 로드
|
||
|
|
if (reportId === "new") {
|
||
|
|
await loadLayout();
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
toast({
|
||
|
|
title: "오류",
|
||
|
|
description: error.message || "저장에 실패했습니다.",
|
||
|
|
variant: "destructive",
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
setIsSaving(false);
|
||
|
|
}
|
||
|
|
}, [reportId, canvasWidth, canvasHeight, pageOrientation, margins, components, toast, loadLayout]);
|
||
|
|
|
||
|
|
const value: ReportDesignerContextType = {
|
||
|
|
reportId,
|
||
|
|
reportDetail,
|
||
|
|
layout,
|
||
|
|
components,
|
||
|
|
selectedComponentId,
|
||
|
|
isLoading,
|
||
|
|
isSaving,
|
||
|
|
addComponent,
|
||
|
|
updateComponent,
|
||
|
|
removeComponent,
|
||
|
|
selectComponent,
|
||
|
|
updateLayout,
|
||
|
|
saveLayout,
|
||
|
|
loadLayout,
|
||
|
|
canvasWidth,
|
||
|
|
canvasHeight,
|
||
|
|
pageOrientation,
|
||
|
|
margins,
|
||
|
|
};
|
||
|
|
|
||
|
|
return <ReportDesignerContext.Provider value={value}>{children}</ReportDesignerContext.Provider>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useReportDesigner() {
|
||
|
|
const context = useContext(ReportDesignerContext);
|
||
|
|
if (context === undefined) {
|
||
|
|
throw new Error("useReportDesigner must be used within a ReportDesignerProvider");
|
||
|
|
}
|
||
|
|
return context;
|
||
|
|
}
|