diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-22 08:20:45 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-22 08:20:45 +0000 |
| commit | d38877eef87917087a4a217bea32ae84d6738a7d (patch) | |
| tree | 8713d9239e6fb34c4ca0704129906044784b30e5 /components | |
| parent | e9b1bf22cf2f45a7db8152f7ba83af7799ed71a5 (diff) | |
(최겸) 인포메이션 첨부파일 뷰어 추가
Diffstat (limited to 'components')
| -rw-r--r-- | components/document-viewer/pdftron-viewer-dialog.tsx | 207 | ||||
| -rw-r--r-- | components/information/information-button.tsx | 61 | ||||
| -rw-r--r-- | components/information/information-client.tsx | 2 |
3 files changed, 265 insertions, 5 deletions
diff --git a/components/document-viewer/pdftron-viewer-dialog.tsx b/components/document-viewer/pdftron-viewer-dialog.tsx new file mode 100644 index 00000000..58d2a91f --- /dev/null +++ b/components/document-viewer/pdftron-viewer-dialog.tsx @@ -0,0 +1,207 @@ +"use client" + +import * as React from "react" +import { useState, useRef, useEffect } from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { X, Loader2, AlertCircle } from "lucide-react" + +interface PDFTronViewerDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + fileUrl?: string + fileName?: string +} + + + +export function PDFTronViewerDialog({ + open, + onOpenChange, + fileUrl, + fileName +}: PDFTronViewerDialogProps) { + const viewerRef = useRef<HTMLDivElement>(null) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState<string | null>(null) + const [instance, setInstance] = useState<any>(null) + + // PDFTron WebViewer 초기화 + const initializeViewer = async () => { + if (!viewerRef.current || !fileUrl) return + + setIsLoading(true) + setError(null) + + try { + // @pdftron/webviewer 패키지에서 동적으로 import + const { default: WebViewer } = await import("@pdftron/webviewer") + + const viewerInstance = await WebViewer({ + path: '/pdftronWeb', + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + fullAPI: false, + enableFilePicker: false, + enableRedaction: false, + enableMeasurement: false, + enableAnnotations: false, + disabledElements: [ + 'header', + 'toolsHeader', + 'searchButton', + 'menuButton', + 'viewControlsButton', + 'selectToolButton', + 'freeHandToolButton', + 'textSelectButton', + 'panToolButton', + 'annotationCommentButton', + 'leftPanel', + 'leftPanelButton', + 'notesPanelButton', + 'searchPanel', + 'thumbnailControl', + 'outlineControl', + 'contextMenuPopup', + 'toolsOverlay', + 'signatureModal', + 'redactionModal', + 'linkModal', + 'filterModal', + 'passwordModal', + 'progressModal', + 'errorModal', + 'toolbarGroup-Annotate', + 'toolbarGroup-Shapes', + 'toolbarGroup-Edit', + 'toolbarGroup-Insert', + 'toolbarGroup-Forms', + 'toolbarGroup-View', + 'downloadButton', + 'printButton', + 'saveAsButton', + 'textToolButton', + 'stickyToolButton', + 'calloutToolButton', + 'rubberStampToolButton', + 'stampToolButton', + 'freeTextToolButton', + 'toolsButton' + ] + }, viewerRef.current) + + setInstance(viewerInstance) + + // 문서 로드 + if (fileUrl) { + const { documentViewer } = viewerInstance.Core + + // 문서 로드 완료 후 읽기 전용 설정 + documentViewer.addEventListener('documentLoaded', () => { + setIsLoading(false) + }) + + // 문서 로드 + viewerInstance.UI.loadDocument(fileUrl) + } else { + setIsLoading(false) + } + + } catch (err) { + console.error('PDFTron 뷰어 초기화 실패:', err) + setError('문서를 불러올 수 없습니다. PDFTron이 설치되어 있는지 확인해주세요.') + setIsLoading(false) + } + } + + // 다이얼로그가 열릴 때 뷰어 초기화 + useEffect(() => { + if (open && fileUrl) { + // 약간의 지연을 두어 DOM이 완전히 렌더링된 후 초기화 + const timer = setTimeout(initializeViewer, 100) + return () => clearTimeout(timer) + } + }, [open, fileUrl]) + + // 다이얼로그가 닫힐 때 정리 + useEffect(() => { + if (!open && instance) { + try { + // WebViewer 인스턴스 정리 + if (viewerRef.current) { + viewerRef.current.innerHTML = '' + } + setInstance(null) + } catch (err) { + console.error('뷰어 정리 실패:', err) + } + } + }, [open, instance]) + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-6xl max-h-[90vh] h-[90vh] p-0"> + <DialogHeader className="px-4 py-3 border-b"> + <div className="flex items-center justify-between"> + <div> + <DialogTitle className="text-lg font-semibold"> + {fileName || '문서 뷰어'} + </DialogTitle> + <DialogDescription className="text-sm text-gray-600 mt-1"> + PDF 및 Office 문서를 읽기 전용으로 보기 + </DialogDescription> + </div> + {/* <Button + variant="ghost" + size="sm" + onClick={() => onOpenChange(false)} + > + </Button> */} + </div> + </DialogHeader> + + <div className="flex-1 relative"> + {isLoading && ( + <div className="absolute inset-0 flex items-center justify-center bg-white z-10"> + <div className="flex flex-col items-center gap-3"> + <Loader2 className="h-8 w-8 animate-spin text-blue-500" /> + <span className="text-gray-600">문서를 불러오는 중...</span> + </div> + </div> + )} + + {error && ( + <div className="absolute inset-0 flex items-center justify-center bg-white z-10"> + <div className="flex flex-col items-center gap-3 text-center max-w-md"> + <AlertCircle className="h-12 w-12 text-red-500" /> + <div> + <h3 className="font-semibold text-gray-900 mb-2">문서를 불러올 수 없습니다</h3> + <p className="text-gray-600 text-sm">{error}</p> + </div> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + className="mt-2" + > + 닫기 + </Button> + </div> + </div> + )} + + <div + ref={viewerRef} + className="w-full h-full" + style={{ height: 'calc(90vh - 60px)' }} + /> + </div> + </DialogContent> + </Dialog> + ) +} diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx index 69cbb106..015894c1 100644 --- a/components/information/information-button.tsx +++ b/components/information/information-button.tsx @@ -11,15 +11,17 @@ import { DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
-import { Info, Download, Edit, Loader2 } from "lucide-react"
+import { Info, Download, Edit, Loader2,Eye, EyeIcon } from "lucide-react"
import { getPageInformationDirect, getEditPermissionDirect } from "@/lib/information/service"
import { getPageNotices } from "@/lib/notice/service"
import { UpdateInformationDialog } from "@/lib/information/table/update-information-dialog"
import { NoticeViewDialog } from "@/components/notice/notice-view-dialog"
+import { PDFTronViewerDialog } from "@/components/document-viewer/pdftron-viewer-dialog"
import type { PageInformation, InformationAttachment } from "@/db/schema/information"
import type { Notice } from "@/db/schema/notice"
import { useSession } from "next-auth/react"
import { formatDate } from "@/lib/utils"
+import prettyBytes from "pretty-bytes"
// downloadFile은 동적으로 import
interface InformationButtonProps {
@@ -51,6 +53,8 @@ export function InformationButton({ const [dataLoaded, setDataLoaded] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [retryCount, setRetryCount] = useState(0)
+ const [viewerDialogOpen, setViewerDialogOpen] = useState(false)
+ const [selectedFile, setSelectedFile] = useState<InformationAttachment | null>(null)
// 데이터 로드 함수
const loadData = React.useCallback(async () => {
@@ -151,6 +155,29 @@ export function InformationButton({ setIsNoticeViewDialogOpen(true)
}
+ // 파일 확장자 확인 함수
+ const getFileExtension = (fileName: string): string => {
+ return fileName.split('.').pop()?.toLowerCase() || ''
+ }
+
+ // 뷰어 지원 파일 형식 확인
+ const isViewerSupported = (fileName: string): boolean => {
+ const extension = getFileExtension(fileName)
+ return ['pdf', 'docx', 'doc'].includes(extension)
+ }
+
+ // 파일 클릭 핸들러 (뷰어 또는 다운로드)
+ const handleFileClick = async (attachment: InformationAttachment) => {
+ if (isViewerSupported(attachment.fileName)) {
+ // PDF/DOCX 파일은 뷰어로 열기
+ setSelectedFile(attachment)
+ setViewerDialogOpen(true)
+ } else {
+ // 기타 파일은 다운로드
+ await handleDownload(attachment)
+ }
+ }
+
// 파일 다운로드 핸들러
const handleDownload = async (attachment: InformationAttachment) => {
try {
@@ -294,25 +321,43 @@ export function InformationButton({ key={attachment.id}
className="flex items-center justify-between p-3 bg-white rounded border"
>
- <div className="flex-1">
- <div className="text-sm font-medium">
+ <div
+ className="flex-1"
+ >
+ <div className="text-sm font-medium flex items-center gap-2">
{attachment.fileName}
+
</div>
{attachment.fileSize && (
<div className="text-xs text-gray-500 mt-1">
- {attachment.fileSize}
+ {prettyBytes(Number(attachment.fileSize))}
</div>
)}
</div>
+ <div className="flex gap-2">
+ {isViewerSupported(attachment.fileName) &&
<Button
size="sm"
variant="outline"
+ disabled={!isViewerSupported(attachment.fileName)}
+ onClick={() => isViewerSupported(attachment.fileName) && handleFileClick(attachment)}
+ className="flex items-center gap-1"
+ >
+ <EyeIcon className="h-3 w-3" />
+ 미리보기
+ </Button>
+ }
+ <Button
+ size="sm"
+ variant="outline"
+
onClick={() => handleDownload(attachment)}
className="flex items-center gap-1"
>
<Download className="h-3 w-3" />
다운로드
</Button>
+ </div>
</div>
))}
</div>
@@ -345,6 +390,14 @@ export function InformationButton({ onSuccess={handleEditSuccess}
/>
)}
+
+ {/* PDFTron 뷰어 다이얼로그 */}
+ <PDFTronViewerDialog
+ open={viewerDialogOpen}
+ onOpenChange={setViewerDialogOpen}
+ fileUrl={selectedFile?.filePath}
+ fileName={selectedFile?.fileName}
+ />
</>
)
}
\ No newline at end of file diff --git a/components/information/information-client.tsx b/components/information/information-client.tsx index 50bc6a39..e2d273e5 100644 --- a/components/information/information-client.tsx +++ b/components/information/information-client.tsx @@ -370,7 +370,7 @@ export function InformationClient({ initialData = [] }: InformationClientProps) </div>
</TableCell>
<TableCell>
- <span className="font-mono text-sm max-w-[200px] truncate block">
+ <span className="font-mono text-sm max-w-[300px] truncate block">
{information.pagePath}
</span>
</TableCell>
|
