ERP-node/frontend/components/report/designer/ReportPreviewModal.tsx

351 lines
11 KiB
TypeScript

"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Printer, FileDown, FileText } from "lucide-react";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { useState } from "react";
import { useToast } from "@/hooks/use-toast";
import { Document, Packer, Paragraph, TextRun, Table, TableCell, TableRow, WidthType } from "docx";
interface ReportPreviewModalProps {
isOpen: boolean;
onClose: () => void;
}
export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) {
const { components, canvasWidth, canvasHeight, getQueryResult, reportDetail } = useReportDesigner();
const [isExporting, setIsExporting] = useState(false);
const { toast } = useToast();
// 컴포넌트의 실제 표시 값 가져오기
const getComponentValue = (component: any): string => {
if (component.queryId && component.fieldName) {
const queryResult = getQueryResult(component.queryId);
if (queryResult && queryResult.rows.length > 0) {
const value = queryResult.rows[0][component.fieldName];
if (value !== null && value !== undefined) {
return String(value);
}
}
return `{${component.fieldName}}`;
}
return component.defaultValue || "텍스트";
};
const handlePrint = () => {
// 현재 미리보기 영역만 인쇄
const printContent = document.getElementById("preview-content");
if (!printContent) return;
const printWindow = window.open("", "_blank");
if (!printWindow) return;
printWindow.document.write(`
<html>
<head>
<title>리포트 인쇄</title>
<style>
body { margin: 0; padding: 20px; }
@media print {
body { margin: 0; padding: 0; }
}
</style>
</head>
<body>
${printContent.innerHTML}
</body>
</html>
`);
printWindow.document.close();
printWindow.print();
};
// PDF 다운로드 (브라우저 인쇄 기능 이용)
const handleDownloadPDF = () => {
const printContent = document.getElementById("preview-content");
if (!printContent) return;
const printWindow = window.open("", "_blank");
if (!printWindow) return;
printWindow.document.write(`
<html>
<head>
<title>리포트 인쇄</title>
<style>
@media print {
@page {
size: A4;
margin: 10mm;
}
body {
margin: 0;
padding: 0;
}
}
body {
font-family: 'Malgun Gothic', sans-serif;
margin: 0;
padding: 20px;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
</head>
<body>
${printContent.innerHTML}
<script>
window.onload = function() {
window.print();
}
</script>
</body>
</html>
`);
printWindow.document.close();
toast({
title: "안내",
description: "인쇄 대화상자에서 'PDF로 저장'을 선택하세요.",
});
};
// WORD 다운로드
const handleDownloadWord = async () => {
setIsExporting(true);
try {
// 컴포넌트를 Paragraph로 변환
const paragraphs: (Paragraph | Table)[] = [];
// Y 좌표로 정렬
const sortedComponents = [...components].sort((a, b) => a.y - b.y);
for (const component of sortedComponents) {
if (component.type === "text" || component.type === "label") {
const value = getComponentValue(component);
paragraphs.push(
new Paragraph({
children: [
new TextRun({
text: value,
size: (component.fontSize || 13) * 2, // pt to half-pt
color: component.fontColor?.replace("#", "") || "000000",
bold: component.fontWeight === "bold",
}),
],
spacing: {
after: 200,
},
}),
);
} else if (component.type === "table" && component.queryId) {
const queryResult = getQueryResult(component.queryId);
if (queryResult && queryResult.rows.length > 0) {
// 테이블 헤더
const headerCells = queryResult.fields.map(
(field) =>
new TableCell({
children: [new Paragraph({ text: field })],
width: { size: 100 / queryResult.fields.length, type: WidthType.PERCENTAGE },
}),
);
// 테이블 행
const dataRows = queryResult.rows.map(
(row) =>
new TableRow({
children: queryResult.fields.map(
(field) =>
new TableCell({
children: [new Paragraph({ text: String(row[field] ?? "") })],
}),
),
}),
);
const table = new Table({
rows: [new TableRow({ children: headerCells }), ...dataRows],
width: { size: 100, type: WidthType.PERCENTAGE },
});
paragraphs.push(table);
}
}
}
// 문서 생성
const doc = new Document({
sections: [
{
properties: {},
children: paragraphs,
},
],
});
// Blob 생성 및 다운로드
const blob = await Packer.toBlob(doc);
const fileName = reportDetail?.report?.report_name_kor || "리포트";
const timestamp = new Date().toISOString().slice(0, 10);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${fileName}_${timestamp}.docx`;
link.click();
window.URL.revokeObjectURL(url);
toast({
title: "성공",
description: "WORD 파일이 다운로드되었습니다.",
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "WORD 생성에 실패했습니다.";
toast({
title: "오류",
description: errorMessage,
variant: "destructive",
});
} finally {
setIsExporting(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
. .
</DialogDescription>
</DialogHeader>
{/* 미리보기 영역 */}
<div className="max-h-[500px] overflow-auto rounded border bg-gray-100 p-4">
<div
id="preview-content"
className="relative mx-auto bg-white shadow-lg"
style={{
width: `${canvasWidth}mm`,
minHeight: `${canvasHeight}mm`,
}}
>
{components.map((component) => {
const displayValue = getComponentValue(component);
const queryResult = component.queryId ? getQueryResult(component.queryId) : null;
return (
<div
key={component.id}
className="absolute"
style={{
left: `${component.x}px`,
top: `${component.y}px`,
width: `${component.width}px`,
height: `${component.height}px`,
backgroundColor: component.backgroundColor,
border: component.borderWidth
? `${component.borderWidth}px solid ${component.borderColor}`
: "none",
padding: "8px",
}}
>
{component.type === "text" && (
<div
style={{
fontSize: `${component.fontSize}px`,
color: component.fontColor,
fontWeight: component.fontWeight,
textAlign: component.textAlign as "left" | "center" | "right",
}}
>
{displayValue}
</div>
)}
{component.type === "label" && (
<div
style={{
fontSize: `${component.fontSize}px`,
color: component.fontColor,
fontWeight: component.fontWeight,
textAlign: component.textAlign as "left" | "center" | "right",
}}
>
{displayValue}
</div>
)}
{component.type === "table" && queryResult && queryResult.rows.length > 0 ? (
<table className="w-full border-collapse text-xs">
<thead>
<tr className="bg-gray-100">
{queryResult.fields.map((field) => (
<th key={field} className="border border-gray-300 p-1">
{field}
</th>
))}
</tr>
</thead>
<tbody>
{queryResult.rows.map((row, idx) => (
<tr key={idx}>
{queryResult.fields.map((field) => (
<td key={field} className="border border-gray-300 p-1">
{String(row[field] ?? "")}
</td>
))}
</tr>
))}
</tbody>
</table>
) : component.type === "table" ? (
<div className="text-xs text-gray-400"> </div>
) : null}
</div>
);
})}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isExporting}>
</Button>
<Button variant="outline" onClick={handlePrint} disabled={isExporting} className="gap-2">
<Printer className="h-4 w-4" />
</Button>
<Button onClick={handleDownloadPDF} className="gap-2">
<FileDown className="h-4 w-4" />
PDF
</Button>
<Button onClick={handleDownloadWord} disabled={isExporting} variant="secondary" className="gap-2">
<FileText className="h-4 w-4" />
{isExporting ? "생성 중..." : "WORD"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}