diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
| commit | 4614210aa9878922cfa1e424ce677ef893a1b6b2 (patch) | |
| tree | 5e7edcce05fbee207230af0a43ed08cd351d7c4f /components/file-manager/SecurePDFViewer.tsx | |
| parent | e41e3af4e72870d44a94b03e0f3246d6ccaaca48 (diff) | |
(대표님) 구매 권한설정, data room 등
Diffstat (limited to 'components/file-manager/SecurePDFViewer.tsx')
| -rw-r--r-- | components/file-manager/SecurePDFViewer.tsx | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/components/file-manager/SecurePDFViewer.tsx b/components/file-manager/SecurePDFViewer.tsx new file mode 100644 index 00000000..cd7c081a --- /dev/null +++ b/components/file-manager/SecurePDFViewer.tsx @@ -0,0 +1,350 @@ +'use client'; + +import React, { useEffect, useRef, useState } from 'react'; +import { useSession } from 'next-auth/react'; +import { WebViewerInstance } from '@pdftron/webviewer'; +import { Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; + +interface SecurePDFViewerProps { + documentUrl: string; + fileName: string; + onClose?: () => void; +} + +export function SecurePDFViewer({ documentUrl, fileName, onClose }: SecurePDFViewerProps) { + const viewerRef = useRef<HTMLDivElement>(null); + const instanceRef = useRef<WebViewerInstance | null>(null); + const initialized = useRef(false); + const isCancelled = useRef(false); + const { data: session } = useSession(); + const [isLoading, setIsLoading] = useState(true); + + // WebViewer 초기화 + useEffect(() => { + if (!initialized.current && viewerRef.current) { + initialized.current = true; + isCancelled.current = false; + + requestAnimationFrame(() => { + if (viewerRef.current) { + import('@pdftron/webviewer').then(({ default: WebViewer }) => { + if (isCancelled.current) { + console.log('📛 WebViewer 초기화 취소됨'); + return; + } + + const viewerElement = viewerRef.current; + if (!viewerElement) return; + + WebViewer( + { + path: '/pdftronWeb', // BasicContractTemplateViewer와 동일한 경로 + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, // 동일한 라이센스 키 + fullAPI: true, + enableFilePicker: false, + enableMeasurement: false, + enableRedaction: false, + enableAnnotations: false, + enableTextSelection: false, + enableFormFilling: false, + enablePrint: false, + enableDownload: false, + }, + viewerElement + ).then(async (instance: WebViewerInstance) => { + instanceRef.current = instance; + + try { + const { disableElements } = instance.UI; + + // 보안을 위해 모든 도구 비활성화 + disableElements([ + 'toolsHeader', + 'viewControlsButton', + 'panToolButton', + 'selectToolButton', + 'menuButton', + 'leftPanel', + 'leftPanelButton', + 'searchButton', + 'notesPanel', + 'notesPanelButton', + 'toolbarGroup-Annotate', + 'toolbarGroup-Shapes', + 'toolbarGroup-Edit', + 'toolbarGroup-Insert', + 'toolbarGroup-FillAndSign', + 'toolbarGroup-Forms', + 'toolsOverlay', + 'printButton', + 'downloadButton', + 'saveAsButton', + 'filePickerHandler', + 'textPopup', + 'contextMenuPopup', + 'pageManipulationOverlay', + 'documentControl', + 'header', + 'ribbons', + 'toggleNotesButton' + ]); + + // CSS 적용으로 추가 보안 + const iframeWindow = instance.UI.iframeWindow; + if (iframeWindow && iframeWindow.document) { + const style = iframeWindow.document.createElement('style'); + style.textContent = ` + /* Hide all toolbars and buttons */ + .HeaderToolsContainer, + .Header, + .ToolsHeader, + .LeftHeader, + .RightHeader, + .DocumentContainer > div:first-child { + display: none !important; + } + + /* Disable right-click context menu */ + * { + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + } + + /* Hide page controls */ + .DocumentContainer .PageControls { + display: none !important; + } + + /* Disable text selection cursor */ + .pageContainer { + cursor: default !important; + } + + /* Prevent drag and drop */ + * { + -webkit-user-drag: none !important; + -khtml-user-drag: none !important; + -moz-user-drag: none !important; + -o-user-drag: none !important; + user-drag: none !important; + } + `; + iframeWindow.document.head.appendChild(style); + } + + console.log('📝 WebViewer 초기화 완료'); + + // 문서 로드 + await loadSecureDocument(instance, documentUrl, fileName); + + } catch (uiError) { + console.warn('⚠️ UI 설정 중 오류:', uiError); + toast.error('뷰어 설정 중 오류가 발생했습니다.'); + } + }).catch((error) => { + console.error('❌ WebViewer 초기화 실패:', error); + setIsLoading(false); + toast.error('뷰어 초기화에 실패했습니다.'); + }); + }); + } + }); + } + + return () => { + if (instanceRef.current) { + instanceRef.current.UI.dispose(); + instanceRef.current = null; + } + isCancelled.current = true; + }; + }, []); + + // 문서 로드 함수 + const loadSecureDocument = async ( + instance: WebViewerInstance, + documentPath: string, + docFileName: string + ) => { + setIsLoading(true); + try { + // 절대 URL로 변환 + const fullPath = documentPath.startsWith('http') + ? documentPath + : `${window.location.origin}${documentPath}`; + + console.log('📄 보안 문서 로드 시작:', fullPath); + + // 문서 로드 + await instance.UI.loadDocument(fullPath, { + filename: docFileName, + extension: docFileName.split('.').pop()?.toLowerCase(), + }); + + const { documentViewer, annotationManager } = instance.Core; + + // 문서 로드 완료 이벤트 + documentViewer.addEventListener('documentLoaded', async () => { + setIsLoading(false); + + // 워터마크 추가 + const watermarkText = `${session?.user?.email || 'CONFIDENTIAL'}\n${new Date().toLocaleString()}`; + + // 대각선 워터마크 + documentViewer.setWatermark({ + text: watermarkText, + fontSize: 30, + fontFamily: 'Arial', + color: 'rgba(255, 0, 0, 0.3)', + opacity: 30, + diagonal: true, + }); + + // 각 페이지에 커스텀 워터마크 추가 + 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; + watermarkAnnot.X = width / 4; + watermarkAnnot.Y = height / 2; + watermarkAnnot.Width = width / 2; + watermarkAnnot.Height = 100; + watermarkAnnot.setContents( + `${session?.user?.email}\n${docFileName}\n${new Date().toLocaleDateString()}` + ); + watermarkAnnot.FillColor = new instance.Core.Annotations.Color(255, 0, 0, 0.1); + watermarkAnnot.TextColor = new instance.Core.Annotations.Color(255, 0, 0, 0.3); + watermarkAnnot.FontSize = '24pt'; + watermarkAnnot.TextAlign = 'center'; + watermarkAnnot.Rotation = -45; + 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('문서가 안전하게 로드되었습니다.'); + }); + + // 에러 처리 + documentViewer.addEventListener('error', (error: any) => { + console.error('Document loading error:', error); + setIsLoading(false); + toast.error('문서 로드 중 오류가 발생했습니다.'); + }); + + } catch (err) { + console.error('❌ 문서 로딩 중 오류:', err); + toast.error(`문서 로드 실패: ${err instanceof Error ? err.message : '알 수 없는 오류'}`); + setIsLoading(false); + } + }; + + // 키보드 단축키 차단 + useEffect(() => { + const preventShortcuts = (e: KeyboardEvent) => { + // Ctrl+C, Ctrl+A, Ctrl+P, Ctrl+S, F12 등 차단 + if ( + (e.ctrlKey && ['c', 'a', 'p', 's', 'x', 'v'].includes(e.key.toLowerCase())) || + (e.metaKey && ['c', 'a', 'p', 's', 'x', 'v'].includes(e.key.toLowerCase())) || + e.key === 'F12' || + (e.ctrlKey && e.shiftKey && ['I', 'C', 'J'].includes(e.key)) + ) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }; + + const preventContextMenu = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }; + + const preventDrag = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }; + + document.addEventListener('keydown', preventShortcuts); + document.addEventListener('contextmenu', preventContextMenu); + document.addEventListener('dragstart', preventDrag); + + return () => { + document.removeEventListener('keydown', preventShortcuts); + document.removeEventListener('contextmenu', preventContextMenu); + document.removeEventListener('dragstart', preventDrag); + }; + }, []); + + return ( + <div className="relative w-full h-full overflow-hidden"> + <div + ref={viewerRef} + className="w-full h-full" + style={{ + position: 'relative', + overflow: 'hidden', + contain: 'layout style paint', + minHeight: '600px' + }} + onCopy={(e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }} + onCut={(e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }} + onPaste={(e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }} + > + {isLoading && ( + <div className="absolute inset-0 flex flex-col items-center justify-center bg-white bg-opacity-90 z-50"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground">보안 문서 로딩 중...</p> + </div> + )} + </div> + + {/* 보안 오버레이 */} + <div + className="absolute inset-0 z-10 pointer-events-none" + style={{ + background: 'transparent', + userSelect: 'none', + WebkitUserSelect: 'none', + MozUserSelect: 'none', + msUserSelect: 'none' + }} + /> + </div> + ); +}
\ No newline at end of file |
