워터마크를 전체 페이지 공유 방식으로 변경

This commit is contained in:
dohyeons 2025-12-22 17:06:11 +09:00
parent d7f015b37d
commit 5f26e998e3
7 changed files with 120 additions and 136 deletions

View File

@ -3067,8 +3067,8 @@ export class ReportController {
children.push(new Paragraph({ children: [] }));
}
// 워터마크 헤더 생성 (워터마크가 활성화된 경우)
const watermark: WatermarkConfig | undefined = page.watermark;
// 워터마크 헤더 생성 (전체 페이지 공유 워터마크)
const watermark: WatermarkConfig | undefined = layoutConfig.watermark;
let headers: { default?: Header } | undefined;
if (watermark?.enabled && watermark.type === "text" && watermark.text) {

View File

@ -147,12 +147,12 @@ export interface PageConfig {
right: number;
};
components: any[];
watermark?: WatermarkConfig;
}
// 레이아웃 설정
export interface ReportLayoutConfig {
pages: PageConfig[];
watermark?: WatermarkConfig; // 전체 페이지 공유 워터마크
}
// 레이아웃 저장 요청

View File

@ -213,6 +213,7 @@ export function ReportDesignerCanvas() {
undo,
redo,
showRuler,
layoutConfig,
} = useReportDesigner();
const [{ isOver }, drop] = useDrop(() => ({
@ -608,10 +609,10 @@ export function ReportDesignerCanvas() {
/>
)}
{/* 워터마크 렌더링 */}
{currentPage?.watermark?.enabled && (
{/* 워터마크 렌더링 (전체 페이지 공유) */}
{layoutConfig.watermark?.enabled && (
<WatermarkLayer
watermark={currentPage.watermark}
watermark={layoutConfig.watermark}
canvasWidth={canvasWidth * MM_TO_PX}
canvasHeight={canvasHeight * MM_TO_PX}
/>

View File

@ -31,6 +31,8 @@ export function ReportDesignerRightPanel() {
currentPageId,
updatePageSettings,
getQueryResult,
layoutConfig,
updateWatermark,
} = context;
const [activeTab, setActiveTab] = useState<string>("properties");
const [uploadingImage, setUploadingImage] = useState(false);
@ -101,7 +103,7 @@ export function ReportDesignerRightPanel() {
// 워터마크 이미지 업로드 핸들러
const handleWatermarkImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !currentPageId) return;
if (!file) return;
// 파일 타입 체크
if (!file.type.startsWith("image/")) {
@ -129,12 +131,10 @@ export function ReportDesignerRightPanel() {
const result = await reportApi.uploadImage(file);
if (result.success) {
// 업로드된 이미지 URL을 워터마크에 설정
updatePageSettings(currentPageId, {
watermark: {
...currentPage!.watermark!,
imageUrl: result.data.fileUrl,
},
// 업로드된 이미지 URL을 전체 워터마크에 설정
updateWatermark({
...layoutConfig.watermark!,
imageUrl: result.data.fileUrl,
});
toast({
@ -2690,44 +2690,40 @@ export function ReportDesignerRightPanel() {
</CardContent>
</Card>
{/* 워터마크 설정 */}
{/* 워터마크 설정 (전체 페이지 공유) */}
<Card>
<CardHeader>
<CardTitle className="text-sm"></CardTitle>
<CardTitle className="text-sm"> ( )</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{/* 워터마크 활성화 */}
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={currentPage.watermark?.enabled ?? false}
checked={layoutConfig.watermark?.enabled ?? false}
onCheckedChange={(checked) =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark,
enabled: checked,
type: currentPage.watermark?.type ?? "text",
opacity: currentPage.watermark?.opacity ?? 0.3,
style: currentPage.watermark?.style ?? "diagonal",
},
updateWatermark({
...layoutConfig.watermark,
enabled: checked,
type: layoutConfig.watermark?.type ?? "text",
opacity: layoutConfig.watermark?.opacity ?? 0.3,
style: layoutConfig.watermark?.style ?? "diagonal",
})
}
/>
</div>
{currentPage.watermark?.enabled && (
{layoutConfig.watermark?.enabled && (
<>
{/* 워터마크 타입 */}
<div>
<Label className="text-xs"></Label>
<Select
value={currentPage.watermark?.type ?? "text"}
value={layoutConfig.watermark?.type ?? "text"}
onValueChange={(value: "text" | "image") =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
type: value,
},
updateWatermark({
...layoutConfig.watermark!,
type: value,
})
}
>
@ -2742,18 +2738,16 @@ export function ReportDesignerRightPanel() {
</div>
{/* 텍스트 워터마크 설정 */}
{currentPage.watermark?.type === "text" && (
{layoutConfig.watermark?.type === "text" && (
<>
<div>
<Label className="text-xs"></Label>
<Input
value={currentPage.watermark?.text ?? ""}
value={layoutConfig.watermark?.text ?? ""}
onChange={(e) =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
text: e.target.value,
},
updateWatermark({
...layoutConfig.watermark!,
text: e.target.value,
})
}
placeholder="DRAFT, 대외비 등"
@ -2765,13 +2759,11 @@ export function ReportDesignerRightPanel() {
<Label className="text-xs"> </Label>
<Input
type="number"
value={currentPage.watermark?.fontSize ?? 48}
value={layoutConfig.watermark?.fontSize ?? 48}
onChange={(e) =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
fontSize: Number(e.target.value),
},
updateWatermark({
...layoutConfig.watermark!,
fontSize: Number(e.target.value),
})
}
className="mt-1"
@ -2784,26 +2776,22 @@ export function ReportDesignerRightPanel() {
<div className="mt-1 flex gap-1">
<Input
type="color"
value={currentPage.watermark?.fontColor ?? "#cccccc"}
value={layoutConfig.watermark?.fontColor ?? "#cccccc"}
onChange={(e) =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
fontColor: e.target.value,
},
updateWatermark({
...layoutConfig.watermark!,
fontColor: e.target.value,
})
}
className="h-9 w-12 cursor-pointer p-1"
/>
<Input
type="text"
value={currentPage.watermark?.fontColor ?? "#cccccc"}
value={layoutConfig.watermark?.fontColor ?? "#cccccc"}
onChange={(e) =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
fontColor: e.target.value,
},
updateWatermark({
...layoutConfig.watermark!,
fontColor: e.target.value,
})
}
className="flex-1"
@ -2815,7 +2803,7 @@ export function ReportDesignerRightPanel() {
)}
{/* 이미지 워터마크 설정 */}
{currentPage.watermark?.type === "image" && (
{layoutConfig.watermark?.type === "image" && (
<div>
<Label className="text-xs"> </Label>
<div className="mt-1 flex gap-2">
@ -2843,21 +2831,19 @@ export function ReportDesignerRightPanel() {
) : (
<>
<Upload className="mr-2 h-4 w-4" />
{currentPage.watermark?.imageUrl ? "이미지 변경" : "이미지 선택"}
{layoutConfig.watermark?.imageUrl ? "이미지 변경" : "이미지 선택"}
</>
)}
</Button>
{currentPage.watermark?.imageUrl && (
{layoutConfig.watermark?.imageUrl && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
imageUrl: "",
},
updateWatermark({
...layoutConfig.watermark!,
imageUrl: "",
})
}
>
@ -2868,9 +2854,9 @@ export function ReportDesignerRightPanel() {
<p className="text-muted-foreground mt-1 text-[10px]">
JPG, PNG, GIF, WEBP ( 5MB)
</p>
{currentPage.watermark?.imageUrl && (
{layoutConfig.watermark?.imageUrl && (
<p className="mt-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-xs text-indigo-600">
현재: ...{currentPage.watermark.imageUrl.slice(-30)}
현재: ...{layoutConfig.watermark.imageUrl.slice(-30)}
</p>
)}
</div>
@ -2880,13 +2866,11 @@ export function ReportDesignerRightPanel() {
<div>
<Label className="text-xs"> </Label>
<Select
value={currentPage.watermark?.style ?? "diagonal"}
value={layoutConfig.watermark?.style ?? "diagonal"}
onValueChange={(value: "diagonal" | "center" | "tile") =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
style: value,
},
updateWatermark({
...layoutConfig.watermark!,
style: value,
})
}
>
@ -2902,19 +2886,17 @@ export function ReportDesignerRightPanel() {
</div>
{/* 대각선/타일 회전 각도 */}
{(currentPage.watermark?.style === "diagonal" ||
currentPage.watermark?.style === "tile") && (
{(layoutConfig.watermark?.style === "diagonal" ||
layoutConfig.watermark?.style === "tile") && (
<div>
<Label className="text-xs"> </Label>
<Input
type="number"
value={currentPage.watermark?.rotation ?? -45}
value={layoutConfig.watermark?.rotation ?? -45}
onChange={(e) =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
rotation: Number(e.target.value),
},
updateWatermark({
...layoutConfig.watermark!,
rotation: Number(e.target.value),
})
}
className="mt-1"
@ -2929,17 +2911,15 @@ export function ReportDesignerRightPanel() {
<div className="flex items-center justify-between">
<Label className="text-xs"></Label>
<span className="text-muted-foreground text-xs">
{Math.round((currentPage.watermark?.opacity ?? 0.3) * 100)}%
{Math.round((layoutConfig.watermark?.opacity ?? 0.3) * 100)}%
</span>
</div>
<Slider
value={[(currentPage.watermark?.opacity ?? 0.3) * 100]}
value={[(layoutConfig.watermark?.opacity ?? 0.3) * 100]}
onValueChange={(value) =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
opacity: value[0] / 100,
},
updateWatermark({
...layoutConfig.watermark!,
opacity: value[0] / 100,
})
}
min={5}
@ -2955,17 +2935,15 @@ export function ReportDesignerRightPanel() {
size="sm"
variant="outline"
onClick={() =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
type: "text",
text: "DRAFT",
fontSize: 64,
fontColor: "#cccccc",
style: "diagonal",
opacity: 0.2,
rotation: -45,
},
updateWatermark({
...layoutConfig.watermark!,
type: "text",
text: "DRAFT",
fontSize: 64,
fontColor: "#cccccc",
style: "diagonal",
opacity: 0.2,
rotation: -45,
})
}
>
@ -2975,17 +2953,15 @@ export function ReportDesignerRightPanel() {
size="sm"
variant="outline"
onClick={() =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
type: "text",
text: "대외비",
fontSize: 64,
fontColor: "#ff0000",
style: "diagonal",
opacity: 0.15,
rotation: -45,
},
updateWatermark({
...layoutConfig.watermark!,
type: "text",
text: "대외비",
fontSize: 64,
fontColor: "#ff0000",
style: "diagonal",
opacity: 0.15,
rotation: -45,
})
}
>
@ -2995,17 +2971,15 @@ export function ReportDesignerRightPanel() {
size="sm"
variant="outline"
onClick={() =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
type: "text",
text: "SAMPLE",
fontSize: 48,
fontColor: "#888888",
style: "tile",
opacity: 0.1,
rotation: -30,
},
updateWatermark({
...layoutConfig.watermark!,
type: "text",
text: "SAMPLE",
fontSize: 48,
fontColor: "#888888",
style: "tile",
opacity: 0.1,
rotation: -30,
})
}
>
@ -3015,16 +2989,14 @@ export function ReportDesignerRightPanel() {
size="sm"
variant="outline"
onClick={() =>
updatePageSettings(currentPageId, {
watermark: {
...currentPage.watermark!,
type: "text",
text: "COPY",
fontSize: 56,
fontColor: "#aaaaaa",
style: "center",
opacity: 0.25,
},
updateWatermark({
...layoutConfig.watermark!,
type: "text",
text: "COPY",
fontSize: 56,
fontColor: "#aaaaaa",
style: "center",
opacity: 0.25,
})
}
>

View File

@ -924,7 +924,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
page.background_color,
pageIndex,
totalPages,
page.watermark,
layoutConfig.watermark, // 전체 페이지 공유 워터마크
),
)
.join('<div style="page-break-after: always;"></div>');
@ -1156,10 +1156,10 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps)
backgroundColor: page.background_color,
}}
>
{/* 워터마크 렌더링 */}
{page.watermark?.enabled && (
{/* 워터마크 렌더링 (전체 페이지 공유) */}
{layoutConfig.watermark?.enabled && (
<PreviewWatermarkLayer
watermark={page.watermark}
watermark={layoutConfig.watermark}
pageWidth={page.width}
pageHeight={page.height}
/>

View File

@ -1,7 +1,7 @@
"use client";
import { createContext, useContext, useState, useCallback, ReactNode, useEffect, useRef } from "react";
import { ComponentConfig, ReportDetail, ReportLayout, ReportPage, ReportLayoutConfig } from "@/types/report";
import { ComponentConfig, ReportDetail, ReportLayout, ReportPage, ReportLayoutConfig, WatermarkConfig } from "@/types/report";
import { reportApi } from "@/lib/api/reportApi";
import { useToast } from "@/hooks/use-toast";
import { v4 as uuidv4 } from "uuid";
@ -40,6 +40,7 @@ interface ReportDesignerContextType {
reorderPages: (sourceIndex: number, targetIndex: number) => void;
selectPage: (pageId: string) => void;
updatePageSettings: (pageId: string, settings: Partial<ReportPage>) => void;
updateWatermark: (watermark: WatermarkConfig | undefined) => void; // 전체 페이지 공유 워터마크
// 컴포넌트 (현재 페이지)
components: ComponentConfig[]; // currentPage의 components (읽기 전용)
@ -988,10 +989,19 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
const updatePageSettings = useCallback((pageId: string, settings: Partial<ReportPage>) => {
setLayoutConfig((prev) => ({
...prev,
pages: prev.pages.map((page) => (page.page_id === pageId ? { ...page, ...settings } : page)),
}));
}, []);
// 전체 페이지 공유 워터마크 업데이트
const updateWatermark = useCallback((watermark: WatermarkConfig | undefined) => {
setLayoutConfig((prev) => ({
...prev,
watermark,
}));
}, []);
// 리포트 및 레이아웃 로드
const loadLayout = useCallback(async () => {
setIsLoading(true);
@ -1471,6 +1481,7 @@ export function ReportDesignerProvider({ reportId, children }: { reportId: strin
reorderPages,
selectPage,
updatePageSettings,
updateWatermark,
// 컴포넌트 (현재 페이지)
components,

View File

@ -113,12 +113,12 @@ export interface ReportPage {
};
background_color: string;
components: ComponentConfig[];
watermark?: WatermarkConfig;
}
// 레이아웃 설정 (페이지 기반)
export interface ReportLayoutConfig {
pages: ReportPage[];
watermark?: WatermarkConfig; // 전체 페이지 공유 워터마크
}
// 컴포넌트 설정