레포트관리에 그리드 시스템 1차 적용(2차적인 개선 필요)
This commit is contained in:
48
frontend/components/report/designer/GridLayer.tsx
Normal file
48
frontend/components/report/designer/GridLayer.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { GridConfig } from "@/types/report";
|
||||
|
||||
interface GridLayerProps {
|
||||
gridConfig: GridConfig;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
}
|
||||
|
||||
export function GridLayer({ gridConfig, pageWidth, pageHeight }: GridLayerProps) {
|
||||
if (!gridConfig.visible) return null;
|
||||
|
||||
const { cellWidth, cellHeight, columns, rows, gridColor, gridOpacity } = gridConfig;
|
||||
|
||||
// SVG로 그리드 선 렌더링
|
||||
return (
|
||||
<svg className="pointer-events-none absolute inset-0" width={pageWidth} height={pageHeight} style={{ zIndex: 0 }}>
|
||||
{/* 세로 선 */}
|
||||
{Array.from({ length: columns + 1 }).map((_, i) => (
|
||||
<line
|
||||
key={`v-${i}`}
|
||||
x1={i * cellWidth}
|
||||
y1={0}
|
||||
x2={i * cellWidth}
|
||||
y2={pageHeight}
|
||||
stroke={gridColor}
|
||||
strokeOpacity={gridOpacity}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 가로 선 */}
|
||||
{Array.from({ length: rows + 1 }).map((_, i) => (
|
||||
<line
|
||||
key={`h-${i}`}
|
||||
x1={0}
|
||||
y1={i * cellHeight}
|
||||
x2={pageWidth}
|
||||
y2={i * cellHeight}
|
||||
stroke={gridColor}
|
||||
strokeOpacity={gridOpacity}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
138
frontend/components/report/designer/GridSettingsPanel.tsx
Normal file
138
frontend/components/report/designer/GridSettingsPanel.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
|
||||
export function GridSettingsPanel() {
|
||||
const { gridConfig, updateGridConfig } = useReportDesigner();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">그리드 설정</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* 그리드 표시 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">그리드 표시</Label>
|
||||
<Switch checked={gridConfig.visible} onCheckedChange={(visible) => updateGridConfig({ visible })} />
|
||||
</div>
|
||||
|
||||
{/* 스냅 활성화 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">그리드 스냅</Label>
|
||||
<Switch checked={gridConfig.snapToGrid} onCheckedChange={(snapToGrid) => updateGridConfig({ snapToGrid })} />
|
||||
</div>
|
||||
|
||||
{/* 프리셋 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">프리셋</Label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
const presets: Record<string, { cellWidth: number; cellHeight: number }> = {
|
||||
fine: { cellWidth: 10, cellHeight: 10 },
|
||||
medium: { cellWidth: 20, cellHeight: 20 },
|
||||
coarse: { cellWidth: 50, cellHeight: 50 },
|
||||
};
|
||||
updateGridConfig(presets[value]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue placeholder="그리드 크기 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fine">세밀 (10x10)</SelectItem>
|
||||
<SelectItem value="medium">중간 (20x20)</SelectItem>
|
||||
<SelectItem value="coarse">넓음 (50x50)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 셀 너비 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">셀 너비</Label>
|
||||
<span className="text-xs text-gray-500">{gridConfig.cellWidth}px</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[gridConfig.cellWidth]}
|
||||
onValueChange={([value]) => updateGridConfig({ cellWidth: value })}
|
||||
min={5}
|
||||
max={100}
|
||||
step={5}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 셀 높이 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">셀 높이</Label>
|
||||
<span className="text-xs text-gray-500">{gridConfig.cellHeight}px</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[gridConfig.cellHeight]}
|
||||
onValueChange={([value]) => updateGridConfig({ cellHeight: value })}
|
||||
min={5}
|
||||
max={100}
|
||||
step={5}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 그리드 투명도 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">투명도</Label>
|
||||
<span className="text-xs text-gray-500">{Math.round(gridConfig.gridOpacity * 100)}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[gridConfig.gridOpacity * 100]}
|
||||
onValueChange={([value]) => updateGridConfig({ gridOpacity: value / 100 })}
|
||||
min={10}
|
||||
max={100}
|
||||
step={10}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 그리드 색상 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">그리드 색상</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={gridConfig.gridColor}
|
||||
onChange={(e) => updateGridConfig({ gridColor: e.target.value })}
|
||||
className="h-8 w-16 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={gridConfig.gridColor}
|
||||
onChange={(e) => updateGridConfig({ gridColor: e.target.value })}
|
||||
className="h-8 flex-1 font-mono text-xs"
|
||||
placeholder="#e5e7eb"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 그리드 정보 */}
|
||||
<div className="rounded border bg-gray-50 p-2 text-xs text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>행:</span>
|
||||
<span className="font-mono">{gridConfig.rows}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>열:</span>
|
||||
<span className="font-mono">{gridConfig.columns}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { ComponentConfig } from "@/types/report";
|
||||
import { CanvasComponent } from "./CanvasComponent";
|
||||
import { Ruler } from "./Ruler";
|
||||
import { GridLayer } from "./GridLayer";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export function ReportDesignerCanvas() {
|
||||
@@ -32,6 +33,7 @@ export function ReportDesignerCanvas() {
|
||||
undo,
|
||||
redo,
|
||||
showRuler,
|
||||
gridConfig,
|
||||
} = useReportDesigner();
|
||||
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
@@ -331,16 +333,16 @@ export function ReportDesignerCanvas() {
|
||||
style={{
|
||||
width: `${canvasWidth}mm`,
|
||||
minHeight: `${canvasHeight}mm`,
|
||||
backgroundImage: showGrid
|
||||
? `
|
||||
linear-gradient(to right, #e5e7eb 1px, transparent 1px),
|
||||
linear-gradient(to bottom, #e5e7eb 1px, transparent 1px)
|
||||
`
|
||||
: undefined,
|
||||
backgroundSize: showGrid ? `${gridSize}px ${gridSize}px` : undefined,
|
||||
}}
|
||||
onClick={handleCanvasClick}
|
||||
>
|
||||
{/* 그리드 레이어 */}
|
||||
<GridLayer
|
||||
gridConfig={gridConfig}
|
||||
pageWidth={canvasWidth * 3.7795} // mm to px
|
||||
pageHeight={canvasHeight * 3.7795}
|
||||
/>
|
||||
|
||||
{/* 페이지 여백 가이드 */}
|
||||
{currentPage && (
|
||||
<div
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { QueryManager } from "./QueryManager";
|
||||
import { SignaturePad } from "./SignaturePad";
|
||||
import { SignatureGenerator } from "./SignatureGenerator";
|
||||
import { GridSettingsPanel } from "./GridSettingsPanel";
|
||||
import { reportApi } from "@/lib/api/reportApi";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
@@ -102,7 +103,7 @@ export function ReportDesignerRightPanel() {
|
||||
<div className="w-[450px] border-l bg-white">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full">
|
||||
<div className="border-b p-2">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="page" className="gap-1 text-xs">
|
||||
<Settings className="h-3 w-3" />
|
||||
페이지
|
||||
@@ -111,6 +112,10 @@ export function ReportDesignerRightPanel() {
|
||||
<Settings className="h-3 w-3" />
|
||||
속성
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="grid" className="gap-1 text-xs">
|
||||
<Settings className="h-3 w-3" />
|
||||
그리드
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="queries" className="gap-1 text-xs">
|
||||
<Database className="h-3 w-3" />
|
||||
쿼리
|
||||
@@ -1396,6 +1401,15 @@ export function ReportDesignerRightPanel() {
|
||||
</TabsContent>
|
||||
|
||||
{/* 쿼리 탭 */}
|
||||
{/* 그리드 탭 */}
|
||||
<TabsContent value="grid" className="mt-0 h-[calc(100vh-120px)]">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="space-y-4 p-4">
|
||||
<GridSettingsPanel />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="queries" className="mt-0 h-[calc(100vh-120px)]">
|
||||
<QueryManager />
|
||||
</TabsContent>
|
||||
|
||||
@@ -13,7 +13,21 @@ import { Printer, FileDown, FileText } from "lucide-react";
|
||||
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||||
import { useState } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Document, Packer, Paragraph, TextRun, Table, TableCell, TableRow, WidthType } from "docx";
|
||||
// @ts-ignore - docx 라이브러리 타입 이슈
|
||||
import {
|
||||
Document,
|
||||
Packer,
|
||||
Paragraph,
|
||||
TextRun,
|
||||
Table,
|
||||
TableCell,
|
||||
TableRow,
|
||||
WidthType,
|
||||
ImageRun,
|
||||
AlignmentType,
|
||||
VerticalAlign,
|
||||
convertInchesToTwip,
|
||||
} from "docx";
|
||||
import { getFullImageUrl } from "@/lib/api/client";
|
||||
|
||||
interface ReportPreviewModalProps {
|
||||
@@ -268,82 +282,263 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
|
||||
});
|
||||
};
|
||||
|
||||
// Base64를 Uint8Array로 변환
|
||||
const base64ToUint8Array = (base64: string): Uint8Array => {
|
||||
const base64Data = base64.split(",")[1] || base64;
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
// 컴포넌트를 TableCell로 변환
|
||||
const createTableCell = (component: any, pageWidth: number, widthPercent?: number): TableCell | null => {
|
||||
const cellWidth = widthPercent || 100;
|
||||
|
||||
if (component.type === "text" || component.type === "label") {
|
||||
const value = getComponentValue(component);
|
||||
return new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: value,
|
||||
size: (component.fontSize || 13) * 2,
|
||||
color: component.fontColor?.replace("#", "") || "000000",
|
||||
bold: component.fontWeight === "bold",
|
||||
}),
|
||||
],
|
||||
alignment:
|
||||
component.textAlign === "center"
|
||||
? AlignmentType.CENTER
|
||||
: component.textAlign === "right"
|
||||
? AlignmentType.RIGHT
|
||||
: AlignmentType.LEFT,
|
||||
}),
|
||||
],
|
||||
width: { size: cellWidth, type: WidthType.PERCENTAGE },
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
} else if (component.type === "signature" || component.type === "stamp") {
|
||||
if (component.imageUrl) {
|
||||
try {
|
||||
const imageData = base64ToUint8Array(component.imageUrl);
|
||||
return new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: imageData,
|
||||
transformation: {
|
||||
width: component.width || 150,
|
||||
height: component.height || 50,
|
||||
},
|
||||
}),
|
||||
],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
],
|
||||
width: { size: cellWidth, type: WidthType.PERCENTAGE },
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return new TableCell({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `[${component.type === "signature" ? "서명" : "도장"}]`,
|
||||
size: 24,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
width: { size: cellWidth, type: WidthType.PERCENTAGE },
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (component.type === "table" && component.queryId) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows.length > 0) {
|
||||
const headerCells = queryResult.fields.map(
|
||||
(field) =>
|
||||
new TableCell({
|
||||
children: [new Paragraph({ text: field })],
|
||||
width: { size: 100 / queryResult.fields.length, type: WidthType.PERCENTAGE },
|
||||
}),
|
||||
);
|
||||
|
||||
const dataRows = queryResult.rows.map(
|
||||
(row) =>
|
||||
new TableRow({
|
||||
children: queryResult.fields.map(
|
||||
(field) =>
|
||||
new TableCell({
|
||||
children: [new Paragraph({ text: String(row[field] ?? "") })],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const table = new Table({
|
||||
rows: [new TableRow({ children: headerCells }), ...dataRows],
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
});
|
||||
|
||||
return new TableCell({
|
||||
children: [table],
|
||||
width: { size: cellWidth, type: WidthType.PERCENTAGE },
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// WORD 다운로드
|
||||
const handleDownloadWord = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
// 컴포넌트를 Paragraph로 변환
|
||||
const paragraphs: (Paragraph | Table)[] = [];
|
||||
|
||||
// 모든 페이지의 컴포넌트 수집
|
||||
const allComponents = layoutConfig.pages
|
||||
// 페이지별로 섹션 생성
|
||||
const sections = layoutConfig.pages
|
||||
.sort((a, b) => a.page_order - b.page_order)
|
||||
.flatMap((page) => page.components);
|
||||
.map((page) => {
|
||||
// 페이지 크기 설정 (A4 기준)
|
||||
const pageWidth = convertInchesToTwip(8.27); // A4 width in inches
|
||||
const pageHeight = convertInchesToTwip(11.69); // A4 height in inches
|
||||
const marginTop = convertInchesToTwip(page.margins.top / 96); // px to inches (96 DPI)
|
||||
const marginBottom = convertInchesToTwip(page.margins.bottom / 96);
|
||||
const marginLeft = convertInchesToTwip(page.margins.left / 96);
|
||||
const marginRight = convertInchesToTwip(page.margins.right / 96);
|
||||
|
||||
// Y 좌표로 정렬
|
||||
const sortedComponents = [...allComponents].sort((a, b) => a.y - b.y);
|
||||
// 페이지 내 컴포넌트를 Y좌표 기준으로 정렬
|
||||
const sortedComponents = [...page.components].sort((a, b) => {
|
||||
// Y좌표 우선, 같으면 X좌표
|
||||
if (Math.abs(a.y - b.y) < 5) {
|
||||
return a.x - b.x;
|
||||
}
|
||||
return a.y - b.y;
|
||||
});
|
||||
|
||||
for (const component of sortedComponents) {
|
||||
if (component.type === "text" || component.type === "label") {
|
||||
const value = getComponentValue(component);
|
||||
paragraphs.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: value,
|
||||
size: (component.fontSize || 13) * 2, // pt to half-pt
|
||||
color: component.fontColor?.replace("#", "") || "000000",
|
||||
bold: component.fontWeight === "bold",
|
||||
}),
|
||||
],
|
||||
spacing: {
|
||||
after: 200,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else if (component.type === "table" && component.queryId) {
|
||||
const queryResult = getQueryResult(component.queryId);
|
||||
if (queryResult && queryResult.rows.length > 0) {
|
||||
// 테이블 헤더
|
||||
const headerCells = queryResult.fields.map(
|
||||
(field) =>
|
||||
new TableCell({
|
||||
children: [new Paragraph({ text: field })],
|
||||
width: { size: 100 / queryResult.fields.length, type: WidthType.PERCENTAGE },
|
||||
}),
|
||||
);
|
||||
// 컴포넌트들을 행으로 그룹화 (같은 Y 범위 = 같은 행)
|
||||
const rows: Array<Array<(typeof sortedComponents)[0]>> = [];
|
||||
const rowTolerance = 20; // Y 좌표 허용 오차
|
||||
|
||||
// 테이블 행
|
||||
const dataRows = queryResult.rows.map(
|
||||
(row) =>
|
||||
new TableRow({
|
||||
children: queryResult.fields.map(
|
||||
(field) =>
|
||||
new TableCell({
|
||||
children: [new Paragraph({ text: String(row[field] ?? "") })],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const table = new Table({
|
||||
rows: [new TableRow({ children: headerCells }), ...dataRows],
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
});
|
||||
|
||||
paragraphs.push(table);
|
||||
for (const component of sortedComponents) {
|
||||
const existingRow = rows.find((row) => Math.abs(row[0].y - component.y) < rowTolerance);
|
||||
if (existingRow) {
|
||||
existingRow.push(component);
|
||||
} else {
|
||||
rows.push([component]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 각 행 내에서 X좌표로 정렬
|
||||
rows.forEach((row) => row.sort((a, b) => a.x - b.x));
|
||||
|
||||
// 페이지 전체를 큰 테이블로 구성 (레이아웃 제어용)
|
||||
const tableRows: TableRow[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.length === 1) {
|
||||
// 단일 컴포넌트 - 전체 너비 사용
|
||||
const component = row[0];
|
||||
const cell = createTableCell(component, pageWidth);
|
||||
if (cell) {
|
||||
tableRows.push(
|
||||
new TableRow({
|
||||
children: [cell],
|
||||
height: { value: component.height * 15, rule: 1 }, // 최소 높이 설정
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 여러 컴포넌트 - 가로 배치
|
||||
const cells: TableCell[] = [];
|
||||
const totalWidth = row.reduce((sum, c) => sum + c.width, 0);
|
||||
|
||||
for (const component of row) {
|
||||
const widthPercent = (component.width / totalWidth) * 100;
|
||||
const cell = createTableCell(component, pageWidth, widthPercent);
|
||||
if (cell) {
|
||||
cells.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
if (cells.length > 0) {
|
||||
const maxHeight = Math.max(...row.map((c) => c.height));
|
||||
tableRows.push(
|
||||
new TableRow({
|
||||
children: cells,
|
||||
height: { value: maxHeight * 15, rule: 1 },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
properties: {
|
||||
page: {
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
margin: {
|
||||
top: marginTop,
|
||||
bottom: marginBottom,
|
||||
left: marginLeft,
|
||||
right: marginRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
children:
|
||||
tableRows.length > 0
|
||||
? [
|
||||
new Table({
|
||||
rows: tableRows,
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
borders: {
|
||||
top: { style: 0, size: 0, color: "FFFFFF" },
|
||||
bottom: { style: 0, size: 0, color: "FFFFFF" },
|
||||
left: { style: 0, size: 0, color: "FFFFFF" },
|
||||
right: { style: 0, size: 0, color: "FFFFFF" },
|
||||
insideHorizontal: { style: 0, size: 0, color: "FFFFFF" },
|
||||
insideVertical: { style: 0, size: 0, color: "FFFFFF" },
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [new Paragraph({ text: "" })],
|
||||
};
|
||||
});
|
||||
|
||||
// 문서 생성
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children: paragraphs,
|
||||
},
|
||||
],
|
||||
sections,
|
||||
});
|
||||
|
||||
// Blob 생성 및 다운로드
|
||||
|
||||
Reference in New Issue
Block a user