레포트에 페이지번호 컴포넌트 추가
This commit is contained in:
parent
0abe87ae1a
commit
0ed8e686c0
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
style={{
|
||||
fontSize: `${component.fontSize}px`,
|
||||
color: component.fontColor,
|
||||
fontWeight: component.fontWeight,
|
||||
textAlign: component.textAlign as "left" | "center" | "right",
|
||||
}}
|
||||
>
|
||||
{pageNumberText}
|
||||
</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 } from "lucide-react";
|
||||
import { Type, Table, Image, Minus, PenLine, Stamp as StampIcon, Hash } from "lucide-react";
|
||||
|
||||
interface ComponentItem {
|
||||
type: string;
|
||||
|
|
@ -16,6 +16,7 @@ const COMPONENTS: ComponentItem[] = [
|
|||
{ type: "divider", label: "구분선", icon: <Minus className="h-4 w-4" /> },
|
||||
{ type: "signature", label: "서명란", icon: <PenLine className="h-4 w-4" /> },
|
||||
{ type: "stamp", label: "도장란", icon: <StampIcon className="h-4 w-4" /> },
|
||||
{ type: "pageNumber", label: "페이지번호", icon: <Hash className="h-4 w-4" /> },
|
||||
];
|
||||
|
||||
function DraggableComponentItem({ type, label, icon }: ComponentItem) {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ export function ReportDesignerCanvas() {
|
|||
} else if (item.componentType === "stamp") {
|
||||
width = 70;
|
||||
height = 70;
|
||||
} else if (item.componentType === "pageNumber") {
|
||||
width = 100;
|
||||
height = 30;
|
||||
}
|
||||
|
||||
// 여백을 px로 변환 (1mm ≈ 3.7795px)
|
||||
|
|
@ -143,6 +146,11 @@ export function ReportDesignerCanvas() {
|
|||
borderWidth: 0,
|
||||
borderColor: "#cccccc",
|
||||
}),
|
||||
// 페이지 번호 전용
|
||||
...(item.componentType === "pageNumber" && {
|
||||
pageNumberFormat: "number" as const, // number, numberTotal, koreanNumber
|
||||
textAlign: "center" as const,
|
||||
}),
|
||||
// 테이블 전용
|
||||
...(item.componentType === "table" && {
|
||||
queryId: undefined,
|
||||
|
|
|
|||
|
|
@ -919,6 +919,37 @@ export function ReportDesignerRightPanel() {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* 페이지 번호 설정 */}
|
||||
{selectedComponent.type === "pageNumber" && (
|
||||
<Card className="mt-4 border-purple-200 bg-purple-50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-purple-900">페이지 번호 설정</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-xs">표시 형식</Label>
|
||||
<Select
|
||||
value={selectedComponent.pageNumberFormat || "number"}
|
||||
onValueChange={(value) =>
|
||||
updateComponent(selectedComponent.id, {
|
||||
pageNumberFormat: value as "number" | "numberTotal" | "koreanNumber",
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="number">숫자만 (1, 2, 3...)</SelectItem>
|
||||
<SelectItem value="numberTotal">현재/전체 (1 / 3)</SelectItem>
|
||||
<SelectItem value="koreanNumber">한글 (1 페이지)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 데이터 바인딩 (텍스트/라벨/테이블 컴포넌트) */}
|
||||
{(selectedComponent.type === "text" ||
|
||||
selectedComponent.type === "label" ||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
pageWidth: number,
|
||||
pageHeight: number,
|
||||
backgroundColor: string,
|
||||
pageIndex: number = 0,
|
||||
totalPages: number = 1,
|
||||
): string => {
|
||||
const componentsHTML = pageComponents
|
||||
.map((component) => {
|
||||
|
|
@ -139,6 +141,26 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
</div>`;
|
||||
}
|
||||
|
||||
// PageNumber 컴포넌트
|
||||
else if (component.type === "pageNumber") {
|
||||
const format = component.pageNumberFormat || "number";
|
||||
let pageNumberText = "";
|
||||
switch (format) {
|
||||
case "number":
|
||||
pageNumberText = `${pageIndex + 1}`;
|
||||
break;
|
||||
case "numberTotal":
|
||||
pageNumberText = `${pageIndex + 1} / ${totalPages}`;
|
||||
break;
|
||||
case "koreanNumber":
|
||||
pageNumberText = `${pageIndex + 1} 페이지`;
|
||||
break;
|
||||
default:
|
||||
pageNumberText = `${pageIndex + 1}`;
|
||||
}
|
||||
content = `<div style="display: flex; align-items: center; justify-content: center; height: 100%; font-size: ${component.fontSize}px; color: ${component.fontColor}; font-weight: ${component.fontWeight}; text-align: ${component.textAlign};">${pageNumberText}</div>`;
|
||||
}
|
||||
|
||||
// Table 컴포넌트
|
||||
else if (component.type === "table" && queryResult && queryResult.rows.length > 0) {
|
||||
const columns =
|
||||
|
|
@ -189,14 +211,18 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
|
||||
// 모든 페이지 HTML 생성 (인쇄/PDF용)
|
||||
const generatePrintHTML = (): string => {
|
||||
const pagesHTML = layoutConfig.pages
|
||||
.sort((a, b) => a.page_order - b.page_order)
|
||||
.map((page) =>
|
||||
const sortedPages = layoutConfig.pages.sort((a, b) => a.page_order - b.page_order);
|
||||
const totalPages = sortedPages.length;
|
||||
|
||||
const pagesHTML = sortedPages
|
||||
.map((page, pageIndex) =>
|
||||
generatePageHTML(
|
||||
Array.isArray(page.components) ? page.components : [],
|
||||
page.width,
|
||||
page.height,
|
||||
page.background_color,
|
||||
pageIndex,
|
||||
totalPages,
|
||||
),
|
||||
)
|
||||
.join('<div style="page-break-after: always;"></div>');
|
||||
|
|
@ -700,6 +726,44 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{component.type === "pageNumber" && (() => {
|
||||
const format = component.pageNumberFormat || "number";
|
||||
const pageIndex = layoutConfig.pages
|
||||
.sort((a, b) => a.page_order - b.page_order)
|
||||
.findIndex((p) => p.page_id === page.page_id);
|
||||
const totalPages = layoutConfig.pages.length;
|
||||
let pageNumberText = "";
|
||||
switch (format) {
|
||||
case "number":
|
||||
pageNumberText = `${pageIndex + 1}`;
|
||||
break;
|
||||
case "numberTotal":
|
||||
pageNumberText = `${pageIndex + 1} / ${totalPages}`;
|
||||
break;
|
||||
case "koreanNumber":
|
||||
pageNumberText = `${pageIndex + 1} 페이지`;
|
||||
break;
|
||||
default:
|
||||
pageNumberText = `${pageIndex + 1}`;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
fontSize: `${component.fontSize}px`,
|
||||
color: component.fontColor,
|
||||
fontWeight: component.fontWeight,
|
||||
}}
|
||||
>
|
||||
{pageNumberText}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,8 @@ export interface ComponentConfig {
|
|||
headerTextColor?: string; // 헤더 텍스트 색상
|
||||
showBorder?: boolean; // 테두리 표시
|
||||
rowHeight?: number; // 행 높이 (px)
|
||||
// 페이지 번호 전용
|
||||
pageNumberFormat?: "number" | "numberTotal" | "koreanNumber"; // 페이지 번호 포맷
|
||||
}
|
||||
|
||||
// 리포트 상세
|
||||
|
|
|
|||
Loading…
Reference in New Issue