미리보기/인쇄에 바코드, QR코드, 체크박스 렌더링 추가
This commit is contained in:
parent
2b912105a8
commit
0decfe95de
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { ComponentConfig } from "@/types/report";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { getFullImageUrl } from "@/lib/api/client";
|
||||
|
|
|
|||
|
|
@ -11,15 +11,162 @@ import {
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Printer, FileDown, FileText } from "lucide-react";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { getFullImageUrl } from "@/lib/api/client";
|
||||
import JsBarcode from "jsbarcode";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
interface ReportPreviewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// 바코드/QR코드 미리보기 컴포넌트
|
||||
function BarcodePreview({
|
||||
component,
|
||||
getQueryResult,
|
||||
}: {
|
||||
component: any;
|
||||
getQueryResult: (queryId: string) => { fields: string[]; rows: Record<string, unknown>[] } | null;
|
||||
}) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const barcodeType = component.barcodeType || "CODE128";
|
||||
const isQR = barcodeType === "QR";
|
||||
|
||||
// 바코드 값 결정
|
||||
const getBarcodeValue = (): string => {
|
||||
// QR코드 다중 필드 모드
|
||||
if (
|
||||
isQR &&
|
||||
component.qrUseMultiField &&
|
||||
component.qrDataFields &&
|
||||
component.qrDataFields.length > 0 &&
|
||||
component.queryId
|
||||
) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
if (component.qrIncludeAllRows) {
|
||||
const allRowsData: Record<string, string>[] = [];
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rowData: Record<string, string> = {};
|
||||
component.qrDataFields.forEach((field: { fieldName: string; label: string }) => {
|
||||
if (field.fieldName && field.label) {
|
||||
const val = row[field.fieldName];
|
||||
rowData[field.label] = val !== null && val !== undefined ? String(val) : "";
|
||||
}
|
||||
});
|
||||
allRowsData.push(rowData);
|
||||
});
|
||||
return JSON.stringify(allRowsData);
|
||||
}
|
||||
const row = queryResult.rows[0];
|
||||
const jsonData: Record<string, string> = {};
|
||||
component.qrDataFields.forEach((field: { fieldName: string; label: string }) => {
|
||||
if (field.fieldName && field.label) {
|
||||
const val = row[field.fieldName];
|
||||
jsonData[field.label] = val !== null && val !== undefined ? String(val) : "";
|
||||
}
|
||||
});
|
||||
return JSON.stringify(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
// 단일 필드 바인딩
|
||||
if (component.barcodeFieldName && component.queryId) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
if (isQR && component.qrIncludeAllRows) {
|
||||
const allValues = queryResult.rows
|
||||
.map((row) => {
|
||||
const val = row[component.barcodeFieldName];
|
||||
return val !== null && val !== undefined ? String(val) : "";
|
||||
})
|
||||
.filter((v) => v !== "");
|
||||
return JSON.stringify(allValues);
|
||||
}
|
||||
const row = queryResult.rows[0];
|
||||
const val = row[component.barcodeFieldName];
|
||||
if (val !== null && val !== undefined) {
|
||||
return String(val);
|
||||
}
|
||||
}
|
||||
return `{${component.barcodeFieldName}}`;
|
||||
}
|
||||
return component.barcodeValue || "SAMPLE123";
|
||||
};
|
||||
|
||||
const barcodeValue = getBarcodeValue();
|
||||
|
||||
useEffect(() => {
|
||||
setError(null);
|
||||
|
||||
if (isQR) {
|
||||
// QR코드 렌더링
|
||||
if (canvasRef.current && barcodeValue) {
|
||||
const bgColor = component.barcodeBackground === "transparent" ? "#ffffff" : (component.barcodeBackground || "#ffffff");
|
||||
QRCode.toCanvas(
|
||||
canvasRef.current,
|
||||
barcodeValue,
|
||||
{
|
||||
width: Math.min(component.width, component.height) - 20,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: component.barcodeColor || "#000000",
|
||||
light: bgColor,
|
||||
},
|
||||
errorCorrectionLevel: component.qrErrorCorrectionLevel || "M",
|
||||
},
|
||||
(err) => {
|
||||
if (err) setError(err.message || "QR코드 생성 실패");
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 1D 바코드 렌더링
|
||||
if (svgRef.current && barcodeValue) {
|
||||
try {
|
||||
const bgColor = component.barcodeBackground === "transparent" ? "" : (component.barcodeBackground || "#ffffff");
|
||||
JsBarcode(svgRef.current, barcodeValue.trim(), {
|
||||
format: barcodeType.toLowerCase(),
|
||||
width: 2,
|
||||
height: Math.max(30, component.height - 40),
|
||||
displayValue: component.showBarcodeText !== false,
|
||||
lineColor: component.barcodeColor || "#000000",
|
||||
background: bgColor,
|
||||
margin: component.barcodeMargin ?? 10,
|
||||
fontSize: 12,
|
||||
textMargin: 2,
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err?.message || "바코드 생성 실패");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [barcodeValue, barcodeType, isQR, component]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", color: "#ef4444", fontSize: "12px" }}>
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", backgroundColor: component.barcodeBackground || "transparent" }}>
|
||||
{isQR ? (
|
||||
<canvas ref={canvasRef} style={{ maxWidth: "100%", maxHeight: "100%" }} />
|
||||
) : (
|
||||
<svg ref={svgRef} style={{ maxWidth: "100%", maxHeight: "100%" }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) {
|
||||
const { layoutConfig, getQueryResult, reportDetail } = useReportDesigner();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
|
@ -40,9 +187,131 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
return component.defaultValue || "텍스트";
|
||||
};
|
||||
|
||||
const handlePrint = () => {
|
||||
// 바코드/QR코드를 base64 이미지로 변환
|
||||
const generateBarcodeImage = async (component: any): Promise<string | null> => {
|
||||
const barcodeType = component.barcodeType || "CODE128";
|
||||
const isQR = barcodeType === "QR";
|
||||
|
||||
// 바코드 값 결정
|
||||
const getBarcodeValue = (): string => {
|
||||
if (
|
||||
isQR &&
|
||||
component.qrUseMultiField &&
|
||||
component.qrDataFields &&
|
||||
component.qrDataFields.length > 0 &&
|
||||
component.queryId
|
||||
) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
if (component.qrIncludeAllRows) {
|
||||
const allRowsData: Record<string, string>[] = [];
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rowData: Record<string, string> = {};
|
||||
component.qrDataFields.forEach((field: { fieldName: string; label: string }) => {
|
||||
if (field.fieldName && field.label) {
|
||||
const val = row[field.fieldName];
|
||||
rowData[field.label] = val !== null && val !== undefined ? String(val) : "";
|
||||
}
|
||||
});
|
||||
allRowsData.push(rowData);
|
||||
});
|
||||
return JSON.stringify(allRowsData);
|
||||
}
|
||||
const row = queryResult.rows[0];
|
||||
const jsonData: Record<string, string> = {};
|
||||
component.qrDataFields.forEach((field: { fieldName: string; label: string }) => {
|
||||
if (field.fieldName && field.label) {
|
||||
const val = row[field.fieldName];
|
||||
jsonData[field.label] = val !== null && val !== undefined ? String(val) : "";
|
||||
}
|
||||
});
|
||||
return JSON.stringify(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
if (component.barcodeFieldName && component.queryId) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
if (isQR && component.qrIncludeAllRows) {
|
||||
const allValues = queryResult.rows
|
||||
.map((row) => {
|
||||
const val = row[component.barcodeFieldName];
|
||||
return val !== null && val !== undefined ? String(val) : "";
|
||||
})
|
||||
.filter((v) => v !== "");
|
||||
return JSON.stringify(allValues);
|
||||
}
|
||||
const row = queryResult.rows[0];
|
||||
const val = row[component.barcodeFieldName];
|
||||
if (val !== null && val !== undefined) {
|
||||
return String(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return component.barcodeValue || "SAMPLE123";
|
||||
};
|
||||
|
||||
const barcodeValue = getBarcodeValue();
|
||||
|
||||
try {
|
||||
if (isQR) {
|
||||
// QR코드를 canvas에 렌더링 후 base64로 변환
|
||||
const canvas = document.createElement("canvas");
|
||||
const bgColor = component.barcodeBackground === "transparent" ? "#ffffff" : (component.barcodeBackground || "#ffffff");
|
||||
await QRCode.toCanvas(canvas, barcodeValue, {
|
||||
width: Math.min(component.width, component.height) - 10,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: component.barcodeColor || "#000000",
|
||||
light: bgColor,
|
||||
},
|
||||
errorCorrectionLevel: component.qrErrorCorrectionLevel || "M",
|
||||
});
|
||||
return canvas.toDataURL("image/png");
|
||||
} else {
|
||||
// 1D 바코드를 SVG로 렌더링 후 base64로 변환
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
const bgColor = component.barcodeBackground === "transparent" ? "" : (component.barcodeBackground || "#ffffff");
|
||||
JsBarcode(svg, barcodeValue.trim(), {
|
||||
format: barcodeType.toLowerCase(),
|
||||
width: 2,
|
||||
height: Math.max(30, component.height - 40),
|
||||
displayValue: component.showBarcodeText !== false,
|
||||
lineColor: component.barcodeColor || "#000000",
|
||||
background: bgColor,
|
||||
margin: component.barcodeMargin ?? 10,
|
||||
fontSize: 12,
|
||||
textMargin: 2,
|
||||
});
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const svgBase64 = btoa(unescape(encodeURIComponent(svgData)));
|
||||
return `data:image/svg+xml;base64,${svgBase64}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("바코드 생성 오류:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrint = async () => {
|
||||
// 바코드 이미지 미리 생성
|
||||
const pagesWithBarcodes = await Promise.all(
|
||||
layoutConfig.pages.map(async (page) => {
|
||||
const componentsWithBarcodes = await Promise.all(
|
||||
(Array.isArray(page.components) ? page.components : []).map(async (component) => {
|
||||
if (component.type === "barcode") {
|
||||
const barcodeImage = await generateBarcodeImage(component);
|
||||
return { ...component, barcodeImageBase64: barcodeImage };
|
||||
}
|
||||
return component;
|
||||
})
|
||||
);
|
||||
return { ...page, components: componentsWithBarcodes };
|
||||
})
|
||||
);
|
||||
|
||||
// HTML 생성하여 인쇄
|
||||
const printHtml = generatePrintHTML();
|
||||
const printHtml = generatePrintHTML(pagesWithBarcodes);
|
||||
|
||||
const printWindow = window.open("", "_blank");
|
||||
if (!printWindow) return;
|
||||
|
|
@ -298,6 +567,46 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
</div>`;
|
||||
}
|
||||
|
||||
// 바코드/QR코드 컴포넌트 (인쇄용 - base64 이미지 사용)
|
||||
else if (component.type === "barcode") {
|
||||
// 바코드 이미지는 미리 생성된 base64 사용 (handlePrint에서 생성)
|
||||
const barcodeImage = (component as any).barcodeImageBase64;
|
||||
if (barcodeImage) {
|
||||
content = `<img src="${barcodeImage}" style="max-width: 100%; max-height: 100%; object-fit: contain;" />`;
|
||||
} else {
|
||||
content = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 12px;">바코드</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 체크박스 컴포넌트 (인쇄용)
|
||||
else if (component.type === "checkbox") {
|
||||
const checkboxSize = component.checkboxSize || 18;
|
||||
const checkboxColor = component.checkboxColor || "#2563eb";
|
||||
const checkboxBorderColor = component.checkboxBorderColor || "#6b7280";
|
||||
const checkboxLabel = component.checkboxLabel || "";
|
||||
const checkboxLabelPosition = component.checkboxLabelPosition || "right";
|
||||
|
||||
// 체크 상태 결정
|
||||
let isChecked = component.checkboxChecked === true;
|
||||
if (component.checkboxFieldName && queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
const val = queryResult.rows[0][component.checkboxFieldName];
|
||||
isChecked = val === true || val === "Y" || val === "1" || val === 1 || val === "true";
|
||||
}
|
||||
|
||||
const checkboxHTML = `
|
||||
<div style="width: ${checkboxSize}px; height: ${checkboxSize}px; border: 2px solid ${isChecked ? checkboxColor : checkboxBorderColor}; border-radius: 2px; background-color: ${isChecked ? checkboxColor : "transparent"}; display: flex; align-items: center; justify-content: center;">
|
||||
${isChecked ? `<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="width: ${checkboxSize * 0.7}px; height: ${checkboxSize * 0.7}px;"><polyline points="20 6 9 17 4 12" /></svg>` : ""}
|
||||
</div>
|
||||
`;
|
||||
|
||||
content = `
|
||||
<div style="display: flex; align-items: center; gap: 8px; height: 100%; flex-direction: ${checkboxLabelPosition === "left" ? "row-reverse" : "row"}; ${checkboxLabelPosition === "left" ? "justify-content: flex-end;" : ""}">
|
||||
${checkboxHTML}
|
||||
${checkboxLabel ? `<span style="font-size: 12px;">${checkboxLabel}</span>` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Table 컴포넌트
|
||||
else if (component.type === "table" && queryResult && queryResult.rows.length > 0) {
|
||||
const columns =
|
||||
|
|
@ -347,8 +656,9 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
};
|
||||
|
||||
// 모든 페이지 HTML 생성 (인쇄/PDF용)
|
||||
const generatePrintHTML = (): string => {
|
||||
const sortedPages = layoutConfig.pages.sort((a, b) => a.page_order - b.page_order);
|
||||
const generatePrintHTML = (pagesWithBarcodes?: any[]): string => {
|
||||
const pages = pagesWithBarcodes || layoutConfig.pages;
|
||||
const sortedPages = pages.sort((a, b) => a.page_order - b.page_order);
|
||||
const totalPages = sortedPages.length;
|
||||
|
||||
const pagesHTML = sortedPages
|
||||
|
|
@ -422,8 +732,24 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
};
|
||||
|
||||
// PDF 다운로드 (브라우저 인쇄 기능 이용)
|
||||
const handleDownloadPDF = () => {
|
||||
const printHtml = generatePrintHTML();
|
||||
const handleDownloadPDF = async () => {
|
||||
// 바코드 이미지 미리 생성
|
||||
const pagesWithBarcodes = await Promise.all(
|
||||
layoutConfig.pages.map(async (page) => {
|
||||
const componentsWithBarcodes = await Promise.all(
|
||||
(Array.isArray(page.components) ? page.components : []).map(async (component) => {
|
||||
if (component.type === "barcode") {
|
||||
const barcodeImage = await generateBarcodeImage(component);
|
||||
return { ...component, barcodeImageBase64: barcodeImage };
|
||||
}
|
||||
return component;
|
||||
})
|
||||
);
|
||||
return { ...page, components: componentsWithBarcodes };
|
||||
})
|
||||
);
|
||||
|
||||
const printHtml = generatePrintHTML(pagesWithBarcodes);
|
||||
|
||||
const printWindow = window.open("", "_blank");
|
||||
if (!printWindow) return;
|
||||
|
|
@ -1113,6 +1439,76 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 바코드/QR코드 컴포넌트 */}
|
||||
{component.type === "barcode" && (
|
||||
<BarcodePreview component={component} getQueryResult={getQueryResult} />
|
||||
)}
|
||||
|
||||
{/* 체크박스 컴포넌트 */}
|
||||
{component.type === "checkbox" && (() => {
|
||||
const checkboxSize = component.checkboxSize || 18;
|
||||
const checkboxColor = component.checkboxColor || "#2563eb";
|
||||
const checkboxBorderColor = component.checkboxBorderColor || "#6b7280";
|
||||
const checkboxLabel = component.checkboxLabel || "";
|
||||
const checkboxLabelPosition = component.checkboxLabelPosition || "right";
|
||||
|
||||
// 체크 상태 결정
|
||||
let isChecked = component.checkboxChecked === true;
|
||||
if (component.checkboxFieldName && component.queryId) {
|
||||
const qResult = getQueryResult(component.queryId);
|
||||
if (qResult && qResult.rows && qResult.rows.length > 0) {
|
||||
const val = qResult.rows[0][component.checkboxFieldName];
|
||||
isChecked = val === true || val === "Y" || val === "1" || val === 1 || val === "true";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
height: "100%",
|
||||
flexDirection: checkboxLabelPosition === "left" ? "row-reverse" : "row",
|
||||
justifyContent: checkboxLabelPosition === "left" ? "flex-end" : "flex-start",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${checkboxSize}px`,
|
||||
height: `${checkboxSize}px`,
|
||||
borderRadius: "2px",
|
||||
border: `2px solid ${isChecked ? checkboxColor : checkboxBorderColor}`,
|
||||
backgroundColor: isChecked ? checkboxColor : "transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{isChecked && (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
width: `${checkboxSize * 0.7}px`,
|
||||
height: `${checkboxSize * 0.7}px`,
|
||||
}}
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{checkboxLabel && (
|
||||
<span style={{ fontSize: "12px" }}>{checkboxLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Reference in New Issue