pdf/word 저장기능 임시
This commit is contained in:
parent
62d36abb65
commit
1c00ee28e8
|
|
@ -106,19 +106,24 @@
|
|||
- `frontend/components/report/designer/ReportDesignerRightPanel.tsx`
|
||||
- `frontend/components/report/designer/CanvasComponent.tsx`
|
||||
|
||||
### 8. 미리보기 및 인쇄
|
||||
### 8. 미리보기 및 내보내기
|
||||
|
||||
- [x] 미리보기 모달
|
||||
- [x] 실제 쿼리 데이터로 렌더링
|
||||
- [x] 편집용 UI 제거 (순수 데이터만 표시)
|
||||
- [x] 브라우저 인쇄 기능
|
||||
- [ ] PDF 다운로드 (추후 구현)
|
||||
- [ ] WORD 다운로드 (추후 구현)
|
||||
- [x] PDF 다운로드 (브라우저 네이티브 인쇄 기능)
|
||||
- [x] WORD 다운로드 (docx 라이브러리)
|
||||
- [x] 파일명 자동 생성 (리포트명\_날짜)
|
||||
|
||||
**파일**:
|
||||
|
||||
- `frontend/components/report/designer/ReportPreviewModal.tsx`
|
||||
|
||||
**사용 라이브러리**:
|
||||
|
||||
- `docx`: WORD 문서 생성 (PDF는 브라우저 기본 기능 사용)
|
||||
|
||||
### 9. 템플릿 시스템
|
||||
|
||||
- [x] 시스템 템플릿 적용 (발주서, 청구서, 기본)
|
||||
|
|
@ -175,21 +180,14 @@
|
|||
|
||||
### Phase 1: 사용성 개선 (권장)
|
||||
|
||||
1. **PDF/WORD 내보내기** ⬅️ 다음 권장 작업
|
||||
|
||||
- jsPDF 또는 pdfmake 라이브러리 사용
|
||||
- HTML to DOCX 변환
|
||||
- 다운로드 기능 구현
|
||||
- 미리보기 모달에 버튼 추가
|
||||
|
||||
2. **레이아웃 도구**
|
||||
1. **레이아웃 도구** ⬅️ 다음 권장 작업
|
||||
|
||||
- 격자 스냅 (Grid Snap)
|
||||
- 정렬 가이드라인
|
||||
- 컴포넌트 그룹화
|
||||
- 실행 취소/다시 실행 (Undo/Redo)
|
||||
|
||||
3. **쿼리 관리 개선**
|
||||
2. **쿼리 관리 개선**
|
||||
- 쿼리 미리보기 개선 (테이블 형태)
|
||||
- 쿼리 저장/불러오기
|
||||
- 쿼리 템플릿
|
||||
|
|
@ -315,4 +313,4 @@
|
|||
|
||||
**최종 업데이트**: 2025-10-01
|
||||
**작성자**: AI Assistant
|
||||
**상태**: 핵심 기능 구현 완료 (90% 완료)
|
||||
**상태**: PDF/WORD 내보내기 완료 (95% 완료)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Printer, FileDown } from "lucide-react";
|
||||
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;
|
||||
|
|
@ -18,7 +21,9 @@ interface ReportPreviewModalProps {
|
|||
}
|
||||
|
||||
export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) {
|
||||
const { components, canvasWidth, canvasHeight, getQueryResult } = useReportDesigner();
|
||||
const { components, canvasWidth, canvasHeight, getQueryResult, reportDetail } = useReportDesigner();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// 컴포넌트의 실제 표시 값 가져오기
|
||||
const getComponentValue = (component: any): string => {
|
||||
|
|
@ -63,12 +68,165 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
printWindow.print();
|
||||
};
|
||||
|
||||
// PDF 다운로드 (브라우저 인쇄 기능 이용)
|
||||
const handleDownloadPDF = () => {
|
||||
alert("PDF 다운로드 기능은 추후 구현 예정입니다.");
|
||||
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로 저장'을 선택하세요.",
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadWord = () => {
|
||||
alert("WORD 다운로드 기능은 추후 구현 예정입니다.");
|
||||
// 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 (
|
||||
|
|
@ -170,10 +328,10 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
<Button variant="outline" onClick={onClose} disabled={isExporting}>
|
||||
닫기
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handlePrint} className="gap-2">
|
||||
<Button variant="outline" onClick={handlePrint} disabled={isExporting} className="gap-2">
|
||||
<Printer className="h-4 w-4" />
|
||||
인쇄
|
||||
</Button>
|
||||
|
|
@ -181,9 +339,9 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
<FileDown className="h-4 w-4" />
|
||||
PDF
|
||||
</Button>
|
||||
<Button onClick={handleDownloadWord} variant="secondary" className="gap-2">
|
||||
<FileDown className="h-4 w-4" />
|
||||
WORD
|
||||
<Button onClick={handleDownloadWord} disabled={isExporting} variant="secondary" className="gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
{isExporting ? "생성 중..." : "WORD"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"docx": "^9.5.1",
|
||||
"docx-preview": "^0.3.6",
|
||||
"lucide-react": "^0.525.0",
|
||||
"mammoth": "^1.11.0",
|
||||
|
|
@ -4500,6 +4501,23 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz",
|
||||
"integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^24.0.1",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/docx-preview": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/docx-preview/-/docx-preview-0.3.6.tgz",
|
||||
|
|
@ -4509,6 +4527,39 @@
|
|||
"jszip": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/@types/node": {
|
||||
"version": "24.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.1.tgz",
|
||||
"integrity": "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/nanoid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/undici-types": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
|
||||
"integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
|
|
@ -5769,6 +5820,16 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
|
|
@ -6824,6 +6885,12 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
|
|
@ -8049,6 +8116,12 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
|
|
@ -9153,6 +9226,24 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"docx": "^9.5.1",
|
||||
"docx-preview": "^0.3.6",
|
||||
"lucide-react": "^0.525.0",
|
||||
"mammoth": "^1.11.0",
|
||||
|
|
|
|||
Loading…
Reference in New Issue