계산 컴포넌트 연산자 로직 개선

This commit is contained in:
dohyeons
2025-12-18 11:41:48 +09:00
parent 1fd428c016
commit 403bd0f8a1
7 changed files with 1157 additions and 1 deletions

View File

@@ -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];