인쇄 기능 개선 - 중복 호출 제거 및 레이아웃 정확도 향상

This commit is contained in:
dohyeons 2025-12-23 17:37:22 +09:00
parent 82a7ff62ee
commit 859d68fff8
2 changed files with 68 additions and 60 deletions

View File

@ -357,11 +357,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
height: snappedSize,
});
} else {
// Grid Snap 적용
updateComponent(component.id, {
width: snapValueToGrid(boundedWidth),
height: snapValueToGrid(boundedHeight),
});
// Grid Snap 적용
updateComponent(component.id, {
width: snapValueToGrid(boundedWidth),
height: snapValueToGrid(boundedHeight),
});
}
}
};
@ -444,17 +444,17 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
case "text":
case "label":
return (
<div
<div
className="h-full w-full"
style={{
fontSize: `${component.fontSize}px`,
color: component.fontColor,
fontWeight: component.fontWeight,
textAlign: component.textAlign as "left" | "center" | "right",
whiteSpace: "pre-wrap",
}}
>
{displayValue}
style={{
fontSize: `${component.fontSize}px`,
color: component.fontColor,
fontWeight: component.fontWeight,
textAlign: component.textAlign as "left" | "center" | "right",
whiteSpace: "pre-wrap",
}}
>
{displayValue}
</div>
);
@ -534,7 +534,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
// 기본 테이블 (데이터 없을 때)
return (
<div className="flex h-full w-full items-center justify-center border-2 border-dashed border-gray-300 bg-gray-50 text-xs text-gray-400">
</div>
);
@ -899,30 +899,30 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
item: { label: string; value: number | string; operator: string; fieldName?: string },
index: number,
) => {
const itemValue = getCalcItemValue(item);
return (
<div key={index} className="flex items-center justify-between py-1">
<span
className="flex-shrink-0"
style={{
width: `${calcLabelWidth}px`,
fontSize: `${calcLabelFontSize}px`,
color: calcLabelColor,
}}
>
{item.label}
</span>
<span
className="text-right"
style={{
fontSize: `${calcValueFontSize}px`,
color: calcValueColor,
}}
>
{formatNumber(itemValue)}
</span>
</div>
);
const itemValue = getCalcItemValue(item);
return (
<div key={index} className="flex items-center justify-between py-1">
<span
className="flex-shrink-0"
style={{
width: `${calcLabelWidth}px`,
fontSize: `${calcLabelFontSize}px`,
color: calcLabelColor,
}}
>
{item.label}
</span>
<span
className="text-right"
style={{
fontSize: `${calcValueFontSize}px`,
color: calcValueColor,
}}
>
{formatNumber(itemValue)}
</span>
</div>
);
},
)}
</div>

View File

@ -17,6 +17,9 @@ import { getFullImageUrl } from "@/lib/api/client";
import JsBarcode from "jsbarcode";
import QRCode from "qrcode";
// mm -> px 변환 상수
const MM_TO_PX = 4;
interface ReportPreviewModalProps {
isOpen: boolean;
onClose: () => void;
@ -149,8 +152,8 @@ function PreviewWatermarkLayer({ watermark, pageWidth, pageHeight }: PreviewWate
// 타일 스타일
if (watermark.style === "tile") {
const tileSize = watermark.type === "text" ? (watermark.fontSize || 48) * 4 : 150;
const cols = Math.ceil((pageWidth * 3.7795) / tileSize) + 2;
const rows = Math.ceil((pageHeight * 3.7795) / tileSize) + 2;
const cols = Math.ceil((pageWidth * MM_TO_PX) / tileSize) + 2;
const rows = Math.ceil((pageHeight * MM_TO_PX) / tileSize) + 2;
return (
<div style={baseStyle}>
@ -514,7 +517,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
printWindow.document.write(printHtml);
printWindow.document.close();
printWindow.print();
// print()는 HTML 내 스크립트에서 이미지 로드 완료 후 자동 호출됨
};
// 워터마크 HTML 생성 헬퍼 함수
@ -554,8 +557,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
if (watermark.style === "tile") {
const tileSize = watermark.type === "text" ? (watermark.fontSize || 48) * 4 : 150;
const cols = Math.ceil((pageWidth * 3.7795) / tileSize) + 2;
const rows = Math.ceil((pageHeight * 3.7795) / tileSize) + 2;
const cols = Math.ceil((pageWidth * MM_TO_PX) / tileSize) + 2;
const rows = Math.ceil((pageHeight * MM_TO_PX) / tileSize) + 2;
const tileItems = Array.from({ length: rows * cols })
.map(() => `<div style="width: ${tileSize}px; height: ${tileSize}px; display: flex; align-items: center; justify-content: center;">${textContent}</div>`)
.join("");
@ -650,9 +653,9 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
: "";
content = `
<div style="display: flex; align-items: center; gap: 8px; height: 100%;">
${personName ? `<div style="font-size: 12px;">${personName}</div>` : ""}
<div style="position: relative; width: ${component.width}px; height: ${component.height}px;">
<div style="display: flex; align-items: center; gap: 8px; width: 100%; height: 100%;">
${personName ? `<div style="font-size: 12px; white-space: nowrap;">${personName}</div>` : ""}
<div style="position: relative; flex: 1; height: 100%;">
${imageUrl ? `<img src="${imageUrl}" style="width: 100%; height: 100%; object-fit: ${component.objectFit || "contain"}; border-radius: 50%;" />` : ""}
${showLabel ? `<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px; font-weight: bold; color: #dc2626;">${labelText}</div>` : ""}
</div>
@ -891,8 +894,15 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
</table>`;
}
// 컴포넌트 값은 px로 저장됨 (캔버스는 pageWidth * MM_TO_PX px)
// 인쇄용 mm 단위로 변환: px / MM_TO_PX = mm
const xMm = component.x / MM_TO_PX;
const yMm = component.y / MM_TO_PX;
const widthMm = component.width / MM_TO_PX;
const heightMm = component.height / MM_TO_PX;
return `
<div style="position: absolute; left: ${component.x}px; top: ${component.y}px; width: ${component.width}px; height: ${component.height}px; background-color: ${component.backgroundColor || "transparent"}; border: ${component.borderWidth ? `${component.borderWidth}px solid ${component.borderColor}` : "none"}; padding: 8px; box-sizing: border-box;">
<div style="position: absolute; left: ${xMm}mm; top: ${yMm}mm; width: ${widthMm}mm; height: ${heightMm}mm; background-color: ${component.backgroundColor || "transparent"}; border: ${component.borderWidth ? `${component.borderWidth}px solid ${component.borderColor}` : "none"}; box-sizing: border-box; overflow: hidden;">
${content}
</div>`;
})
@ -901,7 +911,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
const watermarkHTML = generateWatermarkHTML(watermark, pageWidth, pageHeight);
return `
<div style="position: relative; width: ${pageWidth}mm; min-height: ${pageHeight}mm; background-color: ${backgroundColor}; margin: 0 auto;">
<div class="print-page" style="position: relative; width: ${pageWidth}mm; min-height: ${pageHeight}mm; background-color: ${backgroundColor};">
${watermarkHTML}
${componentsHTML}
</div>`;
@ -933,20 +943,18 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
<meta charset="UTF-8">
<title> </title>
<style>
* { box-sizing: border-box; }
* { box-sizing: border-box; margin: 0; padding: 0; }
@page {
size: A4;
margin: 10mm;
margin: 0;
}
@media print {
body { margin: 0; padding: 0; }
html, body { width: 210mm; height: 297mm; }
.print-page { page-break-after: always; page-break-inside: avoid; }
.print-page:last-child { page-break-after: auto; }
}
body {
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
margin: 0;
padding: 20px;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
@ -1070,15 +1078,15 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
const barcodeImage = await generateBarcodeImage(component);
return { ...component, barcodeImageBase64: barcodeImage };
} catch {
return component;
return component;
}
}
return component;
})
);
);
return { ...page, components: componentsWithBase64 };
})
);
);
// 쿼리 결과 수집
const queryResults: Record<string, { fields: string[]; rows: Record<string, unknown>[] }> = {};