summaryrefslogtreecommitdiff
path: root/components/file-manager/SecurePDFViewer.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-29 13:31:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-29 13:31:40 +0000
commit4614210aa9878922cfa1e424ce677ef893a1b6b2 (patch)
tree5e7edcce05fbee207230af0a43ed08cd351d7c4f /components/file-manager/SecurePDFViewer.tsx
parente41e3af4e72870d44a94b03e0f3246d6ccaaca48 (diff)
(대표님) 구매 권한설정, data room 등
Diffstat (limited to 'components/file-manager/SecurePDFViewer.tsx')
-rw-r--r--components/file-manager/SecurePDFViewer.tsx350
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