계산 컴포넌트 연산자 로직 개선
This commit is contained in:
parent
1fd428c016
commit
403bd0f8a1
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -677,6 +677,133 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
|
|||
</div>
|
||||
);
|
||||
|
||||
case "calculation":
|
||||
// 계산 컴포넌트
|
||||
const calcItems = component.calcItems || [];
|
||||
const resultLabel = component.resultLabel || "합계";
|
||||
const calcLabelWidth = component.labelWidth || 120;
|
||||
const calcLabelFontSize = component.labelFontSize || 13;
|
||||
const calcValueFontSize = component.valueFontSize || 13;
|
||||
const calcResultFontSize = component.resultFontSize || 16;
|
||||
const calcLabelColor = component.labelColor || "#374151";
|
||||
const calcValueColor = component.valueColor || "#000000";
|
||||
const calcResultColor = component.resultColor || "#2563eb";
|
||||
const numberFormat = component.numberFormat || "currency";
|
||||
const currencySuffix = component.currencySuffix || "원";
|
||||
|
||||
// 숫자 포맷팅 함수
|
||||
const formatNumber = (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 getCalcItemValue = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => {
|
||||
if (item.fieldName && component.queryId) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
const row = queryResult.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;
|
||||
};
|
||||
|
||||
// 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용)
|
||||
const calculateResult = (): number => {
|
||||
if (calcItems.length === 0) return 0;
|
||||
|
||||
// 첫 번째 항목은 기준값
|
||||
let result = getCalcItemValue(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 = getCalcItemValue(item as { label: string; value: number | string; operator: string; fieldName?: string });
|
||||
switch (item.operator) {
|
||||
case "+":
|
||||
result += val;
|
||||
break;
|
||||
case "-":
|
||||
result -= val;
|
||||
break;
|
||||
case "x":
|
||||
result *= val;
|
||||
break;
|
||||
case "÷":
|
||||
result = val !== 0 ? result / val : result;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const calcResult = calculateResult();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
{/* 항목 목록 */}
|
||||
<div className="flex-1 overflow-auto px-2 py-1">
|
||||
{calcItems.map((item: { label: string; value: number | string; operator: string; fieldName?: string }, index: number) => {
|
||||
const itemValue = getCalcItemValue(item);
|
||||
return (
|
||||
<div key={index} className="flex items-center justify-between py-1">
|
||||
<span
|
||||
className="flex-shrink-0"
|
||||
style={{
|
||||
width: `${calcLabelWidth}px`,
|
||||
fontSize: `${calcLabelFontSize}px`,
|
||||
color: calcLabelColor,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
<span
|
||||
className="text-right"
|
||||
style={{
|
||||
fontSize: `${calcValueFontSize}px`,
|
||||
color: calcValueColor,
|
||||
}}
|
||||
>
|
||||
{formatNumber(itemValue)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* 구분선 */}
|
||||
<div
|
||||
className="mx-1 flex-shrink-0 border-t"
|
||||
style={{ borderColor: component.borderColor || "#374151" }}
|
||||
/>
|
||||
{/* 결과 */}
|
||||
<div className="flex items-center justify-between px-2 py-2">
|
||||
<span
|
||||
className="font-semibold"
|
||||
style={{
|
||||
width: `${calcLabelWidth}px`,
|
||||
fontSize: `${calcResultFontSize}px`,
|
||||
color: calcLabelColor,
|
||||
}}
|
||||
>
|
||||
{resultLabel}
|
||||
</span>
|
||||
<span
|
||||
className="text-right font-bold"
|
||||
style={{
|
||||
fontSize: `${calcResultFontSize}px`,
|
||||
color: calcResultColor,
|
||||
}}
|
||||
>
|
||||
{formatNumber(calcResult)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <div>알 수 없는 컴포넌트</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useDrag } from "react-dnd";
|
||||
import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash, CreditCard } from "lucide-react";
|
||||
import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash, CreditCard, Calculator } from "lucide-react";
|
||||
|
||||
interface ComponentItem {
|
||||
type: string;
|
||||
|
|
@ -18,6 +18,7 @@ const COMPONENTS: ComponentItem[] = [
|
|||
{ type: "stamp", label: "도장란", icon: <StampIcon className="h-4 w-4" /> },
|
||||
{ type: "pageNumber", label: "페이지번호", icon: <Hash className="h-4 w-4" /> },
|
||||
{ type: "card", label: "정보카드", icon: <CreditCard className="h-4 w-4" /> },
|
||||
{ type: "calculation", label: "계산", icon: <Calculator className="h-4 w-4" /> },
|
||||
];
|
||||
|
||||
function DraggableComponentItem({ type, label, icon }: ComponentItem) {
|
||||
|
|
|
|||
|
|
@ -173,6 +173,28 @@ export function ReportDesignerCanvas() {
|
|||
borderWidth: 1,
|
||||
borderColor: "#e5e7eb",
|
||||
}),
|
||||
// 계산 컴포넌트 전용
|
||||
...(item.componentType === "calculation" && {
|
||||
width: 350,
|
||||
height: 120,
|
||||
calcItems: [
|
||||
{ label: "공급가액", value: 0, operator: "+" as const, fieldName: "" },
|
||||
{ label: "부가세 (10%)", value: 0, operator: "+" as const, fieldName: "" },
|
||||
],
|
||||
resultLabel: "합계 금액",
|
||||
labelWidth: 120,
|
||||
labelFontSize: 13,
|
||||
valueFontSize: 13,
|
||||
resultFontSize: 16,
|
||||
labelColor: "#374151",
|
||||
valueColor: "#000000",
|
||||
resultColor: "#2563eb",
|
||||
showCalcBorder: false,
|
||||
numberFormat: "currency" as const,
|
||||
currencySuffix: "원",
|
||||
borderWidth: 0,
|
||||
borderColor: "#e5e7eb",
|
||||
}),
|
||||
// 테이블 전용
|
||||
...(item.componentType === "table" && {
|
||||
queryId: undefined,
|
||||
|
|
|
|||
|
|
@ -1272,6 +1272,365 @@ export function ReportDesignerRightPanel() {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* 계산 컴포넌트 설정 */}
|
||||
{selectedComponent.type === "calculation" && (
|
||||
<Card className="mt-4 border-orange-200 bg-orange-50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-orange-900">계산 설정</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* 결과 라벨 */}
|
||||
<div>
|
||||
<Label className="text-xs">결과 라벨</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={selectedComponent.resultLabel || "합계"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
resultLabel: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="합계 금액"
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 라벨 너비 */}
|
||||
<div>
|
||||
<Label className="text-xs">라벨 너비 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.labelWidth || 120}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
labelWidth: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
min={60}
|
||||
max={200}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 숫자 포맷 */}
|
||||
<div>
|
||||
<Label className="text-xs">숫자 포맷</Label>
|
||||
<Select
|
||||
value={selectedComponent.numberFormat || "currency"}
|
||||
onValueChange={(value) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
numberFormat: value as "none" | "comma" | "currency",
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">없음</SelectItem>
|
||||
<SelectItem value="comma">천단위 구분</SelectItem>
|
||||
<SelectItem value="currency">통화 (원)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 통화 접미사 */}
|
||||
{selectedComponent.numberFormat === "currency" && (
|
||||
<div>
|
||||
<Label className="text-xs">통화 단위</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={selectedComponent.currencySuffix || "원"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
currencySuffix: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="원"
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 폰트 크기 설정 */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label className="text-xs">라벨 크기</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.labelFontSize || 13}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
labelFontSize: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
min={10}
|
||||
max={20}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">값 크기</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.valueFontSize || 13}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
valueFontSize: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
min={10}
|
||||
max={20}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">결과 크기</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.resultFontSize || 16}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
resultFontSize: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
min={12}
|
||||
max={24}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 색상 설정 */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label className="text-xs">라벨 색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.labelColor || "#374151"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
labelColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8 w-full cursor-pointer p-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">값 색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.valueColor || "#000000"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
valueColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8 w-full cursor-pointer p-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">결과 색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.resultColor || "#2563eb"}
|
||||
onChange={(e) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
resultColor: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8 w-full cursor-pointer p-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 계산 항목 목록 관리 */}
|
||||
<div className="mt-4 border-t pt-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-xs font-semibold">계산 항목</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => {
|
||||
const currentItems = selectedComponent.calcItems || [];
|
||||
updateComponent(selectedComponent.id, {
|
||||
calcItems: [
|
||||
...currentItems,
|
||||
{
|
||||
label: `항목${currentItems.length + 1}`,
|
||||
value: 0,
|
||||
operator: "+" as const,
|
||||
fieldName: "",
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
>
|
||||
+ 항목 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 쿼리 선택 (데이터 바인딩용) */}
|
||||
<div className="mb-2">
|
||||
<Label className="text-xs">데이터 소스 (쿼리)</Label>
|
||||
<Select
|
||||
value={selectedComponent.queryId || "none"}
|
||||
onValueChange={(value) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
queryId: value === "none" ? undefined : value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue placeholder="쿼리 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">직접 입력</SelectItem>
|
||||
{queries.map((q) => (
|
||||
<SelectItem key={q.id} value={q.id}>
|
||||
{q.name} ({q.type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 항목 리스트 */}
|
||||
<div className="max-h-48 space-y-2 overflow-y-auto">
|
||||
{(selectedComponent.calcItems || []).map((item, index: number) => (
|
||||
<div key={index} className="rounded border bg-white p-2">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-xs font-medium">항목 {index + 1}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-5 w-5 p-0 text-red-500 hover:text-red-700"
|
||||
onClick={() => {
|
||||
const currentItems = [...(selectedComponent.calcItems || [])];
|
||||
currentItems.splice(index, 1);
|
||||
updateComponent(selectedComponent.id, {
|
||||
calcItems: currentItems,
|
||||
});
|
||||
}}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`grid gap-1 ${index === 0 ? "grid-cols-1" : "grid-cols-3"}`}>
|
||||
<div className={index === 0 ? "" : "col-span-2"}>
|
||||
<Label className="text-[10px]">라벨</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={item.label}
|
||||
onChange={(e) => {
|
||||
const currentItems = [...(selectedComponent.calcItems || [])];
|
||||
currentItems[index] = { ...currentItems[index], label: e.target.value };
|
||||
updateComponent(selectedComponent.id, {
|
||||
calcItems: currentItems,
|
||||
});
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
placeholder="항목명"
|
||||
/>
|
||||
</div>
|
||||
{/* 두 번째 항목부터 연산자 표시 */}
|
||||
{index > 0 && (
|
||||
<div>
|
||||
<Label className="text-[10px]">연산자</Label>
|
||||
<Select
|
||||
value={item.operator}
|
||||
onValueChange={(value) => {
|
||||
const currentItems = [...(selectedComponent.calcItems || [])];
|
||||
currentItems[index] = {
|
||||
...currentItems[index],
|
||||
operator: value as "+" | "-" | "x" | "÷",
|
||||
};
|
||||
updateComponent(selectedComponent.id, {
|
||||
calcItems: currentItems,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="+">+</SelectItem>
|
||||
<SelectItem value="-">-</SelectItem>
|
||||
<SelectItem value="x">x</SelectItem>
|
||||
<SelectItem value="÷">÷</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
{selectedComponent.queryId ? (
|
||||
<div>
|
||||
<Label className="text-[10px]">필드</Label>
|
||||
<Select
|
||||
value={item.fieldName || "none"}
|
||||
onValueChange={(value) => {
|
||||
const currentItems = [...(selectedComponent.calcItems || [])];
|
||||
currentItems[index] = {
|
||||
...currentItems[index],
|
||||
fieldName: value === "none" ? "" : value,
|
||||
};
|
||||
updateComponent(selectedComponent.id, {
|
||||
calcItems: currentItems,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-6 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">직접 입력</SelectItem>
|
||||
{(() => {
|
||||
const query = queries.find((q) => q.id === selectedComponent.queryId);
|
||||
const result = query ? getQueryResult(query.id) : null;
|
||||
if (result && result.fields) {
|
||||
return result.fields.map((field: string) => (
|
||||
<SelectItem key={field} value={field}>
|
||||
{field}
|
||||
</SelectItem>
|
||||
));
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Label className="text-[10px]">값</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={item.value}
|
||||
onChange={(e) => {
|
||||
const currentItems = [...(selectedComponent.calcItems || [])];
|
||||
currentItems[index] = {
|
||||
...currentItems[index],
|
||||
value: Number(e.target.value),
|
||||
};
|
||||
updateComponent(selectedComponent.id, {
|
||||
calcItems: currentItems,
|
||||
});
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 데이터 바인딩 (텍스트/라벨/테이블 컴포넌트) */}
|
||||
{(selectedComponent.type === "text" ||
|
||||
selectedComponent.type === "label" ||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,91 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
</div>`;
|
||||
}
|
||||
|
||||
// 계산 컴포넌트
|
||||
else if (component.type === "calculation") {
|
||||
const calcItems = component.calcItems || [];
|
||||
const resultLabel = component.resultLabel || "합계";
|
||||
const calcLabelWidth = component.labelWidth || 120;
|
||||
const calcLabelFontSize = component.labelFontSize || 13;
|
||||
const calcValueFontSize = component.valueFontSize || 13;
|
||||
const calcResultFontSize = component.resultFontSize || 16;
|
||||
const calcLabelColor = component.labelColor || "#374151";
|
||||
const calcValueColor = component.valueColor || "#000000";
|
||||
const calcResultColor = component.resultColor || "#2563eb";
|
||||
const numberFormat = component.numberFormat || "currency";
|
||||
const currencySuffix = component.currencySuffix || "원";
|
||||
const borderColor = component.borderColor || "#374151";
|
||||
|
||||
// 숫자 포맷팅 함수
|
||||
const formatNumber = (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 getCalcItemValue = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => {
|
||||
if (item.fieldName && queryResult && queryResult.rows && queryResult.rows.length > 0) {
|
||||
const row = queryResult.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 = getCalcItemValue(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 = getCalcItemValue(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 itemsHtml = calcItems
|
||||
.map((item: { label: string; value: number | string; operator: string; fieldName?: string }) => {
|
||||
const itemValue = getCalcItemValue(item);
|
||||
return `
|
||||
<div style="display: flex; justify-content: space-between; padding: 4px 8px;">
|
||||
<span style="width: ${calcLabelWidth}px; font-size: ${calcLabelFontSize}px; color: ${calcLabelColor};">${item.label}</span>
|
||||
<span style="font-size: ${calcValueFontSize}px; color: ${calcValueColor}; text-align: right;">${formatNumber(itemValue)}</span>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
content = `
|
||||
<div style="display: flex; flex-direction: column; height: 100%;">
|
||||
<div style="flex: 1;">
|
||||
${itemsHtml}
|
||||
</div>
|
||||
<div style="border-top: 1px solid ${borderColor}; margin: 4px 8px;"></div>
|
||||
<div style="display: flex; justify-content: space-between; padding: 4px 8px;">
|
||||
<span style="width: ${calcLabelWidth}px; font-size: ${calcResultFontSize}px; font-weight: 600; color: ${calcLabelColor};">${resultLabel}</span>
|
||||
<span style="font-size: ${calcResultFontSize}px; font-weight: 700; color: ${calcResultColor}; text-align: right;">${formatNumber(calcResult)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Table 컴포넌트
|
||||
else if (component.type === "table" && queryResult && queryResult.rows.length > 0) {
|
||||
const columns =
|
||||
|
|
@ -903,6 +988,131 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 계산 컴포넌트 */}
|
||||
{component.type === "calculation" && (() => {
|
||||
const calcItems = component.calcItems || [];
|
||||
const resultLabel = component.resultLabel || "합계";
|
||||
const calcLabelWidth = component.labelWidth || 120;
|
||||
const calcLabelFontSize = component.labelFontSize || 13;
|
||||
const calcValueFontSize = component.valueFontSize || 13;
|
||||
const calcResultFontSize = component.resultFontSize || 16;
|
||||
const calcLabelColor = component.labelColor || "#374151";
|
||||
const calcValueColor = component.valueColor || "#000000";
|
||||
const calcResultColor = component.resultColor || "#2563eb";
|
||||
const numberFormat = component.numberFormat || "currency";
|
||||
const currencySuffix = component.currencySuffix || "원";
|
||||
const borderColor = component.borderColor || "#374151";
|
||||
|
||||
// 숫자 포맷팅 함수
|
||||
const formatNumber = (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 getCalcItemValue = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => {
|
||||
if (item.fieldName && component.queryId) {
|
||||
const qResult = getQueryResult(component.queryId);
|
||||
if (qResult && 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 = getCalcItemValue(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 = getCalcItemValue(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
{calcItems.map((item: { label: string; value: number | string; operator: string; fieldName?: string }, idx: number) => {
|
||||
const itemValue = getCalcItemValue(item);
|
||||
return (
|
||||
<div key={idx} style={{ display: "flex", justifyContent: "space-between", padding: "4px 8px" }}>
|
||||
<span
|
||||
style={{
|
||||
width: `${calcLabelWidth}px`,
|
||||
fontSize: `${calcLabelFontSize}px`,
|
||||
color: calcLabelColor,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: `${calcValueFontSize}px`,
|
||||
color: calcValueColor,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{formatNumber(itemValue)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{ borderTop: `1px solid ${borderColor}`, margin: "4px 8px" }} />
|
||||
<div style={{ display: "flex", justifyContent: "space-between", padding: "4px 8px" }}>
|
||||
<span
|
||||
style={{
|
||||
width: `${calcLabelWidth}px`,
|
||||
fontSize: `${calcResultFontSize}px`,
|
||||
fontWeight: 600,
|
||||
color: calcLabelColor,
|
||||
}}
|
||||
>
|
||||
{resultLabel}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: `${calcResultFontSize}px`,
|
||||
fontWeight: 700,
|
||||
color: calcResultColor,
|
||||
textAlign: "right",
|
||||
}}
|
||||
>
|
||||
{formatNumber(calcResult)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,19 @@ export interface ComponentConfig {
|
|||
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; // 통화 접미사 (예: "원")
|
||||
}
|
||||
|
||||
// 리포트 상세
|
||||
|
|
|
|||
Loading…
Reference in New Issue