"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 { 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, pageIndex: number = 0, totalPages: number = 1, ): 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}
` : ""}
`; } // PageNumber 컴포넌트 else if (component.type === "pageNumber") { const format = component.pageNumberFormat || "number"; let pageNumberText = ""; switch (format) { case "number": pageNumberText = `${pageIndex + 1}`; break; case "numberTotal": pageNumberText = `${pageIndex + 1} / ${totalPages}`; break; case "koreanNumber": pageNumberText = `${pageIndex + 1} 페이지`; break; default: pageNumberText = `${pageIndex + 1}`; } content = `
${pageNumberText}
`; } // Card 컴포넌트 else if (component.type === "card") { const cardTitle = component.cardTitle || "정보 카드"; const cardItems = component.cardItems || []; const labelWidth = component.labelWidth || 80; const showCardTitle = component.showCardTitle !== false; const titleFontSize = component.titleFontSize || 14; const labelFontSize = component.labelFontSize || 13; const valueFontSize = component.valueFontSize || 13; const titleColor = component.titleColor || "#1e40af"; const labelColor = component.labelColor || "#374151"; const valueColor = component.valueColor || "#000000"; const borderColor = component.borderColor || "#e5e7eb"; // 쿼리 바인딩된 값 가져오기 const getCardValue = (item: { label: string; value: string; fieldName?: string }) => { if (item.fieldName && queryResult && queryResult.rows && queryResult.rows.length > 0) { const row = queryResult.rows[0]; return row[item.fieldName] !== undefined ? String(row[item.fieldName]) : item.value; } return item.value; }; const itemsHtml = cardItems .map( (item: { label: string; value: string; fieldName?: string }) => `
${item.label} ${getCardValue(item)}
` ) .join(""); content = `
${ showCardTitle ? `
${cardTitle}
` : "" }
${itemsHtml}
`; } // 계산 컴포넌트 else if (component.type === "calculation") { const calcItems = component.calcItems || []; const resultLabel = component.resultLabel || "합계"; const calcLabelWidth = component.labelWidth || 120; const calcLabelFontSize = component.labelFontSize || 13; const calcValueFontSize = component.valueFontSize || 13; const calcResultFontSize = component.resultFontSize || 16; const calcLabelColor = component.labelColor || "#374151"; const calcValueColor = component.valueColor || "#000000"; const calcResultColor = component.resultColor || "#2563eb"; const numberFormat = component.numberFormat || "currency"; const currencySuffix = component.currencySuffix || "원"; const borderColor = component.borderColor || "#374151"; // 숫자 포맷팅 함수 const formatNumber = (num: number): string => { if (numberFormat === "none") return String(num); if (numberFormat === "comma") return num.toLocaleString(); if (numberFormat === "currency") return num.toLocaleString() + currencySuffix; return String(num); }; // 쿼리 바인딩된 값 가져오기 const getCalcItemValue = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => { if (item.fieldName && queryResult && queryResult.rows && queryResult.rows.length > 0) { const row = queryResult.rows[0]; const val = row[item.fieldName]; return typeof val === "number" ? val : parseFloat(String(val)) || 0; } return typeof item.value === "number" ? item.value : parseFloat(String(item.value)) || 0; }; // 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용) let calcResult = 0; if (calcItems.length > 0) { // 첫 번째 항목은 기준값 calcResult = getCalcItemValue(calcItems[0] as { label: string; value: number | string; operator: string; fieldName?: string }); // 두 번째 항목부터 연산자 적용 for (let i = 1; i < calcItems.length; i++) { const item = calcItems[i]; const val = getCalcItemValue(item as { label: string; value: number | string; operator: string; fieldName?: string }); switch ((item as { operator: string }).operator) { case "+": calcResult += val; break; case "-": calcResult -= val; break; case "x": calcResult *= val; break; case "÷": calcResult = val !== 0 ? calcResult / val : calcResult; break; } } } const itemsHtml = calcItems .map((item: { label: string; value: number | string; operator: string; fieldName?: string }) => { const itemValue = getCalcItemValue(item); return `
${item.label} ${formatNumber(itemValue)}
`; }) .join(""); content = `
${itemsHtml}
${resultLabel} ${formatNumber(calcResult)}
`; } // 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 sortedPages = layoutConfig.pages.sort((a, b) => a.page_order - b.page_order); const totalPages = sortedPages.length; const pagesHTML = sortedPages .map((page, pageIndex) => generatePageHTML( Array.isArray(page.components) ? page.components : [], page.width, page.height, page.background_color, pageIndex, totalPages, ), ) .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로 저장'을 선택하세요.", }); }; // 이미지 URL을 Base64로 변환 const imageUrlToBase64 = async (url: string): Promise => { try { // 이미 Base64인 경우 그대로 반환 if (url.startsWith("data:")) { return url; } // 서버 이미지 URL을 fetch하여 Base64로 변환 const fullUrl = getFullImageUrl(url); const response = await fetch(fullUrl); const blob = await response.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.onerror = reject; reader.readAsDataURL(blob); }); } catch (error) { console.error("이미지 변환 실패:", error); return ""; } }; // WORD 다운로드 (백엔드 API 사용 - 컴포넌트 데이터 전송) const handleDownloadWord = async () => { setIsExporting(true); try { toast({ title: "처리 중", description: "WORD 파일을 생성하고 있습니다...", }); // 이미지를 Base64로 변환하여 컴포넌트 데이터에 포함 const pagesWithBase64 = await Promise.all( layoutConfig.pages.map(async (page) => { const componentsWithBase64 = await Promise.all( (Array.isArray(page.components) ? page.components : []).map(async (component) => { // 이미지가 있는 컴포넌트는 Base64로 변환 if (component.imageUrl) { try { const base64 = await imageUrlToBase64(component.imageUrl); return { ...component, imageBase64: base64 }; } catch { return component; } } return component; }), ); return { ...page, components: componentsWithBase64 }; }), ); // 쿼리 결과 수집 const queryResults: Record[] }> = {}; for (const page of layoutConfig.pages) { const pageComponents = Array.isArray(page.components) ? page.components : []; for (const component of pageComponents) { if (component.queryId) { const result = getQueryResult(component.queryId); if (result) { queryResults[component.queryId] = result; } } } } const fileName = reportDetail?.report?.report_name_kor || "리포트"; // 백엔드 API 호출 (컴포넌트 데이터 전송) const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.post( "/admin/reports/export-word", { layoutConfig: { ...layoutConfig, pages: pagesWithBase64 }, queryResults, fileName, }, { responseType: "blob" }, ); // Blob 다운로드 const blob = new Blob([response.data], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", }); 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) { console.error("WORD 변환 오류:", 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) => (
{/* 페이지 컨텐츠 */}
{(Array.isArray(page.components) ? 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 || "(인)"}
)}
)} {component.type === "pageNumber" && (() => { const format = component.pageNumberFormat || "number"; const pageIndex = layoutConfig.pages .sort((a, b) => a.page_order - b.page_order) .findIndex((p) => p.page_id === page.page_id); const totalPages = layoutConfig.pages.length; let pageNumberText = ""; switch (format) { case "number": pageNumberText = `${pageIndex + 1}`; break; case "numberTotal": pageNumberText = `${pageIndex + 1} / ${totalPages}`; break; case "koreanNumber": pageNumberText = `${pageIndex + 1} 페이지`; break; default: pageNumberText = `${pageIndex + 1}`; } return (
{pageNumberText}
); })()} {/* Card 컴포넌트 */} {component.type === "card" && (() => { const cardTitle = component.cardTitle || "정보 카드"; const cardItems = component.cardItems || []; const labelWidth = component.labelWidth || 80; const showCardTitle = component.showCardTitle !== false; const titleFontSize = component.titleFontSize || 14; const labelFontSize = component.labelFontSize || 13; const valueFontSize = component.valueFontSize || 13; const titleColor = component.titleColor || "#1e40af"; const labelColor = component.labelColor || "#374151"; const valueColor = component.valueColor || "#000000"; const borderColor = component.borderColor || "#e5e7eb"; // 쿼리 바인딩된 값 가져오기 const getCardValue = (item: { label: string; value: string; fieldName?: string }) => { if (item.fieldName && component.queryId) { const qResult = getQueryResult(component.queryId); if (qResult && qResult.rows && qResult.rows.length > 0) { const row = qResult.rows[0]; return row[item.fieldName] !== undefined ? String(row[item.fieldName]) : item.value; } } return item.value; }; return (
{showCardTitle && ( <>
{cardTitle}
)}
{cardItems.map((item: { label: string; value: string; fieldName?: string }, idx: number) => (
{item.label} {getCardValue(item)}
))}
); })()} {/* 계산 컴포넌트 */} {component.type === "calculation" && (() => { const calcItems = component.calcItems || []; const resultLabel = component.resultLabel || "합계"; const calcLabelWidth = component.labelWidth || 120; const calcLabelFontSize = component.labelFontSize || 13; const calcValueFontSize = component.valueFontSize || 13; const calcResultFontSize = component.resultFontSize || 16; const calcLabelColor = component.labelColor || "#374151"; const calcValueColor = component.valueColor || "#000000"; const calcResultColor = component.resultColor || "#2563eb"; const numberFormat = component.numberFormat || "currency"; const currencySuffix = component.currencySuffix || "원"; const borderColor = component.borderColor || "#374151"; // 숫자 포맷팅 함수 const formatNumber = (num: number): string => { if (numberFormat === "none") return String(num); if (numberFormat === "comma") return num.toLocaleString(); if (numberFormat === "currency") return num.toLocaleString() + currencySuffix; return String(num); }; // 쿼리 바인딩된 값 가져오기 const getCalcItemValue = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => { if (item.fieldName && component.queryId) { const qResult = getQueryResult(component.queryId); if (qResult && qResult.rows && qResult.rows.length > 0) { const row = qResult.rows[0]; const val = row[item.fieldName]; return typeof val === "number" ? val : parseFloat(String(val)) || 0; } } return typeof item.value === "number" ? item.value : parseFloat(String(item.value)) || 0; }; // 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용) let calcResult = 0; if (calcItems.length > 0) { // 첫 번째 항목은 기준값 calcResult = getCalcItemValue(calcItems[0] as { label: string; value: number | string; operator: string; fieldName?: string }); // 두 번째 항목부터 연산자 적용 for (let i = 1; i < calcItems.length; i++) { const item = calcItems[i]; const val = getCalcItemValue(item as { label: string; value: number | string; operator: string; fieldName?: string }); switch ((item as { operator: string }).operator) { case "+": calcResult += val; break; case "-": calcResult -= val; break; case "x": calcResult *= val; break; case "÷": calcResult = val !== 0 ? calcResult / val : calcResult; break; } } } return (
{calcItems.map((item: { label: string; value: number | string; operator: string; fieldName?: string }, idx: number) => { const itemValue = getCalcItemValue(item); return (
{item.label} {formatNumber(itemValue)}
); })}
{resultLabel} {formatNumber(calcResult)}
); })()}
); })}
))}
); }