계산 컴포넌트 연산자 로직 개선
This commit is contained in:
@@ -878,6 +878,215 @@ export class ReportController {
|
||||
}
|
||||
}
|
||||
|
||||
// 계산 컴포넌트
|
||||
else if (component.type === "calculation") {
|
||||
const calcItems = component.calcItems || [];
|
||||
const resultLabel = component.resultLabel || "합계";
|
||||
const calcLabelWidth = component.labelWidth || 120;
|
||||
const calcLabelFontSize = pxToHalfPtFn(component.labelFontSize || 13);
|
||||
const calcValueFontSize = pxToHalfPtFn(component.valueFontSize || 13);
|
||||
const calcResultFontSize = pxToHalfPtFn(component.resultFontSize || 16);
|
||||
const calcLabelColor = (component.labelColor || "#374151").replace("#", "");
|
||||
const calcValueColor = (component.valueColor || "#000000").replace("#", "");
|
||||
const calcResultColor = (component.resultColor || "#2563eb").replace("#", "");
|
||||
const numberFormat = component.numberFormat || "currency";
|
||||
const currencySuffix = component.currencySuffix || "원";
|
||||
const borderColor = (component.borderColor || "#374151").replace("#", "");
|
||||
|
||||
// 숫자 포맷팅 함수
|
||||
const formatNumberFn = (num: number): string => {
|
||||
if (numberFormat === "none") return String(num);
|
||||
if (numberFormat === "comma") return num.toLocaleString();
|
||||
if (numberFormat === "currency") return num.toLocaleString() + currencySuffix;
|
||||
return String(num);
|
||||
};
|
||||
|
||||
// 쿼리 바인딩된 값 가져오기
|
||||
const getCalcItemValueFn = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => {
|
||||
if (item.fieldName && 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[item.fieldName];
|
||||
return typeof val === "number" ? val : parseFloat(String(val)) || 0;
|
||||
}
|
||||
}
|
||||
return typeof item.value === "number" ? item.value : parseFloat(String(item.value)) || 0;
|
||||
};
|
||||
|
||||
// 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용)
|
||||
let calcResult = 0;
|
||||
if (calcItems.length > 0) {
|
||||
// 첫 번째 항목은 기준값
|
||||
calcResult = getCalcItemValueFn(calcItems[0] as { label: string; value: number | string; operator: string; fieldName?: string });
|
||||
|
||||
// 두 번째 항목부터 연산자 적용
|
||||
for (let i = 1; i < calcItems.length; i++) {
|
||||
const item = calcItems[i];
|
||||
const val = getCalcItemValueFn(item as { label: string; value: number | string; operator: string; fieldName?: string });
|
||||
switch ((item as { operator: string }).operator) {
|
||||
case "+":
|
||||
calcResult += val;
|
||||
break;
|
||||
case "-":
|
||||
calcResult -= val;
|
||||
break;
|
||||
case "x":
|
||||
calcResult *= val;
|
||||
break;
|
||||
case "÷":
|
||||
calcResult = val !== 0 ? calcResult / val : calcResult;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블로 계산 항목 렌더링
|
||||
const calcTableRows = [];
|
||||
|
||||
// 각 항목
|
||||
for (const item of calcItems) {
|
||||
const itemValue = getCalcItemValueFn(item as { label: string; value: number | string; operator: string; fieldName?: string });
|
||||
calcTableRows.push(
|
||||
new TableRowRef({
|
||||
children: [
|
||||
new TableCellRef({
|
||||
children: [
|
||||
new ParagraphRef({
|
||||
children: [
|
||||
new TextRunRef({
|
||||
text: item.label,
|
||||
size: calcLabelFontSize,
|
||||
color: calcLabelColor,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
width: { size: pxToTwipFn(calcLabelWidth), type: WidthTypeRef.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
margins: { top: 50, bottom: 50, left: 100, right: 100 },
|
||||
}),
|
||||
new TableCellRef({
|
||||
children: [
|
||||
new ParagraphRef({
|
||||
alignment: AlignmentTypeRef.RIGHT,
|
||||
children: [
|
||||
new TextRunRef({
|
||||
text: formatNumberFn(itemValue),
|
||||
size: calcValueFontSize,
|
||||
color: calcValueColor,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
borders: {
|
||||
top: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
margins: { top: 50, bottom: 50, left: 100, right: 100 },
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 구분선 행
|
||||
calcTableRows.push(
|
||||
new TableRowRef({
|
||||
children: [
|
||||
new TableCellRef({
|
||||
columnSpan: 2,
|
||||
children: [new ParagraphRef({ children: [] })],
|
||||
borders: {
|
||||
top: { style: BorderStyleRef.SINGLE, size: 8, color: borderColor },
|
||||
bottom: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 결과 행
|
||||
calcTableRows.push(
|
||||
new TableRowRef({
|
||||
children: [
|
||||
new TableCellRef({
|
||||
children: [
|
||||
new ParagraphRef({
|
||||
children: [
|
||||
new TextRunRef({
|
||||
text: resultLabel,
|
||||
size: calcResultFontSize,
|
||||
color: calcLabelColor,
|
||||
bold: true,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
width: { size: pxToTwipFn(calcLabelWidth), type: WidthTypeRef.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
margins: { top: 50, bottom: 50, left: 100, right: 100 },
|
||||
}),
|
||||
new TableCellRef({
|
||||
children: [
|
||||
new ParagraphRef({
|
||||
alignment: AlignmentTypeRef.RIGHT,
|
||||
children: [
|
||||
new TextRunRef({
|
||||
text: formatNumberFn(calcResult),
|
||||
size: calcResultFontSize,
|
||||
color: calcResultColor,
|
||||
bold: true,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
borders: {
|
||||
top: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
margins: { top: 50, bottom: 50, left: 100, right: 100 },
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
result.push(
|
||||
new TableRef({
|
||||
rows: calcTableRows,
|
||||
width: { size: pxToTwipFn(component.width), type: WidthTypeRef.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
insideHorizontal: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
insideVertical: { style: BorderStyleRef.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Divider - 테이블 셀로 감싸서 정확한 너비 적용
|
||||
else if (component.type === "divider" && component.orientation === "horizontal") {
|
||||
result.push(
|
||||
@@ -1532,6 +1741,221 @@ export class ReportController {
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
|
||||
// 계산 컴포넌트 - 테이블로 감싸서 정확한 위치 적용
|
||||
else if (component.type === "calculation") {
|
||||
const calcItems = component.calcItems || [];
|
||||
const resultLabel = component.resultLabel || "합계";
|
||||
const calcLabelWidth = component.labelWidth || 120;
|
||||
const calcLabelFontSize = pxToHalfPt(component.labelFontSize || 13);
|
||||
const calcValueFontSize = pxToHalfPt(component.valueFontSize || 13);
|
||||
const calcResultFontSize = pxToHalfPt(component.resultFontSize || 16);
|
||||
const calcLabelColor = (component.labelColor || "#374151").replace("#", "");
|
||||
const calcValueColor = (component.valueColor || "#000000").replace("#", "");
|
||||
const calcResultColor = (component.resultColor || "#2563eb").replace("#", "");
|
||||
const numberFormat = component.numberFormat || "currency";
|
||||
const currencySuffix = component.currencySuffix || "원";
|
||||
const borderColor = (component.borderColor || "#374151").replace("#", "");
|
||||
|
||||
// 숫자 포맷팅 함수
|
||||
const formatNumberFn = (num: number): string => {
|
||||
if (numberFormat === "none") return String(num);
|
||||
if (numberFormat === "comma") return num.toLocaleString();
|
||||
if (numberFormat === "currency") return num.toLocaleString() + currencySuffix;
|
||||
return String(num);
|
||||
};
|
||||
|
||||
// 쿼리 바인딩된 값 가져오기
|
||||
const getCalcItemValueFn = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => {
|
||||
if (item.fieldName && component.queryId && queryResultsMap[component.queryId]) {
|
||||
const qResult = queryResultsMap[component.queryId];
|
||||
if (qResult.rows && qResult.rows.length > 0) {
|
||||
const row = qResult.rows[0];
|
||||
const val = row[item.fieldName];
|
||||
return typeof val === "number" ? val : parseFloat(String(val)) || 0;
|
||||
}
|
||||
}
|
||||
return typeof item.value === "number" ? item.value : parseFloat(String(item.value)) || 0;
|
||||
};
|
||||
|
||||
// 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용)
|
||||
let calcResult = 0;
|
||||
if (calcItems.length > 0) {
|
||||
// 첫 번째 항목은 기준값
|
||||
calcResult = getCalcItemValueFn(calcItems[0] as { label: string; value: number | string; operator: string; fieldName?: string });
|
||||
|
||||
// 두 번째 항목부터 연산자 적용
|
||||
for (let i = 1; i < calcItems.length; i++) {
|
||||
const calcItem = calcItems[i];
|
||||
const val = getCalcItemValueFn(calcItem as { label: string; value: number | string; operator: string; fieldName?: string });
|
||||
switch ((calcItem as { operator: string }).operator) {
|
||||
case "+":
|
||||
calcResult += val;
|
||||
break;
|
||||
case "-":
|
||||
calcResult -= val;
|
||||
break;
|
||||
case "x":
|
||||
calcResult *= val;
|
||||
break;
|
||||
case "÷":
|
||||
calcResult = val !== 0 ? calcResult / val : calcResult;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 행 생성
|
||||
const calcTableRows: TableRow[] = [];
|
||||
|
||||
// 각 항목 행
|
||||
for (const calcItem of calcItems) {
|
||||
const itemValue = getCalcItemValueFn(calcItem as { label: string; value: number | string; operator: string; fieldName?: string });
|
||||
calcTableRows.push(
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: calcItem.label,
|
||||
size: calcLabelFontSize,
|
||||
color: calcLabelColor,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
width: { size: pxToTwip(calcLabelWidth), type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
margins: { top: 50, bottom: 50, left: 100, right: 100 },
|
||||
}),
|
||||
new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.RIGHT,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: formatNumberFn(itemValue),
|
||||
size: calcValueFontSize,
|
||||
color: calcValueColor,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
margins: { top: 50, bottom: 50, left: 100, right: 100 },
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 구분선 행
|
||||
calcTableRows.push(
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
columnSpan: 2,
|
||||
children: [new Paragraph({ children: [] })],
|
||||
borders: {
|
||||
top: { style: BorderStyle.SINGLE, size: 8, color: borderColor },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// 결과 행
|
||||
calcTableRows.push(
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: resultLabel,
|
||||
size: calcResultFontSize,
|
||||
color: calcLabelColor,
|
||||
bold: true,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
width: { size: pxToTwip(calcLabelWidth), type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
margins: { top: 50, bottom: 50, left: 100, right: 100 },
|
||||
}),
|
||||
new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.RIGHT,
|
||||
children: [
|
||||
new TextRun({
|
||||
text: formatNumberFn(calcResult),
|
||||
size: calcResultFontSize,
|
||||
color: calcResultColor,
|
||||
bold: true,
|
||||
font: "맑은 고딕",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
margins: { top: 50, bottom: 50, left: 100, right: 100 },
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const calcTable = new Table({
|
||||
rows: calcTableRows,
|
||||
width: { size: pxToTwip(component.width), type: WidthType.DXA },
|
||||
indent: { size: indentLeft, type: WidthType.DXA },
|
||||
borders: {
|
||||
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
insideHorizontal: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
insideVertical: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
|
||||
// spacing을 위한 빈 paragraph
|
||||
if (spacingBefore > 0) {
|
||||
children.push(new Paragraph({ spacing: { before: spacingBefore, after: 0 }, children: [] }));
|
||||
}
|
||||
children.push(calcTable);
|
||||
lastBottomY = adjustedY + component.height;
|
||||
}
|
||||
|
||||
// Table 컴포넌트
|
||||
else if (component.type === "table" && component.queryId) {
|
||||
const queryResult = queryResultsMap[component.queryId];
|
||||
|
||||
Reference in New Issue
Block a user