209 lines
6.6 KiB
TypeScript
209 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Save, Eye, RotateCcw, ArrowLeft, Loader2, BookTemplate, Grid3x3 } from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
|
import { useState } from "react";
|
|
|
|
import { SaveAsTemplateModal } from "./SaveAsTemplateModal";
|
|
import { reportApi } from "@/lib/api/reportApi";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { ReportPreviewModal } from "./ReportPreviewModal";
|
|
|
|
export function ReportDesignerToolbar() {
|
|
const router = useRouter();
|
|
const {
|
|
reportDetail,
|
|
saveLayout,
|
|
isSaving,
|
|
loadLayout,
|
|
components,
|
|
canvasWidth,
|
|
canvasHeight,
|
|
queries,
|
|
snapToGrid,
|
|
setSnapToGrid,
|
|
showGrid,
|
|
setShowGrid,
|
|
} = useReportDesigner();
|
|
const [showPreview, setShowPreview] = useState(false);
|
|
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
|
|
const { toast } = useToast();
|
|
|
|
// 템플릿 저장 가능 여부: 컴포넌트가 있어야 함
|
|
const canSaveAsTemplate = components.length > 0;
|
|
|
|
// Grid 토글 (Snap과 Grid 표시 함께 제어)
|
|
const handleToggleGrid = () => {
|
|
const newValue = !snapToGrid;
|
|
setSnapToGrid(newValue);
|
|
setShowGrid(newValue);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
await saveLayout();
|
|
};
|
|
|
|
const handleSaveAndClose = async () => {
|
|
await saveLayout();
|
|
router.push("/admin/report");
|
|
};
|
|
|
|
const handleReset = async () => {
|
|
if (confirm("현재 변경사항을 모두 취소하고 마지막 저장 상태로 되돌리시겠습니까?")) {
|
|
await loadLayout();
|
|
}
|
|
};
|
|
|
|
const handleBack = () => {
|
|
if (confirm("저장하지 않은 변경사항이 있을 수 있습니다. 목록으로 돌아가시겠습니까?")) {
|
|
router.push("/admin/report");
|
|
}
|
|
};
|
|
|
|
const handleSaveAsTemplate = async (data: {
|
|
templateNameKor: string;
|
|
templateNameEng?: string;
|
|
description?: string;
|
|
}) => {
|
|
try {
|
|
// 현재 레이아웃 데이터로 직접 템플릿 생성 (리포트 저장 불필요)
|
|
const response = await reportApi.createTemplateFromLayout({
|
|
templateNameKor: data.templateNameKor,
|
|
templateNameEng: data.templateNameEng,
|
|
templateType: reportDetail?.report?.report_type || "GENERAL",
|
|
description: data.description,
|
|
layoutConfig: {
|
|
width: canvasWidth,
|
|
height: canvasHeight,
|
|
orientation: "portrait",
|
|
margins: {
|
|
top: 10,
|
|
bottom: 10,
|
|
left: 10,
|
|
right: 10,
|
|
},
|
|
components: components,
|
|
},
|
|
defaultQueries: queries.map((q, index) => ({
|
|
name: q.name,
|
|
type: q.type,
|
|
sqlQuery: q.sqlQuery,
|
|
parameters: q.parameters,
|
|
externalConnectionId: q.externalConnectionId || null,
|
|
displayOrder: index,
|
|
})),
|
|
});
|
|
|
|
if (response.success) {
|
|
toast({
|
|
title: "성공",
|
|
description: "템플릿이 생성되었습니다.",
|
|
});
|
|
setShowSaveAsTemplate(false);
|
|
}
|
|
} catch (error: unknown) {
|
|
const errorMessage =
|
|
error instanceof Error && "response" in error
|
|
? (error as { response?: { data?: { message?: string } } }).response?.data?.message ||
|
|
"템플릿 생성에 실패했습니다."
|
|
: "템플릿 생성에 실패했습니다.";
|
|
|
|
toast({
|
|
title: "오류",
|
|
description: errorMessage,
|
|
variant: "destructive",
|
|
});
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center justify-between border-b bg-white px-4 py-3 shadow-sm">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" size="sm" onClick={handleBack} className="gap-2">
|
|
<ArrowLeft className="h-4 w-4" />
|
|
목록으로
|
|
</Button>
|
|
<div className="h-6 w-px bg-gray-300" />
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
{reportDetail?.report.report_name_kor || "리포트 디자이너"}
|
|
</h2>
|
|
{reportDetail?.report.report_name_eng && (
|
|
<p className="text-sm text-gray-500">{reportDetail.report.report_name_eng}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant={snapToGrid && showGrid ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={handleToggleGrid}
|
|
className="gap-2"
|
|
title="Grid Snap 및 표시 켜기/끄기"
|
|
>
|
|
<Grid3x3 className="h-4 w-4" />
|
|
{snapToGrid && showGrid ? "Grid ON" : "Grid OFF"}
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={handleReset} className="gap-2">
|
|
<RotateCcw className="h-4 w-4" />
|
|
초기화
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={() => setShowPreview(true)} className="gap-2">
|
|
<Eye className="h-4 w-4" />
|
|
미리보기
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowSaveAsTemplate(true)}
|
|
disabled={!canSaveAsTemplate}
|
|
className="gap-2"
|
|
title={!canSaveAsTemplate ? "컴포넌트를 추가한 후 템플릿으로 저장할 수 있습니다" : ""}
|
|
>
|
|
<BookTemplate className="h-4 w-4" />
|
|
템플릿으로 저장
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={handleSave} disabled={isSaving} className="gap-2">
|
|
{isSaving ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
저장 중...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="h-4 w-4" />
|
|
저장
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button size="sm" onClick={handleSaveAndClose} disabled={isSaving} className="gap-2">
|
|
{isSaving ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
저장 중...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="h-4 w-4" />
|
|
저장 후 닫기
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<ReportPreviewModal isOpen={showPreview} onClose={() => setShowPreview(false)} />
|
|
<SaveAsTemplateModal
|
|
isOpen={showSaveAsTemplate}
|
|
onClose={() => setShowSaveAsTemplate(false)}
|
|
onSave={handleSaveAsTemplate}
|
|
/>
|
|
</>
|
|
);
|
|
}
|