인쇄 기능 개선 - 중복 호출 제거 및 레이아웃 정확도 향상
This commit is contained in:
parent
82a7ff62ee
commit
859d68fff8
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>[] }> = {};
|
||||
|
|
|
|||
Loading…
Reference in New Issue