diff --git a/backend-node/src/controllers/reportController.ts b/backend-node/src/controllers/reportController.ts index c6605d3e..d334e46e 100644 --- a/backend-node/src/controllers/reportController.ts +++ b/backend-node/src/controllers/reportController.ts @@ -30,6 +30,7 @@ import { Header, Footer, HeadingLevel, + TableLayoutType, } from "docx"; import { WatermarkConfig } from "../types/report"; import bwipjs from "bwip-js"; @@ -592,8 +593,12 @@ export class ReportController { // mm를 twip으로 변환 const mmToTwip = (mm: number) => convertMillimetersToTwip(mm); - // px를 twip으로 변환 (1px = 15twip at 96DPI) - const pxToTwip = (px: number) => Math.round(px * 15); + + // 프론트엔드와 동일한 MM_TO_PX 상수 (캔버스에서 mm를 px로 변환할 때 사용하는 값) + const MM_TO_PX = 4; + // 1mm = 56.692913386 twip (docx 라이브러리 기준) + // px를 twip으로 변환: px -> mm -> twip + const pxToTwip = (px: number) => Math.round((px / MM_TO_PX) * 56.692913386); // 쿼리 결과 맵 const queryResultsMap: Record< @@ -726,6 +731,9 @@ export class ReportController { const base64Data = component.imageBase64.split(",")[1] || component.imageBase64; const imageBuffer = Buffer.from(base64Data, "base64"); + // 서명 이미지 크기: 라벨 옆에 인라인으로 표시될 수 있도록 적절한 크기로 조정 + const sigImageHeight = 30; // 고정 높이 (약 40px) + const sigImageWidth = Math.round((component.width / component.height) * sigImageHeight) || 80; result.push( new ParagraphRef({ children: [ @@ -733,8 +741,8 @@ export class ReportController { new ImageRunRef({ data: imageBuffer, transformation: { - width: Math.round(component.width * 0.75), - height: Math.round(component.height * 0.75), + width: sigImageWidth, + height: sigImageHeight, }, type: "png", }), @@ -1443,7 +1451,11 @@ export class ReportController { try { const barcodeType = component.barcodeType || "CODE128"; const barcodeColor = (component.barcodeColor || "#000000").replace("#", ""); - const barcodeBackground = (component.barcodeBackground || "#ffffff").replace("#", ""); + // transparent는 bwip-js에서 지원하지 않으므로 흰색으로 변환 + let barcodeBackground = (component.barcodeBackground || "#ffffff").replace("#", ""); + if (barcodeBackground === "transparent" || barcodeBackground === "") { + barcodeBackground = "ffffff"; + } // 바코드 값 결정 (쿼리 바인딩 또는 고정값) let barcodeValue = component.barcodeValue || "SAMPLE123"; @@ -1739,6 +1751,7 @@ export class ReportController { const rowTable = new Table({ rows: [new TableRow({ children: cells })], width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, // 셀 너비 고정 borders: { top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, @@ -1821,6 +1834,7 @@ export class ReportController { const textTable = new Table({ rows: [new TableRow({ children: [textCell] })], width: { size: pxToTwip(component.width), type: WidthType.DXA }, + layout: TableLayoutType.FIXED, // 셀 너비 고정 indent: { size: indentLeft, type: WidthType.DXA }, borders: { top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, @@ -1970,6 +1984,10 @@ export class ReportController { component.imageBase64.split(",")[1] || component.imageBase64; const imageBuffer = Buffer.from(base64Data, "base64"); + // 서명 이미지 크기: 라벨 옆에 인라인으로 표시될 수 있도록 적절한 크기로 조정 + const sigImageHeight = 30; // 고정 높이 + const sigImageWidth = Math.round((component.width / component.height) * sigImageHeight) || 80; + const paragraph = new Paragraph({ spacing: { before: spacingBefore, after: 0 }, indent: { left: indentLeft }, @@ -1978,8 +1996,8 @@ export class ReportController { new ImageRun({ data: imageBuffer, transformation: { - width: Math.round(component.width * 0.75), - height: Math.round(component.height * 0.75), + width: sigImageWidth, + height: sigImageHeight, }, type: "png", }), diff --git a/frontend/components/report/designer/ReportPreviewModal.tsx b/frontend/components/report/designer/ReportPreviewModal.tsx index b8fcb9ce..0ba67bae 100644 --- a/frontend/components/report/designer/ReportPreviewModal.tsx +++ b/frontend/components/report/designer/ReportPreviewModal.tsx @@ -1052,7 +1052,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) description: "WORD 파일을 생성하고 있습니다...", }); - // 이미지를 Base64로 변환하여 컴포넌트 데이터에 포함 + // 이미지 및 바코드를 Base64로 변환하여 컴포넌트 데이터에 포함 const pagesWithBase64 = await Promise.all( layoutConfig.pages.map(async (page) => { const componentsWithBase64 = await Promise.all( @@ -1066,12 +1066,21 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) return component; } } + // 바코드/QR코드 컴포넌트는 이미지로 변환 + if (component.type === "barcode") { + try { + const barcodeImage = await generateBarcodeImage(component); + return { ...component, barcodeImageBase64: barcodeImage }; + } catch { + return component; + } + } return component; - }), - ); + }) + ); return { ...page, components: componentsWithBase64 }; - }), - ); + }) + ); // 쿼리 결과 수집 const queryResults: Record[] }> = {};