인쇄 기능 개선 - 중복 호출 제거 및 레이아웃 정확도 향상
This commit is contained in:
parent
82a7ff62ee
commit
859d68fff8
|
|
@ -357,11 +357,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
height: snappedSize,
|
height: snappedSize,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Grid Snap 적용
|
// Grid Snap 적용
|
||||||
updateComponent(component.id, {
|
updateComponent(component.id, {
|
||||||
width: snapValueToGrid(boundedWidth),
|
width: snapValueToGrid(boundedWidth),
|
||||||
height: snapValueToGrid(boundedHeight),
|
height: snapValueToGrid(boundedHeight),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -444,17 +444,17 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
case "text":
|
case "text":
|
||||||
case "label":
|
case "label":
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${component.fontSize}px`,
|
fontSize: `${component.fontSize}px`,
|
||||||
color: component.fontColor,
|
color: component.fontColor,
|
||||||
fontWeight: component.fontWeight,
|
fontWeight: component.fontWeight,
|
||||||
textAlign: component.textAlign as "left" | "center" | "right",
|
textAlign: component.textAlign as "left" | "center" | "right",
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{displayValue}
|
{displayValue}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -534,7 +534,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
// 기본 테이블 (데이터 없을 때)
|
// 기본 테이블 (데이터 없을 때)
|
||||||
return (
|
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 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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -858,12 +858,12 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
// 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용)
|
// 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용)
|
||||||
const calculateResult = (): number => {
|
const calculateResult = (): number => {
|
||||||
if (calcItems.length === 0) return 0;
|
if (calcItems.length === 0) return 0;
|
||||||
|
|
||||||
// 첫 번째 항목은 기준값
|
// 첫 번째 항목은 기준값
|
||||||
let result = getCalcItemValue(
|
let result = getCalcItemValue(
|
||||||
calcItems[0] as { label: string; value: number | string; operator: string; fieldName?: string },
|
calcItems[0] as { label: string; value: number | string; operator: string; fieldName?: string },
|
||||||
);
|
);
|
||||||
|
|
||||||
// 두 번째 항목부터 연산자 적용
|
// 두 번째 항목부터 연산자 적용
|
||||||
for (let i = 1; i < calcItems.length; i++) {
|
for (let i = 1; i < calcItems.length; i++) {
|
||||||
const item = calcItems[i];
|
const item = calcItems[i];
|
||||||
|
|
@ -899,30 +899,30 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
||||||
item: { label: string; value: number | string; operator: string; fieldName?: string },
|
item: { label: string; value: number | string; operator: string; fieldName?: string },
|
||||||
index: number,
|
index: number,
|
||||||
) => {
|
) => {
|
||||||
const itemValue = getCalcItemValue(item);
|
const itemValue = getCalcItemValue(item);
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex items-center justify-between py-1">
|
<div key={index} className="flex items-center justify-between py-1">
|
||||||
<span
|
<span
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
width: `${calcLabelWidth}px`,
|
width: `${calcLabelWidth}px`,
|
||||||
fontSize: `${calcLabelFontSize}px`,
|
fontSize: `${calcLabelFontSize}px`,
|
||||||
color: calcLabelColor,
|
color: calcLabelColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="text-right"
|
className="text-right"
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${calcValueFontSize}px`,
|
fontSize: `${calcValueFontSize}px`,
|
||||||
color: calcValueColor,
|
color: calcValueColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formatNumber(itemValue)}
|
{formatNumber(itemValue)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ import { getFullImageUrl } from "@/lib/api/client";
|
||||||
import JsBarcode from "jsbarcode";
|
import JsBarcode from "jsbarcode";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
// mm -> px 변환 상수
|
||||||
|
const MM_TO_PX = 4;
|
||||||
|
|
||||||
interface ReportPreviewModalProps {
|
interface ReportPreviewModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -149,8 +152,8 @@ function PreviewWatermarkLayer({ watermark, pageWidth, pageHeight }: PreviewWate
|
||||||
// 타일 스타일
|
// 타일 스타일
|
||||||
if (watermark.style === "tile") {
|
if (watermark.style === "tile") {
|
||||||
const tileSize = watermark.type === "text" ? (watermark.fontSize || 48) * 4 : 150;
|
const tileSize = watermark.type === "text" ? (watermark.fontSize || 48) * 4 : 150;
|
||||||
const cols = Math.ceil((pageWidth * 3.7795) / tileSize) + 2;
|
const cols = Math.ceil((pageWidth * MM_TO_PX) / tileSize) + 2;
|
||||||
const rows = Math.ceil((pageHeight * 3.7795) / tileSize) + 2;
|
const rows = Math.ceil((pageHeight * MM_TO_PX) / tileSize) + 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
|
|
@ -514,7 +517,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
||||||
|
|
||||||
printWindow.document.write(printHtml);
|
printWindow.document.write(printHtml);
|
||||||
printWindow.document.close();
|
printWindow.document.close();
|
||||||
printWindow.print();
|
// print()는 HTML 내 스크립트에서 이미지 로드 완료 후 자동 호출됨
|
||||||
};
|
};
|
||||||
|
|
||||||
// 워터마크 HTML 생성 헬퍼 함수
|
// 워터마크 HTML 생성 헬퍼 함수
|
||||||
|
|
@ -554,8 +557,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
||||||
|
|
||||||
if (watermark.style === "tile") {
|
if (watermark.style === "tile") {
|
||||||
const tileSize = watermark.type === "text" ? (watermark.fontSize || 48) * 4 : 150;
|
const tileSize = watermark.type === "text" ? (watermark.fontSize || 48) * 4 : 150;
|
||||||
const cols = Math.ceil((pageWidth * 3.7795) / tileSize) + 2;
|
const cols = Math.ceil((pageWidth * MM_TO_PX) / tileSize) + 2;
|
||||||
const rows = Math.ceil((pageHeight * 3.7795) / tileSize) + 2;
|
const rows = Math.ceil((pageHeight * MM_TO_PX) / tileSize) + 2;
|
||||||
const tileItems = Array.from({ length: rows * cols })
|
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>`)
|
.map(() => `<div style="width: ${tileSize}px; height: ${tileSize}px; display: flex; align-items: center; justify-content: center;">${textContent}</div>`)
|
||||||
.join("");
|
.join("");
|
||||||
|
|
@ -650,9 +653,9 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
content = `
|
content = `
|
||||||
<div style="display: flex; align-items: center; gap: 8px; height: 100%;">
|
<div style="display: flex; align-items: center; gap: 8px; width: 100%; height: 100%;">
|
||||||
${personName ? `<div style="font-size: 12px;">${personName}</div>` : ""}
|
${personName ? `<div style="font-size: 12px; white-space: nowrap;">${personName}</div>` : ""}
|
||||||
<div style="position: relative; width: ${component.width}px; height: ${component.height}px;">
|
<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%;" />` : ""}
|
${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>` : ""}
|
${showLabel ? `<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px; font-weight: bold; color: #dc2626;">${labelText}</div>` : ""}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -891,8 +894,15 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
||||||
</table>`;
|
</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 `
|
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}
|
${content}
|
||||||
</div>`;
|
</div>`;
|
||||||
})
|
})
|
||||||
|
|
@ -901,7 +911,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
||||||
const watermarkHTML = generateWatermarkHTML(watermark, pageWidth, pageHeight);
|
const watermarkHTML = generateWatermarkHTML(watermark, pageWidth, pageHeight);
|
||||||
|
|
||||||
return `
|
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}
|
${watermarkHTML}
|
||||||
${componentsHTML}
|
${componentsHTML}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
@ -933,20 +943,18 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>리포트 인쇄</title>
|
<title>리포트 인쇄</title>
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
@page {
|
@page {
|
||||||
size: A4;
|
size: A4;
|
||||||
margin: 10mm;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@media print {
|
@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 { page-break-after: always; page-break-inside: avoid; }
|
||||||
.print-page:last-child { page-break-after: auto; }
|
.print-page:last-child { page-break-after: auto; }
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
-webkit-print-color-adjust: exact;
|
-webkit-print-color-adjust: exact;
|
||||||
print-color-adjust: exact;
|
print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
|
|
@ -1070,15 +1078,15 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
||||||
const barcodeImage = await generateBarcodeImage(component);
|
const barcodeImage = await generateBarcodeImage(component);
|
||||||
return { ...component, barcodeImageBase64: barcodeImage };
|
return { ...component, barcodeImageBase64: barcodeImage };
|
||||||
} catch {
|
} catch {
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return component;
|
return component;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return { ...page, components: componentsWithBase64 };
|
return { ...page, components: componentsWithBase64 };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 쿼리 결과 수집
|
// 쿼리 결과 수집
|
||||||
const queryResults: Record<string, { fields: string[]; rows: Record<string, unknown>[] }> = {};
|
const queryResults: Record<string, { fields: string[]; rows: Record<string, unknown>[] }> = {};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue