"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"; // @ts-ignore - docx 라이브러리 타입 이슈 import { Document, Packer, Paragraph, TextRun, Table, TableCell, TableRow, WidthType, ImageRun, AlignmentType, VerticalAlign, convertInchesToTwip, } from "docx"; import { getFullImageUrl } from "@/lib/api/client"; interface ReportPreviewModalProps { isOpen: boolean; onClose: () => void; } export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) { const { layoutConfig, 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 = () => { // HTML 생성하여 인쇄 const printHtml = generatePrintHTML(); const printWindow = window.open("", "_blank"); if (!printWindow) return; printWindow.document.write(printHtml); printWindow.document.close(); printWindow.print(); }; // 페이지별 컴포넌트 HTML 생성 const generatePageHTML = ( pageComponents: any[], pageWidth: number, pageHeight: number, backgroundColor: string, ): string => { const componentsHTML = pageComponents .map((component) => { const queryResult = component.queryId ? getQueryResult(component.queryId) : null; let content = ""; // Text/Label 컴포넌트 if (component.type === "text" || component.type === "label") { const displayValue = getComponentValue(component); content = `
${displayValue}
`; } // Image 컴포넌트 else if (component.type === "image" && component.imageUrl) { const imageUrl = component.imageUrl.startsWith("data:") ? component.imageUrl : getFullImageUrl(component.imageUrl); content = ``; } // Divider 컴포넌트 else if (component.type === "divider") { const width = component.orientation === "horizontal" ? "100%" : `${component.lineWidth || 1}px`; const height = component.orientation === "vertical" ? "100%" : `${component.lineWidth || 1}px`; content = `
`; } // Signature 컴포넌트 else if (component.type === "signature") { const labelPosition = component.labelPosition || "left"; const showLabel = component.showLabel !== false; const labelText = component.labelText || "서명:"; const imageUrl = component.imageUrl ? component.imageUrl.startsWith("data:") ? component.imageUrl : getFullImageUrl(component.imageUrl) : ""; if (labelPosition === "left" || labelPosition === "right") { content = `
${showLabel ? `
${labelText}
` : ""}
${imageUrl ? `` : ""} ${component.showUnderline ? '
' : ""}
`; } else { content = `
${showLabel && labelPosition === "top" ? `
${labelText}
` : ""}
${imageUrl ? `` : ""} ${component.showUnderline ? '
' : ""}
${showLabel && labelPosition === "bottom" ? `
${labelText}
` : ""}
`; } } // Stamp 컴포넌트 else if (component.type === "stamp") { const showLabel = component.showLabel !== false; const labelText = component.labelText || "(인)"; const personName = component.personName || ""; const imageUrl = component.imageUrl ? component.imageUrl.startsWith("data:") ? component.imageUrl : getFullImageUrl(component.imageUrl) : ""; content = `
${personName ? `
${personName}
` : ""}
${imageUrl ? `` : ""} ${showLabel ? `
${labelText}
` : ""}
`; } // Table 컴포넌트 else if (component.type === "table" && queryResult && queryResult.rows.length > 0) { const columns = component.tableColumns && component.tableColumns.length > 0 ? component.tableColumns : queryResult.fields.map((field) => ({ field, header: field, align: "left" as const, width: undefined, })); const tableRows = queryResult.rows .map( (row) => ` ${columns.map((col: { field: string; align?: string }) => `${String(row[col.field] ?? "")}`).join("")} `, ) .join(""); content = ` ${columns.map((col: { header: string; align?: string; width?: number }) => ``).join("")} ${tableRows}
${col.header}
`; } return `
${content}
`; }) .join(""); return `
${componentsHTML}
`; }; // 모든 페이지 HTML 생성 (인쇄/PDF용) const generatePrintHTML = (): string => { const pagesHTML = layoutConfig.pages .sort((a, b) => a.page_order - b.page_order) .map((page) => generatePageHTML(page.components, page.width, page.height, page.background_color)) .join('
'); return ` 리포트 인쇄 ${pagesHTML} `; }; // PDF 다운로드 (브라우저 인쇄 기능 이용) const handleDownloadPDF = () => { const printHtml = generatePrintHTML(); const printWindow = window.open("", "_blank"); if (!printWindow) return; printWindow.document.write(printHtml); printWindow.document.close(); toast({ title: "안내", description: "인쇄 대화상자에서 'PDF로 저장'을 선택하세요.", }); }; // Base64를 Uint8Array로 변환 const base64ToUint8Array = (base64: string): Uint8Array => { const base64Data = base64.split(",")[1] || base64; const binaryString = atob(base64Data); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; }; // 컴포넌트를 TableCell로 변환 const createTableCell = (component: any, pageWidth: number, widthPercent?: number): TableCell | null => { const cellWidth = widthPercent || 100; if (component.type === "text" || component.type === "label") { const value = getComponentValue(component); return new TableCell({ children: [ new Paragraph({ children: [ new TextRun({ text: value, size: (component.fontSize || 13) * 2, color: component.fontColor?.replace("#", "") || "000000", bold: component.fontWeight === "bold", }), ], alignment: component.textAlign === "center" ? AlignmentType.CENTER : component.textAlign === "right" ? AlignmentType.RIGHT : AlignmentType.LEFT, }), ], width: { size: cellWidth, type: WidthType.PERCENTAGE }, verticalAlign: VerticalAlign.CENTER, borders: { top: { style: 0, size: 0, color: "FFFFFF" }, bottom: { style: 0, size: 0, color: "FFFFFF" }, left: { style: 0, size: 0, color: "FFFFFF" }, right: { style: 0, size: 0, color: "FFFFFF" }, }, }); } else if (component.type === "signature" || component.type === "stamp") { if (component.imageUrl) { try { const imageData = base64ToUint8Array(component.imageUrl); return new TableCell({ children: [ new Paragraph({ children: [ new ImageRun({ data: imageData, transformation: { width: component.width || 150, height: component.height || 50, }, }), ], alignment: AlignmentType.CENTER, }), ], width: { size: cellWidth, type: WidthType.PERCENTAGE }, verticalAlign: VerticalAlign.CENTER, borders: { top: { style: 0, size: 0, color: "FFFFFF" }, bottom: { style: 0, size: 0, color: "FFFFFF" }, left: { style: 0, size: 0, color: "FFFFFF" }, right: { style: 0, size: 0, color: "FFFFFF" }, }, }); } catch { return new TableCell({ children: [ new Paragraph({ children: [ new TextRun({ text: `[${component.type === "signature" ? "서명" : "도장"}]`, size: 24, }), ], }), ], width: { size: cellWidth, type: WidthType.PERCENTAGE }, borders: { top: { style: 0, size: 0, color: "FFFFFF" }, bottom: { style: 0, size: 0, color: "FFFFFF" }, left: { style: 0, size: 0, color: "FFFFFF" }, right: { style: 0, size: 0, color: "FFFFFF" }, }, }); } } } 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 }, }); return new TableCell({ children: [table], width: { size: cellWidth, type: WidthType.PERCENTAGE }, borders: { top: { style: 0, size: 0, color: "FFFFFF" }, bottom: { style: 0, size: 0, color: "FFFFFF" }, left: { style: 0, size: 0, color: "FFFFFF" }, right: { style: 0, size: 0, color: "FFFFFF" }, }, }); } } return null; }; // WORD 다운로드 const handleDownloadWord = async () => { setIsExporting(true); try { // 페이지별로 섹션 생성 const sections = layoutConfig.pages .sort((a, b) => a.page_order - b.page_order) .map((page) => { // 페이지 크기 설정 (A4 기준) const pageWidth = convertInchesToTwip(8.27); // A4 width in inches const pageHeight = convertInchesToTwip(11.69); // A4 height in inches const marginTop = convertInchesToTwip(page.margins.top / 96); // px to inches (96 DPI) const marginBottom = convertInchesToTwip(page.margins.bottom / 96); const marginLeft = convertInchesToTwip(page.margins.left / 96); const marginRight = convertInchesToTwip(page.margins.right / 96); // 페이지 내 컴포넌트를 Y좌표 기준으로 정렬 const sortedComponents = [...page.components].sort((a, b) => { // Y좌표 우선, 같으면 X좌표 if (Math.abs(a.y - b.y) < 5) { return a.x - b.x; } return a.y - b.y; }); // 컴포넌트들을 행으로 그룹화 (같은 Y 범위 = 같은 행) const rows: Array> = []; const rowTolerance = 20; // Y 좌표 허용 오차 for (const component of sortedComponents) { const existingRow = rows.find((row) => Math.abs(row[0].y - component.y) < rowTolerance); if (existingRow) { existingRow.push(component); } else { rows.push([component]); } } // 각 행 내에서 X좌표로 정렬 rows.forEach((row) => row.sort((a, b) => a.x - b.x)); // 페이지 전체를 큰 테이블로 구성 (레이아웃 제어용) const tableRows: TableRow[] = []; for (const row of rows) { if (row.length === 1) { // 단일 컴포넌트 - 전체 너비 사용 const component = row[0]; const cell = createTableCell(component, pageWidth); if (cell) { tableRows.push( new TableRow({ children: [cell], height: { value: component.height * 15, rule: 1 }, // 최소 높이 설정 }), ); } } else { // 여러 컴포넌트 - 가로 배치 const cells: TableCell[] = []; const totalWidth = row.reduce((sum, c) => sum + c.width, 0); for (const component of row) { const widthPercent = (component.width / totalWidth) * 100; const cell = createTableCell(component, pageWidth, widthPercent); if (cell) { cells.push(cell); } } if (cells.length > 0) { const maxHeight = Math.max(...row.map((c) => c.height)); tableRows.push( new TableRow({ children: cells, height: { value: maxHeight * 15, rule: 1 }, }), ); } } } return { properties: { page: { width: pageWidth, height: pageHeight, margin: { top: marginTop, bottom: marginBottom, left: marginLeft, right: marginRight, }, }, }, children: tableRows.length > 0 ? [ new Table({ rows: tableRows, width: { size: 100, type: WidthType.PERCENTAGE }, borders: { top: { style: 0, size: 0, color: "FFFFFF" }, bottom: { style: 0, size: 0, color: "FFFFFF" }, left: { style: 0, size: 0, color: "FFFFFF" }, right: { style: 0, size: 0, color: "FFFFFF" }, insideHorizontal: { style: 0, size: 0, color: "FFFFFF" }, insideVertical: { style: 0, size: 0, color: "FFFFFF" }, }, }), ] : [new Paragraph({ text: "" })], }; }); // 문서 생성 const doc = new Document({ sections, }); // 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 ( 미리보기 현재 레이아웃의 미리보기입니다. 인쇄하거나 파일로 다운로드할 수 있습니다. {/* 미리보기 영역 - 모든 페이지 표시 */}
{layoutConfig.pages .sort((a, b) => a.page_order - b.page_order) .map((page) => (
{/* 페이지 번호 라벨 */}
페이지 {page.page_order + 1} - {page.page_name}
{/* 페이지 컨텐츠 */}
{page.components.map((component) => { const displayValue = getComponentValue(component); const queryResult = component.queryId ? getQueryResult(component.queryId) : null; return (
{component.type === "text" && (
{displayValue}
)} {component.type === "label" && (
{displayValue}
)} {component.type === "table" && queryResult && queryResult.rows.length > 0 ? ( (() => { // tableColumns가 없으면 자동 생성 const columns = component.tableColumns && component.tableColumns.length > 0 ? component.tableColumns : queryResult.fields.map((field) => ({ field, header: field, align: "left" as const, width: undefined, })); return ( {columns.map((col) => ( ))} {queryResult.rows.map((row, idx) => ( {columns.map((col) => ( ))} ))}
{col.header}
{String(row[col.field] ?? "")}
); })() ) : component.type === "table" ? (
쿼리를 실행해주세요
) : null} {component.type === "image" && component.imageUrl && ( 이미지 )} {component.type === "divider" && (
)} {component.type === "signature" && (
{component.showLabel !== false && (
{component.labelText || "서명:"}
)}
{component.imageUrl && ( 서명 )} {component.showUnderline !== false && (
)}
)} {component.type === "stamp" && (
{component.personName && (
{component.personName}
)}
{component.imageUrl && ( 도장 )} {component.showLabel !== false && (
{component.labelText || "(인)"}
)}
)}
); })}
))}
); }