diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-28 02:13:30 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-28 02:13:30 +0000 |
| commit | ef4c533ebacc2cdc97e518f30e9a9350004fcdfb (patch) | |
| tree | 345251a3ed0f4429716fa5edaa31024d8f4cb560 /lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | |
| parent | 9ceed79cf32c896f8a998399bf1b296506b2cd4a (diff) | |
~20250428 작업사항
Diffstat (limited to 'lib/basic-contract/viewer/basic-contract-sign-viewer.tsx')
| -rw-r--r-- | lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx new file mode 100644 index 00000000..0409151e --- /dev/null +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -0,0 +1,224 @@ +"use client"; + +import React, { + useState, + useEffect, + useRef, + SetStateAction, + Dispatch, +} from "react"; +import { WebViewerInstance } from "@pdftron/webviewer"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; + +interface BasicContractSignViewerProps { + contractId?: number; + filePath?: string; + isOpen?: boolean; + onClose?: () => void; + onSign?: (documentData: ArrayBuffer) => Promise<void>; + instance: WebViewerInstance | null; + setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>; +} + +export function BasicContractSignViewer({ + contractId, + filePath, + isOpen = false, + onClose, + onSign, + instance, + setInstance, +}: BasicContractSignViewerProps) { + const [fileLoading, setFileLoading] = useState<boolean>(true); + const viewer = useRef<HTMLDivElement>(null); + const initialized = useRef(false); + const isCancelled = useRef(false); + const [showDialog, setShowDialog] = useState(isOpen); + + // 다이얼로그 상태 동기화 + useEffect(() => { + setShowDialog(isOpen); + }, [isOpen]); + + // WebViewer 초기화 + useEffect(() => { + if (!initialized.current && viewer.current) { + initialized.current = true; + isCancelled.current = false; + + requestAnimationFrame(() => { + if (viewer.current) { + import("@pdftron/webviewer").then(({ default: WebViewer }) => { + if (isCancelled.current) { + console.log("📛 WebViewer 초기화 취소됨"); + return; + } + + // viewerElement이 확실히 존재함을 확인 + const viewerElement = viewer.current; + if (!viewerElement) return; + + WebViewer( + { + path: "/pdftronWeb", + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + fullAPI: true, + }, + viewerElement + ).then((instance: WebViewerInstance) => { + setInstance(instance); + setFileLoading(false); + + const { disableElements, setToolbarGroup } = instance.UI; + + disableElements([ + "toolbarGroup-Annotate", + "toolbarGroup-Shapes", + "toolbarGroup-Insert", + "toolbarGroup-Edit", + // "toolbarGroup-FillAndSign", + "toolbarGroup-Forms", + ]); + setToolbarGroup("toolbarGroup-View"); + }); + }); + } + }); + } + + return () => { + if (instance) { + instance.UI.dispose(); + } + isCancelled.current = true; + setTimeout(() => cleanupHtmlStyle(), 500); + }; + }, []); + + // 문서 로드 + useEffect(() => { + if (!instance || !filePath) return; + + loadDocument(instance, filePath); + }, [instance, filePath]); + + // 간소화된 문서 로드 함수 + const loadDocument = async (instance: WebViewerInstance, documentPath: string) => { + setFileLoading(true); + try { + const { documentViewer } = instance.Core; + + await documentViewer.loadDocument(documentPath, { extension: 'pdf' }); + + } catch (err) { + console.error("문서 로딩 중 오류 발생:", err); + toast.error("문서를 불러오는데 실패했습니다."); + } finally { + setFileLoading(false); + } + }; + + // 서명 저장 핸들러 + const handleSave = async () => { + if (!instance) return; + + try { + const { documentViewer } = instance.Core; + const doc = documentViewer.getDocument(); + + // 서명된 문서 데이터 가져오기 + const documentData = await doc.getFileData({ + includeAnnotations: true, + }); + + // 외부에서 제공된 onSign 핸들러가 있으면 호출 + if (onSign) { + await onSign(documentData); + } else { + // 기본 동작 - 서명 성공 메시지 표시 + toast.success("계약서가 성공적으로 서명되었습니다."); + } + + handleClose(); + } catch (err) { + console.error("서명 저장 중 오류 발생:", err); + toast.error("서명을 저장하는데 실패했습니다."); + } + }; + + // 다이얼로그 닫기 핸들러 + const handleClose = () => { + if (onClose) { + onClose(); + } else { + setShowDialog(false); + } + }; + + // 인라인 뷰어 렌더링 (다이얼로그 모드가 아닐 때) + if (!isOpen && !onClose) { + return ( + <div className="border rounded-md overflow-hidden" style={{ height: '600px' }}> + <div ref={viewer} className="h-[100%]"> + {fileLoading && ( + <div className="flex flex-col items-center justify-center py-12"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground">문서 로딩 중...</p> + </div> + )} + </div> + </div> + ); + } + + // 다이얼로그 뷰어 렌더링 + return ( + <Dialog open={showDialog} onOpenChange={handleClose}> + <DialogContent className="w-[70vw]" style={{ maxWidth: "none" }}> + <DialogHeader> + <DialogTitle>기본계약서 서명</DialogTitle> + <DialogDescription> + 계약서를 확인하고 서명을 진행해주세요. + </DialogDescription> + </DialogHeader> + <div className="h-[calc(70vh-60px)]"> + <div ref={viewer} className="h-[100%]"> + {fileLoading && ( + <div className="flex flex-col items-center justify-center py-12"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> + <p className="text-sm text-muted-foreground">문서 로딩 중...</p> + </div> + )} + </div> + </div> + <DialogFooter> + <Button variant="outline" onClick={handleClose} disabled={fileLoading}> + 취소 + </Button> + <Button onClick={handleSave} disabled={fileLoading}> + 서명 완료 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} + +// WebViewer 정리 함수 +const cleanupHtmlStyle = () => { + // iframe 스타일 정리 (WebViewer가 추가한 스타일) + const elements = document.querySelectorAll('.Document_container'); + elements.forEach((elem) => { + elem.remove(); + }); +};
\ No newline at end of file |
