summaryrefslogtreecommitdiff
path: root/components/file-manager
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-20 11:15:54 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-20 11:15:54 +0000
commit52b0f03803a94689ccc08578b5538405c88be1f2 (patch)
treeae3c17182d65416c4737c67ad471a56863345bcd /components/file-manager
parentbbf276882ec813beee465cec785fd2d31ff15a54 (diff)
(대표님) 데이터룸 관련: 워터마크, PDF 뷰어, 로그인
Diffstat (limited to 'components/file-manager')
-rw-r--r--components/file-manager/SecurePDFViewer.tsx70
-rw-r--r--components/file-manager/creaetWaterMarks.tsx233
2 files changed, 203 insertions, 100 deletions
diff --git a/components/file-manager/SecurePDFViewer.tsx b/components/file-manager/SecurePDFViewer.tsx
index 707d95dc..eeb59b83 100644
--- a/components/file-manager/SecurePDFViewer.tsx
+++ b/components/file-manager/SecurePDFViewer.tsx
@@ -55,10 +55,10 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
viewerElement
).then(async (instance: WebViewerInstance) => {
instanceRef.current = instance;
-
+
try {
const { disableElements } = instance.UI;
-
+
// 보안을 위해 모든 도구 비활성화
disableElements([
'toolsHeader',
@@ -137,10 +137,10 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
}
console.log('📝 WebViewer 초기화 완료');
-
+
// 문서 로드
await loadSecureDocument(instance, documentUrl, fileName);
-
+
} catch (uiError) {
console.warn('⚠️ UI 설정 중 오류:', uiError);
toast.error('뷰어 설정 중 오류가 발생했습니다.');
@@ -166,19 +166,19 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
// 문서 로드 함수
const loadSecureDocument = async (
- instance: WebViewerInstance,
- documentPath: string,
+ instance: WebViewerInstance,
+ documentPath: string,
docFileName: string
) => {
setIsLoading(true);
try {
// 절대 URL로 변환
- const fullPath = documentPath.startsWith('http')
- ? documentPath
+ const fullPath = documentPath.startsWith('http')
+ ? documentPath
: `${window.location.origin}${documentPath}`;
-
+
console.log('📄 보안 문서 로드 시작:', fullPath);
-
+
// 문서 로드
await instance.UI.loadDocument(fullPath, {
filename: docFileName,
@@ -190,28 +190,28 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
// 문서 로드 완료 이벤트
documentViewer.addEventListener('documentLoaded', async () => {
setIsLoading(false);
-
+
// 워터마크 추가
const watermarkText = `${session?.user?.email || 'CONFIDENTIAL'}\n${new Date().toLocaleString()}`;
-
+
// 대각선 워터마크
- documentViewer.setWatermark(
- {custom:createCustomWatermark({
- text: watermarkText,
- fontSize: 30,
- fontFamily: 'Arial',
- color: 'rgba(255, 0, 0, 0.3)',
- opacity: 30,
- // diagonal: true,
- })}
- );
+ documentViewer.setWatermark({
+ custom: createCustomWatermark({
+ text: watermarkText,
+ fontSize: 14,
+ fontFamily: 'Arial',
+ color: 'rgba(128, 128, 128, 0.15)', // 연한 회색
+ opacity: 15, // 15% 투명도
+ rotation: -45,
+ })
+ });
// 각 페이지에 커스텀 워터마크 추가
const pageCount = documentViewer.getPageCount();
for (let i = 1; i <= pageCount; i++) {
const pageInfo = documentViewer.getDocument().getPageInfo(i);
const { width, height } = pageInfo;
-
+
// FreeTextAnnotation 생성
const watermarkAnnot = new instance.Core.Annotations.FreeTextAnnotation();
watermarkAnnot.PageNumber = i;
@@ -230,21 +230,21 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
watermarkAnnot.ReadOnly = true;
watermarkAnnot.Locked = true;
watermarkAnnot.Printable = true;
-
+
annotationManager.addAnnotation(watermarkAnnot);
}
-
+
annotationManager.drawAnnotations(documentViewer.getCurrentPage());
-
+
// Pan 모드로 설정 (텍스트 선택 불가)
documentViewer.setToolMode(documentViewer.getTool('Pan'));
-
+
// 페이지 이동 로깅
documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
console.log(`Page ${pageNumber} viewed at ${new Date().toISOString()}`);
// 서버로 감사 로그 전송 가능
});
-
+
console.log('✅ 보안 문서 로드 완료');
toast.success('문서가 안전하게 로드되었습니다.');
});
@@ -255,7 +255,7 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
setIsLoading(false);
toast.error('문서 로드 중 오류가 발생했습니다.');
});
-
+
} catch (err) {
console.error('❌ 문서 로딩 중 오류:', err);
toast.error(`문서 로드 실패: ${err instanceof Error ? err.message : '알 수 없는 오류'}`);
@@ -294,7 +294,7 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
document.addEventListener('keydown', preventShortcuts);
document.addEventListener('contextmenu', preventContextMenu);
document.addEventListener('dragstart', preventDrag);
-
+
return () => {
document.removeEventListener('keydown', preventShortcuts);
document.removeEventListener('contextmenu', preventContextMenu);
@@ -304,8 +304,8 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
return (
<div className="relative w-full h-full overflow-hidden">
- <div
- ref={viewerRef}
+ <div
+ ref={viewerRef}
className="w-full h-full"
style={{
position: 'relative',
@@ -336,11 +336,11 @@ export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFVie
</div>
)}
</div>
-
+
{/* 보안 오버레이 */}
- <div
+ <div
className="absolute inset-0 z-10 pointer-events-none"
- style={{
+ style={{
background: 'transparent',
userSelect: 'none',
WebkitUserSelect: 'none',
diff --git a/components/file-manager/creaetWaterMarks.tsx b/components/file-manager/creaetWaterMarks.tsx
index 524b18ee..a9072150 100644
--- a/components/file-manager/creaetWaterMarks.tsx
+++ b/components/file-manager/creaetWaterMarks.tsx
@@ -1,71 +1,174 @@
export const createCustomWatermark: CreateCustomWatermark = ({
- text,
- fontSize,
- color,
- opacity,
- rotation = -45,
- fontFamily = "Helvetica",
- }) => {
- return (ctx, pageNumber, pageWidth, pageHeight) => {
- if (!text) return;
-
- const lines = text.split("\n"); // 줄바꿈 기준 멀치 처리
-
- ctx.save();
- ctx.translate(pageWidth / 2, pageHeight / 2);
- ctx.rotate((rotation * Math.PI) / 180);
- ctx.fillStyle = color;
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
-
- const lineHeights = lines.map((s) => {
- return fontSize;
- });
-
- const totalHeight =
- lineHeights.reduce((sum, h) => sum + h, 0) - lineHeights[0]; // 첫 줄은 기준선 0
-
- let yOffset = -totalHeight / 2;
-
- lines.forEach((line, i) => {
- ctx.font = `900 ${fontSize}px ${fontFamily}`;
- ctx.fillText(line, 0, yOffset);
- yOffset += lineHeights[i];
- });
-
- ctx.restore();
- };
+ text,
+ fontSize = 14,
+ color = "rgba(128, 128, 128, 0.15)", // 더 연한 회색, 더 투명하게
+ opacity = 15, // 더 낮은 opacity
+ rotation = -45,
+ fontFamily = "Arial",
+}) => {
+ return (ctx, pageNumber, pageWidth, pageHeight) => {
+ if (!text) return;
+
+ const lines = text.split("\n");
+
+ ctx.save();
+
+ // 전체 opacity 설정
+ ctx.globalAlpha = opacity / 100;
+
+ // 텍스트 스타일 설정
+ ctx.fillStyle = color;
+ ctx.font = `${fontSize}px ${fontFamily}`;
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+
+ // 텍스트 크기 계산
+ const maxLineWidth = Math.max(...lines.map(line =>
+ ctx.measureText(line).width
+ ));
+ const lineHeight = fontSize * 1.2;
+ const textBlockHeight = lines.length * lineHeight;
+
+ // 격자 간격 계산 (텍스트 블록 크기 기반)
+ const horizontalSpacing = maxLineWidth * 1.8; // 가로 간격
+ const verticalSpacing = textBlockHeight * 2.5; // 세로 간격
+
+ // 회전을 고려한 대각선 길이 계산
+ const diagonal = Math.sqrt(pageWidth * pageWidth + pageHeight * pageHeight);
+ const rotationRad = (rotation * Math.PI) / 180;
+
+ // 시작 위치 계산 (페이지 밖에서 시작하여 전체 커버)
+ const startX = -diagonal / 2;
+ const startY = -diagonal / 2;
+ const endX = diagonal / 2;
+ const endY = diagonal / 2;
+
+ // 격자 패턴으로 워터마크 그리기
+ for (let x = startX; x <= endX; x += horizontalSpacing) {
+ for (let y = startY; y <= endY; y += verticalSpacing) {
+ ctx.save();
+
+ // 중심점으로 이동
+ ctx.translate(pageWidth / 2, pageHeight / 2);
+
+ // 회전 적용
+ ctx.rotate(rotationRad);
+
+ // 현재 위치로 이동
+ ctx.translate(x, y);
+
+ // 각 줄 그리기
+ let yOffset = -(lines.length - 1) * lineHeight / 2;
+ lines.forEach((line) => {
+ ctx.fillText(line, 0, yOffset);
+ yOffset += lineHeight;
+ });
+
+ ctx.restore();
+ }
+ }
+
+ ctx.restore();
};
-
+};
- import { Core, WebViewerInstance } from "@pdftron/webviewer";
+// 대안 1: 더 촘촘한 격자 패턴 (보안 강화)
+export const createDenseGridWatermark: CreateCustomWatermark = ({
+ text,
+ fontSize = 12, // 더 작은 폰트
+ color = "rgba(150, 150, 150, 0.08)", // 매우 연한 색상
+ opacity = 8,
+ rotation = -45,
+ fontFamily = "Arial",
+}) => {
+ return (ctx, pageNumber, pageWidth, pageHeight) => {
+ if (!text) return;
-export interface WaterMarkOption {
- fontSize: number;
- color: string;
- opacity: number;
- rotation: number;
- fontFamily: string;
- split: boolean;
- shipNameCheck: boolean;
- shipName: string;
- ownerNameCheck: boolean;
- ownerName: string;
- classNameCheck: boolean;
- className: string;
- classList: string[];
- customCheck: boolean;
- text: string;
-}
+ const lines = text.split("\n");
+
+ ctx.save();
+ ctx.globalAlpha = opacity / 100;
+ ctx.fillStyle = color;
+ ctx.font = `${fontSize}px ${fontFamily}`;
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+
+ // 더 촘촘한 간격
+ const horizontalSpacing = 120; // 고정 간격
+ const verticalSpacing = 80;
+
+ const diagonal = Math.sqrt(pageWidth * pageWidth + pageHeight * pageHeight);
+ const rotationRad = (rotation * Math.PI) / 180;
+
+ for (let x = -diagonal; x <= diagonal; x += horizontalSpacing) {
+ for (let y = -diagonal; y <= diagonal; y += verticalSpacing) {
+ ctx.save();
+ ctx.translate(pageWidth / 2 + x, pageHeight / 2 + y);
+ ctx.rotate(rotationRad);
+
+ let yOffset = 0;
+ lines.forEach((line) => {
+ ctx.fillText(line, 0, yOffset);
+ yOffset += fontSize * 1.2;
+ });
+
+ ctx.restore();
+ }
+ }
+
+ ctx.restore();
+ };
+};
-type CreateCustomWatermark = ({
+// 대안 2: 랜덤 위치 워터마크 (패턴 예측 방지)
+export const createRandomizedWatermark: CreateCustomWatermark = ({
text,
- fontSize,
- color,
- opacity,
- rotation,
- fontFamily,
-}: Pick<
- WaterMarkOption,
- "text" | "fontSize" | "color" | "opacity" | "rotation" | "fontFamily"
->) => Core.DocumentViewer.CustomWatermarkCallback; \ No newline at end of file
+ fontSize = 14,
+ color = "rgba(140, 140, 140, 0.1)",
+ opacity = 10,
+ rotation = -45,
+ fontFamily = "Arial",
+}) => {
+ return (ctx, pageNumber, pageWidth, pageHeight) => {
+ if (!text) return;
+
+ const lines = text.split("\n");
+
+ ctx.save();
+ ctx.globalAlpha = opacity / 100;
+ ctx.fillStyle = color;
+ ctx.font = `${fontSize}px ${fontFamily}`;
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+
+ // 페이지 번호를 시드로 사용하여 일관된 랜덤 생성
+ const seed = pageNumber * 1000;
+ const random = (min: number, max: number, index: number) => {
+ const x = Math.sin(seed + index) * 10000;
+ return min + (x - Math.floor(x)) * (max - min);
+ };
+
+ // 워터마크 개수 (페이지 크기에 비례)
+ const count = Math.floor((pageWidth * pageHeight) / 40000); // 조정 가능
+
+ for (let i = 0; i < count; i++) {
+ const x = random(0, pageWidth, i * 2);
+ const y = random(0, pageHeight, i * 2 + 1);
+ const rotationVariation = random(-5, 5, i * 3); // ±5도 변화
+
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.rotate((rotation + rotationVariation) * Math.PI / 180);
+
+ let yOffset = 0;
+ lines.forEach((line) => {
+ ctx.fillText(line, 0, yOffset);
+ yOffset += fontSize * 1.2;
+ });
+
+ ctx.restore();
+ }
+
+ ctx.restore();
+ };
+}; \ No newline at end of file