diff options
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 |
