미리보기/인쇄에 바코드, QR코드, 체크박스 렌더링 추가

This commit is contained in:
dohyeons 2025-12-22 13:58:12 +09:00
parent 2b912105a8
commit 0decfe95de
2 changed files with 404 additions and 8 deletions

View File

@ -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";

View File

@ -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>
);
})}