카드 컴포넌트 추가 및 페이지번호/쿼리 버그 수정

This commit is contained in:
dohyeons
2025-12-18 10:39:57 +09:00
parent 0ed8e686c0
commit 1fd428c016
8 changed files with 831 additions and 2 deletions

View File

@@ -791,6 +791,93 @@ export class ReportController {
);
}
// Card 컴포넌트
else if (component.type === "card") {
const cardTitle = component.cardTitle || "정보 카드";
const cardItems = component.cardItems || [];
const labelWidth = component.labelWidth || 80;
const showCardTitle = component.showCardTitle !== false;
const titleFontSize = pxToHalfPtFn(component.titleFontSize || 14);
const labelFontSize = pxToHalfPtFn(component.labelFontSize || 13);
const valueFontSize = pxToHalfPtFn(component.valueFontSize || 13);
const titleColor = (component.titleColor || "#1e40af").replace("#", "");
const labelColor = (component.labelColor || "#374151").replace("#", "");
const valueColor = (component.valueColor || "#000000").replace("#", "");
const borderColor = (component.borderColor || "#e5e7eb").replace("#", "");
// 쿼리 바인딩된 값 가져오기
const getCardValueFn = (item: { label: string; value: string; fieldName?: string }) => {
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];
return row[item.fieldName] !== undefined ? String(row[item.fieldName]) : item.value;
}
}
return item.value;
};
// 제목
if (showCardTitle) {
result.push(
new ParagraphRef({
children: [
new TextRunRef({
text: cardTitle,
size: titleFontSize,
color: titleColor,
bold: true,
font: "맑은 고딕",
}),
],
})
);
// 구분선
result.push(
new ParagraphRef({
border: {
bottom: {
color: borderColor,
space: 1,
style: BorderStyleRef.SINGLE,
size: 8,
},
},
children: [],
})
);
}
// 항목들
for (const item of cardItems) {
const itemValue = getCardValueFn(item as { label: string; value: string; fieldName?: string });
result.push(
new ParagraphRef({
children: [
new TextRunRef({
text: item.label,
size: labelFontSize,
color: labelColor,
bold: true,
font: "맑은 고딕",
}),
new TextRunRef({
text: " ",
size: labelFontSize,
font: "맑은 고딕",
}),
new TextRunRef({
text: itemValue,
size: valueFontSize,
color: valueColor,
font: "맑은 고딕",
}),
],
})
);
}
}
// Divider - 테이블 셀로 감싸서 정확한 너비 적용
else if (component.type === "divider" && component.orientation === "horizontal") {
result.push(
@@ -1279,6 +1366,172 @@ export class ReportController {
lastBottomY = adjustedY + component.height;
}
// Card 컴포넌트 - 테이블로 감싸서 정확한 위치 적용
else if (component.type === "card") {
const cardTitle = component.cardTitle || "정보 카드";
const cardItems = component.cardItems || [];
const labelWidthPx = component.labelWidth || 80;
const showCardTitle = component.showCardTitle !== false;
const titleFontSize = pxToHalfPt(component.titleFontSize || 14);
const labelFontSizeCard = pxToHalfPt(component.labelFontSize || 13);
const valueFontSizeCard = pxToHalfPt(component.valueFontSize || 13);
const titleColorCard = (component.titleColor || "#1e40af").replace("#", "");
const labelColorCard = (component.labelColor || "#374151").replace("#", "");
const valueColorCard = (component.valueColor || "#000000").replace("#", "");
const borderColorCard = (component.borderColor || "#e5e7eb").replace("#", "");
// 쿼리 바인딩된 값 가져오기
const getCardValueLocal = (item: { label: string; value: string; fieldName?: string }) => {
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];
return row[item.fieldName] !== undefined ? String(row[item.fieldName]) : item.value;
}
}
return item.value;
};
const cardParagraphs: Paragraph[] = [];
// 제목
if (showCardTitle) {
cardParagraphs.push(
new Paragraph({
children: [
new TextRun({
text: cardTitle,
size: titleFontSize,
color: titleColorCard,
bold: true,
font: "맑은 고딕",
}),
],
})
);
// 구분선
cardParagraphs.push(
new Paragraph({
border: {
bottom: {
color: borderColorCard,
space: 1,
style: BorderStyle.SINGLE,
size: 8,
},
},
children: [],
})
);
}
// 항목들을 테이블로 구성 (라벨 + 값)
const itemRows = cardItems.map((item: { label: string; value: string; fieldName?: string }) => {
const itemValue = getCardValueLocal(item);
return new TableRow({
children: [
new TableCell({
width: { size: pxToTwip(labelWidthPx), type: WidthType.DXA },
children: [
new Paragraph({
children: [
new TextRun({
text: item.label,
size: labelFontSizeCard,
color: labelColorCard,
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" },
},
}),
new TableCell({
width: { size: pxToTwip(component.width - labelWidthPx - 16), type: WidthType.DXA },
children: [
new Paragraph({
children: [
new TextRun({
text: itemValue,
size: valueFontSizeCard,
color: valueColorCard,
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" },
},
}),
],
});
});
const itemsTable = new Table({
rows: itemRows,
width: { size: pxToTwip(component.width), 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" },
},
});
// 전체를 하나의 테이블 셀로 감싸기
const cardCell = new TableCell({
children: [...cardParagraphs, itemsTable],
width: { size: pxToTwip(component.width), type: WidthType.DXA },
borders: component.showCardBorder !== false
? {
top: { style: BorderStyle.SINGLE, size: 4, color: borderColorCard },
bottom: { style: BorderStyle.SINGLE, size: 4, color: borderColorCard },
left: { style: BorderStyle.SINGLE, size: 4, color: borderColorCard },
right: { style: BorderStyle.SINGLE, size: 4, color: borderColorCard },
}
: {
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" },
},
verticalAlign: VerticalAlign.TOP,
});
const cardTable = new Table({
rows: [new TableRow({ children: [cardCell] })],
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(cardTable);
lastBottomY = adjustedY + component.height;
}
// Table 컴포넌트
else if (component.type === "table" && component.queryId) {
const queryResult = queryResultsMap[component.queryId];