179 lines
4.9 KiB
TypeScript
179 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { getFullImageUrl } from "@/lib/api/client";
|
|
import { WatermarkConfig } from "@/types/report";
|
|
|
|
interface Props {
|
|
watermark: WatermarkConfig;
|
|
/** 캔버스/페이지 너비 (px) */
|
|
width: number;
|
|
/** 캔버스/페이지 높이 (px) */
|
|
height: number;
|
|
}
|
|
|
|
/**
|
|
* 워터마크 레이어 공용 컴포넌트
|
|
*
|
|
* ReportDesignerCanvas 와 ReportPreviewModal 양쪽에서 사용.
|
|
* imageUrl 이 "data:" 로 시작하면 그대로 사용하고,
|
|
* 서버 경로인 경우 getFullImageUrl 로 변환한다.
|
|
*/
|
|
export function WatermarkLayer({ watermark, width, height }: Props) {
|
|
const baseStyle: React.CSSProperties = {
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
width: "100%",
|
|
height: "100%",
|
|
pointerEvents: "none",
|
|
overflow: "hidden",
|
|
zIndex: 1,
|
|
};
|
|
|
|
const rotation = watermark.rotation ?? -45;
|
|
|
|
const resolveImageSrc = (url: string): string => (url.startsWith("data:") ? url : getFullImageUrl(url));
|
|
|
|
const renderContent = (tileFontSize?: number) => {
|
|
if (watermark.type === "text") {
|
|
return (
|
|
<span
|
|
style={{
|
|
fontSize: `${tileFontSize ?? watermark.fontSize ?? 48}px`,
|
|
color: watermark.fontColor || "#cccccc",
|
|
fontWeight: "bold",
|
|
userSelect: "none",
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
{watermark.text || "WATERMARK"}
|
|
</span>
|
|
);
|
|
}
|
|
if (watermark.imageUrl) {
|
|
return null; // 이미지는 각 스타일 블록에서 직접 렌더링
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// 대각선 스타일
|
|
if (watermark.style === "diagonal") {
|
|
return (
|
|
<div style={baseStyle}>
|
|
<div
|
|
className="absolute flex items-center justify-center"
|
|
style={{
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: `translate(-50%, -50%) rotate(${rotation}deg)`,
|
|
opacity: watermark.opacity,
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
{watermark.type === "text"
|
|
? renderContent()
|
|
: watermark.imageUrl && (
|
|
<img
|
|
src={resolveImageSrc(watermark.imageUrl)}
|
|
alt="watermark"
|
|
style={{
|
|
maxWidth: "50%",
|
|
maxHeight: "50%",
|
|
objectFit: "contain",
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 중앙 스타일
|
|
if (watermark.style === "center") {
|
|
return (
|
|
<div style={baseStyle}>
|
|
<div
|
|
className="absolute flex items-center justify-center"
|
|
style={{
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
opacity: watermark.opacity,
|
|
}}
|
|
>
|
|
{watermark.type === "text"
|
|
? renderContent()
|
|
: watermark.imageUrl && (
|
|
<img
|
|
src={resolveImageSrc(watermark.imageUrl)}
|
|
alt="watermark"
|
|
style={{
|
|
maxWidth: "50%",
|
|
maxHeight: "50%",
|
|
objectFit: "contain",
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 타일 스타일
|
|
if (watermark.style === "tile") {
|
|
const tileRotation = watermark.rotation ?? -30;
|
|
const tileSize = watermark.type === "text" ? (watermark.fontSize || 48) * 4 : 150;
|
|
const cols = Math.ceil(width / tileSize) + 2;
|
|
const rows = Math.ceil(height / tileSize) + 2;
|
|
|
|
return (
|
|
<div style={baseStyle}>
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "-50%",
|
|
left: "-50%",
|
|
width: "200%",
|
|
height: "200%",
|
|
display: "flex",
|
|
flexWrap: "wrap",
|
|
alignContent: "flex-start",
|
|
transform: `rotate(${tileRotation}deg)`,
|
|
opacity: watermark.opacity,
|
|
}}
|
|
>
|
|
{Array.from({ length: rows * cols }).map((_, index) => (
|
|
<div
|
|
key={index}
|
|
style={{
|
|
width: `${tileSize}px`,
|
|
height: `${tileSize}px`,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
}}
|
|
>
|
|
{watermark.type === "text"
|
|
? renderContent(watermark.fontSize || 24)
|
|
: watermark.imageUrl && (
|
|
<img
|
|
src={resolveImageSrc(watermark.imageUrl)}
|
|
alt="watermark"
|
|
style={{
|
|
width: `${tileSize * 0.6}px`,
|
|
height: `${tileSize * 0.6}px`,
|
|
objectFit: "contain",
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|