diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-20 11:15:54 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-20 11:15:54 +0000 |
| commit | 52b0f03803a94689ccc08578b5538405c88be1f2 (patch) | |
| tree | ae3c17182d65416c4737c67ad471a56863345bcd /components/file-manager | |
| parent | bbf276882ec813beee465cec785fd2d31ff15a54 (diff) | |
(대표님) 데이터룸 관련: 워터마크, PDF 뷰어, 로그인
Diffstat (limited to 'components/file-manager')
| -rw-r--r-- | components/file-manager/SecurePDFViewer.tsx | 70 | ||||
| -rw-r--r-- | components/file-manager/creaetWaterMarks.tsx | 233 |
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 |
