diff --git a/backend-node/src/controllers/reportController.ts b/backend-node/src/controllers/reportController.ts index 124eb265..5f755947 100644 --- a/backend-node/src/controllers/reportController.ts +++ b/backend-node/src/controllers/reportController.ts @@ -27,7 +27,11 @@ import { BorderStyle, PageOrientation, convertMillimetersToTwip, + Header, + Footer, + HeadingLevel, } from "docx"; +import { WatermarkConfig } from "../types/report"; import bwipjs from "bwip-js"; export class ReportController { @@ -3063,6 +3067,36 @@ export class ReportController { children.push(new Paragraph({ children: [] })); } + // 워터마크 헤더 생성 (워터마크가 활성화된 경우) + const watermark: WatermarkConfig | undefined = page.watermark; + let headers: { default?: Header } | undefined; + + if (watermark?.enabled && watermark.type === "text" && watermark.text) { + // 워터마크 색상을 hex로 변환 (alpha 적용) + const opacity = watermark.opacity ?? 0.3; + const fontColor = watermark.fontColor || "#CCCCCC"; + // hex 색상에서 # 제거 + const cleanColor = fontColor.replace("#", ""); + + headers = { + default: new Header({ + children: [ + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new TextRun({ + text: watermark.text, + size: (watermark.fontSize || 48) * 2, // Word는 half-point 사용 + color: cleanColor, + bold: true, + }), + ], + }), + ], + }), + }; + } + return { properties: { page: { @@ -3082,6 +3116,7 @@ export class ReportController { }, }, }, + headers, children, }; }); diff --git a/backend-node/src/types/report.ts b/backend-node/src/types/report.ts index 23a7496d..d5641cff 100644 --- a/backend-node/src/types/report.ts +++ b/backend-node/src/types/report.ts @@ -116,6 +116,22 @@ export interface UpdateReportRequest { useYn?: string; } +// 워터마크 설정 +export interface WatermarkConfig { + enabled: boolean; + type: "text" | "image"; + // 텍스트 워터마크 + text?: string; + fontSize?: number; + fontColor?: string; + // 이미지 워터마크 + imageUrl?: string; + // 공통 설정 + opacity: number; // 0~1 + style: "diagonal" | "center" | "tile"; + rotation?: number; // 대각선일 때 각도 (기본 -45) +} + // 페이지 설정 export interface PageConfig { page_id: string; @@ -131,6 +147,7 @@ export interface PageConfig { right: number; }; components: any[]; + watermark?: WatermarkConfig; } // 레이아웃 설정 diff --git a/frontend/components/report/designer/ReportDesignerCanvas.tsx b/frontend/components/report/designer/ReportDesignerCanvas.tsx index f278cd97..7f63123b 100644 --- a/frontend/components/report/designer/ReportDesignerCanvas.tsx +++ b/frontend/components/report/designer/ReportDesignerCanvas.tsx @@ -3,15 +3,192 @@ import { useRef, useEffect } from "react"; import { useDrop } from "react-dnd"; import { useReportDesigner } from "@/contexts/ReportDesignerContext"; -import { ComponentConfig } from "@/types/report"; +import { ComponentConfig, WatermarkConfig } from "@/types/report"; import { CanvasComponent } from "./CanvasComponent"; import { Ruler } from "./Ruler"; import { v4 as uuidv4 } from "uuid"; +import { getFullImageUrl } from "@/lib/api/client"; // mm를 px로 변환하는 고정 스케일 팩터 (화면 해상도와 무관하게 일정) // A4 기준: 210mm x 297mm → 840px x 1188px export const MM_TO_PX = 4; +// 워터마크 레이어 컴포넌트 +interface WatermarkLayerProps { + watermark: WatermarkConfig; + canvasWidth: number; + canvasHeight: number; +} + +function WatermarkLayer({ watermark, canvasWidth, canvasHeight }: WatermarkLayerProps) { + // 공통 스타일 + const baseStyle: React.CSSProperties = { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + pointerEvents: "none", + overflow: "hidden", + zIndex: 1, // 컴포넌트보다 낮은 z-index + }; + + // 대각선 스타일 + if (watermark.style === "diagonal") { + const rotation = watermark.rotation ?? -45; + return ( +
+ JPG, PNG, GIF, WEBP (최대 5MB) +
+ {currentPage.watermark?.imageUrl && ( ++ 현재: ...{currentPage.watermark.imageUrl.slice(-30)} +
+ )} +