리포트 디자이너에 바코드/QR코드 컴포넌트 추가
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
PageOrientation,
|
||||
convertMillimetersToTwip,
|
||||
} from "docx";
|
||||
import bwipjs from "bwip-js";
|
||||
|
||||
export class ReportController {
|
||||
/**
|
||||
@@ -1326,6 +1327,43 @@ export class ReportController {
|
||||
);
|
||||
}
|
||||
|
||||
// Barcode 컴포넌트 (바코드 이미지가 미리 생성되어 전달된 경우)
|
||||
else if (component.type === "barcode" && component.barcodeImageBase64) {
|
||||
try {
|
||||
const base64Data =
|
||||
component.barcodeImageBase64.split(",")[1] || component.barcodeImageBase64;
|
||||
const imageBuffer = Buffer.from(base64Data, "base64");
|
||||
result.push(
|
||||
new ParagraphRef({
|
||||
children: [
|
||||
new ImageRunRef({
|
||||
data: imageBuffer,
|
||||
transformation: {
|
||||
width: Math.round(component.width * 0.75),
|
||||
height: Math.round(component.height * 0.75),
|
||||
},
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
// 바코드 이미지 생성 실패 시 텍스트로 대체
|
||||
const barcodeValue = component.barcodeValue || "BARCODE";
|
||||
result.push(
|
||||
new ParagraphRef({
|
||||
children: [
|
||||
new TextRunRef({
|
||||
text: `[${barcodeValue}]`,
|
||||
size: pxToHalfPtFn(12),
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Divider - 테이블 셀로 감싸서 정확한 너비 적용
|
||||
else if (
|
||||
component.type === "divider" &&
|
||||
@@ -1354,6 +1392,82 @@ export class ReportController {
|
||||
return result;
|
||||
};
|
||||
|
||||
// 바코드 이미지 생성 헬퍼 함수
|
||||
const generateBarcodeImage = async (
|
||||
component: any,
|
||||
queryResultsMapRef: Record<string, { fields: string[]; rows: Record<string, unknown>[] }>
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const barcodeType = component.barcodeType || "CODE128";
|
||||
const barcodeColor = (component.barcodeColor || "#000000").replace("#", "");
|
||||
const barcodeBackground = (component.barcodeBackground || "#ffffff").replace("#", "");
|
||||
|
||||
// 바코드 값 결정 (쿼리 바인딩 또는 고정값)
|
||||
let barcodeValue = component.barcodeValue || "SAMPLE123";
|
||||
if (component.barcodeFieldName && component.queryId && queryResultsMapRef[component.queryId]) {
|
||||
const qResult = queryResultsMapRef[component.queryId];
|
||||
if (qResult.rows && qResult.rows.length > 0) {
|
||||
const row = qResult.rows[0];
|
||||
const val = row[component.barcodeFieldName];
|
||||
if (val !== null && val !== undefined) {
|
||||
barcodeValue = String(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bwip-js 바코드 타입 매핑
|
||||
const bcidMap: Record<string, string> = {
|
||||
"CODE128": "code128",
|
||||
"CODE39": "code39",
|
||||
"EAN13": "ean13",
|
||||
"EAN8": "ean8",
|
||||
"UPC": "upca",
|
||||
"QR": "qrcode",
|
||||
};
|
||||
|
||||
const bcid = bcidMap[barcodeType] || "code128";
|
||||
const isQR = barcodeType === "QR";
|
||||
|
||||
// 바코드 옵션 설정
|
||||
const options: any = {
|
||||
bcid: bcid,
|
||||
text: barcodeValue,
|
||||
scale: 3,
|
||||
includetext: !isQR && component.showBarcodeText !== false,
|
||||
textxalign: "center",
|
||||
barcolor: barcodeColor,
|
||||
backgroundcolor: barcodeBackground,
|
||||
};
|
||||
|
||||
// QR 코드 옵션
|
||||
if (isQR) {
|
||||
options.eclevel = component.qrErrorCorrectionLevel || "M";
|
||||
}
|
||||
|
||||
// 바코드 이미지 생성
|
||||
const png = await bwipjs.toBuffer(options);
|
||||
const base64 = png.toString("base64");
|
||||
return `data:image/png;base64,${base64}`;
|
||||
} catch (error) {
|
||||
console.error("바코드 생성 오류:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 모든 페이지의 바코드 컴포넌트에 대해 이미지 생성
|
||||
for (const page of layoutConfig.pages) {
|
||||
if (page.components) {
|
||||
for (const component of page.components) {
|
||||
if (component.type === "barcode") {
|
||||
const barcodeImage = await generateBarcodeImage(component, queryResultsMap);
|
||||
if (barcodeImage) {
|
||||
component.barcodeImageBase64 = barcodeImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 섹션 생성 (페이지별)
|
||||
const sortedPages = layoutConfig.pages.sort(
|
||||
(a: any, b: any) => a.page_order - b.page_order
|
||||
@@ -2624,6 +2738,77 @@ export class ReportController {
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
|
||||
// Barcode 컴포넌트
|
||||
else if (component.type === "barcode") {
|
||||
if (component.barcodeImageBase64) {
|
||||
try {
|
||||
const base64Data =
|
||||
component.barcodeImageBase64.split(",")[1] || component.barcodeImageBase64;
|
||||
const imageBuffer = Buffer.from(base64Data, "base64");
|
||||
|
||||
// spacing을 위한 빈 paragraph
|
||||
if (spacingBefore > 0) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
spacing: { before: spacingBefore, after: 0 },
|
||||
children: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
indent: { left: indentLeft },
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: imageBuffer,
|
||||
transformation: {
|
||||
width: Math.round(component.width * 0.75),
|
||||
height: Math.round(component.height * 0.75),
|
||||
},
|
||||
type: "png",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
} catch (imgError) {
|
||||
console.error("바코드 이미지 오류:", imgError);
|
||||
// 바코드 이미지 생성 실패 시 텍스트로 대체
|
||||
const barcodeValue = component.barcodeValue || "BARCODE";
|
||||
children.push(
|
||||
new Paragraph({
|
||||
spacing: { before: spacingBefore, after: 0 },
|
||||
indent: { left: indentLeft },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `[${barcodeValue}]`,
|
||||
size: pxToHalfPt(12),
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 바코드 이미지가 없는 경우 텍스트로 대체
|
||||
const barcodeValue = component.barcodeValue || "BARCODE";
|
||||
children.push(
|
||||
new Paragraph({
|
||||
spacing: { before: spacingBefore, after: 0 },
|
||||
indent: { left: indentLeft },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `[${barcodeValue}]`,
|
||||
size: pxToHalfPt(12),
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
|
||||
// Table 컴포넌트
|
||||
else if (component.type === "table" && component.queryId) {
|
||||
const queryResult = queryResultsMap[component.queryId];
|
||||
|
||||
@@ -166,3 +166,98 @@ export interface CreateTemplateRequest {
|
||||
layoutConfig?: any;
|
||||
defaultQueries?: any;
|
||||
}
|
||||
|
||||
// 컴포넌트 설정 (프론트엔드와 동기화)
|
||||
export interface ComponentConfig {
|
||||
id: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
zIndex: number;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
fontWeight?: string;
|
||||
fontColor?: string;
|
||||
backgroundColor?: string;
|
||||
borderWidth?: number;
|
||||
borderColor?: string;
|
||||
borderRadius?: number;
|
||||
textAlign?: string;
|
||||
padding?: number;
|
||||
queryId?: string;
|
||||
fieldName?: string;
|
||||
defaultValue?: string;
|
||||
format?: string;
|
||||
visible?: boolean;
|
||||
printable?: boolean;
|
||||
conditional?: string;
|
||||
locked?: boolean;
|
||||
groupId?: string;
|
||||
// 이미지 전용
|
||||
imageUrl?: string;
|
||||
objectFit?: "contain" | "cover" | "fill" | "none";
|
||||
// 구분선 전용
|
||||
orientation?: "horizontal" | "vertical";
|
||||
lineStyle?: "solid" | "dashed" | "dotted" | "double";
|
||||
lineWidth?: number;
|
||||
lineColor?: string;
|
||||
// 서명/도장 전용
|
||||
showLabel?: boolean;
|
||||
labelText?: string;
|
||||
labelPosition?: "top" | "left" | "bottom" | "right";
|
||||
showUnderline?: boolean;
|
||||
personName?: string;
|
||||
// 테이블 전용
|
||||
tableColumns?: Array<{
|
||||
field: string;
|
||||
header: string;
|
||||
width?: number;
|
||||
align?: "left" | "center" | "right";
|
||||
}>;
|
||||
headerBackgroundColor?: string;
|
||||
headerTextColor?: string;
|
||||
showBorder?: boolean;
|
||||
rowHeight?: number;
|
||||
// 페이지 번호 전용
|
||||
pageNumberFormat?: "number" | "numberTotal" | "koreanNumber";
|
||||
// 카드 컴포넌트 전용
|
||||
cardTitle?: string;
|
||||
cardItems?: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
fieldName?: string;
|
||||
}>;
|
||||
labelWidth?: number;
|
||||
showCardBorder?: boolean;
|
||||
showCardTitle?: boolean;
|
||||
titleFontSize?: number;
|
||||
labelFontSize?: number;
|
||||
valueFontSize?: number;
|
||||
titleColor?: string;
|
||||
labelColor?: string;
|
||||
valueColor?: string;
|
||||
// 계산 컴포넌트 전용
|
||||
calcItems?: Array<{
|
||||
label: string;
|
||||
value: number | string;
|
||||
operator: "+" | "-" | "x" | "÷";
|
||||
fieldName?: string;
|
||||
}>;
|
||||
resultLabel?: string;
|
||||
resultColor?: string;
|
||||
resultFontSize?: number;
|
||||
showCalcBorder?: boolean;
|
||||
numberFormat?: "none" | "comma" | "currency";
|
||||
currencySuffix?: string;
|
||||
// 바코드 컴포넌트 전용
|
||||
barcodeType?: "CODE128" | "CODE39" | "EAN13" | "EAN8" | "UPC" | "QR";
|
||||
barcodeValue?: string;
|
||||
barcodeFieldName?: string;
|
||||
showBarcodeText?: boolean;
|
||||
barcodeColor?: string;
|
||||
barcodeBackground?: string;
|
||||
barcodeMargin?: number;
|
||||
qrErrorCorrectionLevel?: "L" | "M" | "Q" | "H";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user