diff --git a/backend-node/src/controllers/reportController.ts b/backend-node/src/controllers/reportController.ts index c6cd1ee7..e59235d9 100644 --- a/backend-node/src/controllers/reportController.ts +++ b/backend-node/src/controllers/reportController.ts @@ -618,7 +618,9 @@ export class ReportController { ImageRunRef: typeof ImageRun, TableRef: typeof Table, TableRowRef: typeof TableRow, - TableCellRef: typeof TableCell + TableCellRef: typeof TableCell, + pageIndex: number = 0, + totalPages: number = 1 ): (Paragraph | Table)[] => { const result: (Paragraph | Table)[] = []; @@ -749,6 +751,46 @@ export class ReportController { } } + // PageNumber + else if (component.type === "pageNumber") { + const format = component.pageNumberFormat || "number"; + const currentPageNum = pageIndex + 1; + let pageNumberText = ""; + switch (format) { + case "number": + pageNumberText = `${currentPageNum}`; + break; + case "numberTotal": + pageNumberText = `${currentPageNum} / ${totalPages}`; + break; + case "koreanNumber": + pageNumberText = `${currentPageNum} 페이지`; + break; + default: + pageNumberText = `${currentPageNum}`; + } + const fontSizeHalfPt = pxToHalfPtFn(component.fontSize || 13); + const alignment = + component.textAlign === "center" + ? AlignmentTypeRef.CENTER + : component.textAlign === "right" + ? AlignmentTypeRef.RIGHT + : AlignmentTypeRef.LEFT; + result.push( + new ParagraphRef({ + alignment, + children: [ + new TextRunRef({ + text: pageNumberText, + size: fontSizeHalfPt, + color: (component.fontColor || "#000000").replace("#", ""), + font: "맑은 고딕", + }), + ], + }) + ); + } + // Divider - 테이블 셀로 감싸서 정확한 너비 적용 else if (component.type === "divider" && component.orientation === "horizontal") { result.push( @@ -775,9 +817,11 @@ export class ReportController { }; // 섹션 생성 (페이지별) - const sections = layoutConfig.pages - .sort((a: any, b: any) => a.page_order - b.page_order) - .map((page: any) => { + const sortedPages = layoutConfig.pages.sort((a: any, b: any) => a.page_order - b.page_order); + const totalPagesCount = sortedPages.length; + + const sections = sortedPages + .map((page: any, pageIndex: number) => { const pageWidthTwip = mmToTwip(page.width); const pageHeightTwip = mmToTwip(page.height); const marginTopMm = page.margins?.top || 10; @@ -874,7 +918,7 @@ export class ReportController { } // 컴포넌트 셀 생성 - const cellContent = createCellContent(component, displayValue, pxToHalfPt, pxToTwip, queryResultsMap, AlignmentType, VerticalAlign, BorderStyle, Paragraph, TextRun, ImageRun, Table, TableRow, TableCell); + const cellContent = createCellContent(component, displayValue, pxToHalfPt, pxToTwip, queryResultsMap, AlignmentType, VerticalAlign, BorderStyle, Paragraph, TextRun, ImageRun, Table, TableRow, TableCell, pageIndex, totalPagesCount); cells.push( new TableCell({ children: cellContent, @@ -1162,6 +1206,79 @@ export class ReportController { lastBottomY = adjustedY + component.height; } + // PageNumber 컴포넌트 - 테이블 셀로 감싸서 정확한 위치 적용 + else if (component.type === "pageNumber") { + const format = component.pageNumberFormat || "number"; + const currentPageNum = pageIndex + 1; + let pageNumberText = ""; + switch (format) { + case "number": + pageNumberText = `${currentPageNum}`; + break; + case "numberTotal": + pageNumberText = `${currentPageNum} / ${totalPagesCount}`; + break; + case "koreanNumber": + pageNumberText = `${currentPageNum} 페이지`; + break; + default: + pageNumberText = `${currentPageNum}`; + } + const pageNumFontSize = pxToHalfPt(component.fontSize || 13); + const alignment = + component.textAlign === "center" + ? AlignmentType.CENTER + : component.textAlign === "right" + ? AlignmentType.RIGHT + : AlignmentType.LEFT; + + // 테이블 셀로 감싸서 width와 indent 정확히 적용 + const pageNumCell = new TableCell({ + children: [ + new Paragraph({ + alignment, + children: [ + new TextRun({ + text: pageNumberText, + size: pageNumFontSize, + color: (component.fontColor || "#000000").replace("#", ""), + font: "맑은 고딕", + }), + ], + }), + ], + 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" }, + }, + verticalAlign: VerticalAlign.TOP, + }); + + const pageNumTable = new Table({ + rows: [new TableRow({ children: [pageNumCell] })], + 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(pageNumTable); + lastBottomY = adjustedY + component.height; + } + // Table 컴포넌트 else if (component.type === "table" && component.queryId) { const queryResult = queryResultsMap[component.queryId]; diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index cb7b7347..90d8f21a 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -23,6 +23,8 @@ export function CanvasComponent({ component }: CanvasComponentProps) { canvasWidth, canvasHeight, margins, + layoutConfig, + currentPageId, } = useReportDesigner(); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); @@ -563,6 +565,43 @@ export function CanvasComponent({ component }: CanvasComponentProps) { ); + case "pageNumber": + // 페이지 번호 포맷 + const format = component.pageNumberFormat || "number"; + const sortedPages = layoutConfig.pages.sort((a, b) => a.page_order - b.page_order); + const currentPageIndex = sortedPages.findIndex((p) => p.page_id === currentPageId); + const totalPages = sortedPages.length; + const currentPageNum = currentPageIndex + 1; + + let pageNumberText = ""; + switch (format) { + case "number": + pageNumberText = `${currentPageNum}`; + break; + case "numberTotal": + pageNumberText = `${currentPageNum} / ${totalPages}`; + break; + case "koreanNumber": + pageNumberText = `${currentPageNum} 페이지`; + break; + default: + pageNumberText = `${currentPageNum}`; + } + + return ( +