// user-vendor-document-display.tsx - 수정된 버전 "use client" import React from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Building, FileText, AlertCircle, Eye, Download, Loader2, Plus, Trash2, Edit } from "lucide-react" import { SimplifiedDocumentsTable } from "@/lib/vendor-document-list/ship-all/enhanced-documents-table" import { getUserVendorDocumentsAll, getUserVendorDocumentStatsAll, } from "@/lib/vendor-document-list/enhanced-document-service" import { SimplifiedDocumentsView } from "@/db/schema" import { WebViewerInstance } from "@pdftron/webviewer" import { useRouter } from 'next/navigation' import { downloadFile } from "@/lib/file-download" /* ------------------------------------------------------------------------------------------------- * Types & Constants * -----------------------------------------------------------------------------------------------*/ interface UserVendorDocumentDisplayProps { allPromises: Promise<[ Awaited>, // 문서 목록 Awaited>, // 통계 데이터 ]> } interface StageInfo { id: number stageName: string stageStatus: string stageOrder: number planDate: string | null actualDate: string | null assigneeName: string | null priority: string revisions: RevisionInfo[] } interface RevisionInfo { id: number serialNo: string | null issueStageId: number revision: string uploaderType: string uploaderId: number | null uploaderName: string | null comment: string | null usage: string | null usageType: string | null revisionStatus: string submittedDate: string | null approvedDate: string | null uploadedAt: string | null reviewStartDate: string | null rejectedDate: string | null reviewerId: number | null reviewerName: string | null reviewComments: string | null createdAt: Date updatedAt: Date stageName?: string attachments: AttachmentInfo[] } interface AttachmentInfo { id: number revisionId: number fileName: string filePath: string dolceFilePath: string | null fileSize: number | null fileType: string | null createdAt: Date updatedAt: Date } interface DocumentSelectionContextType { selectedDocumentId: number | null selectedStageId: number | null selectedRevisionId: number | null setSelectedDocumentId: (id: number | null) => void setSelectedStageId: (id: number | null) => void setSelectedRevisionId: (id: number | null) => void allData: SimplifiedDocumentsView[] | null setAllData: (data: SimplifiedDocumentsView[]) => void } export const DocumentSelectionContextAll = React.createContext( { selectedDocumentId: null, selectedStageId: null, selectedRevisionId: null, setSelectedDocumentId: (_id: number | null) => { }, setSelectedStageId: (_id: number | null) => { }, setSelectedRevisionId: (_id: number | null) => { }, allData: null, setAllData: (_data: SimplifiedDocumentsView[]) => { }, }, ) /* ------------------------------------------------------------------------------------------------- * Revision & Attachment Tables - 너비 최적화 * -----------------------------------------------------------------------------------------------*/ function getUsageTypeDisplay(usageType: string | null): string { if (!usageType) return '-' // B3 용도 타입 축약 표시 const abbreviations: Record = { 'Approval Submission Full': 'AS-F', 'Approval Submission Partial': 'AS-P', 'Approval Completion Full': 'AC-F', 'Approval Completion Partial': 'AC-P', 'Working Full': 'W-F', 'Working Partial': 'W-P', 'Reference Full': 'R-F', 'Reference Partial': 'R-P', 'Reference Series Full': 'RS-F', 'Reference Series Partial': 'RS-P', } return abbreviations[usageType] || usageType } function RevisionTable({ revisions, onViewRevision, }: { revisions: RevisionInfo[] onViewRevision: (revision: RevisionInfo) => void }) { const { selectedRevisionId, setSelectedRevisionId } = React.useContext(DocumentSelectionContextAll) const toggleSelect = (revisionId: number) => { setSelectedRevisionId(revisionId === selectedRevisionId ? null : revisionId) } const canEditRevision = React.useCallback((revision: RevisionInfo) => { if ((!revision.attachments || revision.attachments.length === 0) && revision.uploaderType === "vendor") { return true } return revision.attachments.every(attachment => !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' ) }, []) const getRevisionProcessStatus = React.useCallback((revision: RevisionInfo) => { if (!revision.attachments || revision.attachments.length === 0) { return 'no-files' } const processedCount = revision.attachments.filter(attachment => attachment.dolceFilePath && attachment.dolceFilePath.trim() !== '' ).length if (processedCount === 0) { return 'not-processed' } else if (processedCount === revision.attachments.length) { return 'fully-processed' } else { return 'partially-processed' } }, []) return ( Revisions
Sel Serial No Rev Category Usage Type Status Uploader Comment Date Files Actions {revisions.map((revision) => { const canEdit = canEditRevision(revision) const processStatus = getRevisionProcessStatus(revision) return ( toggleSelect(revision.id)} className="h-3 w-3 cursor-pointer" /> {revision.serialNo || ''}
{revision.revision} {processStatus === 'fully-processed' && (
)} {processStatus === 'partially-processed' && (
)}
{revision.uploaderType === "vendor" ? "To" : "From"} {revision.usage || '-'} {revision.usageType ? ( getUsageTypeDisplay(revision.usageType) ) : ( - )} {revision.revisionStatus.slice(0, 8)} {revision.uploaderName || '-'} {revision.comment ? (

{revision.comment}

) : ( - )}
{revision.uploadedAt ? new Date(revision.uploadedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '-'}
{revision.attachments.length}
{revision.attachments.length > 0 && ( )}
) })}
) } function AttachmentTable({ attachments, onDownloadFile, }: { attachments: AttachmentInfo[] onDownloadFile: (attachment: AttachmentInfo) => void }) { const { selectedRevisionId, allData, setAllData } = React.useContext(DocumentSelectionContextAll) const [addAttachmentDialogOpen, setAddAttachmentDialogOpen] = React.useState(false) const router = useRouter() const selectedRevisionInfo = React.useMemo(() => { if (!selectedRevisionId || !allData) return null for (const doc of allData) { if (doc.allStages) { for (const stage of doc.allStages as StageInfo[]) { const revision = stage.revisions.find(r => r.id === selectedRevisionId) if (revision) return revision } } } return null }, [selectedRevisionId, allData]) const handleAddAttachment = React.useCallback(() => { if (selectedRevisionInfo) { setAddAttachmentDialogOpen(true) } }, [selectedRevisionInfo]) const canDeleteFile = React.useCallback((attachment: AttachmentInfo) => { return !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' }, []) const handleAttachmentUploadSuccess = React.useCallback((uploadResult?: any) => { if (!selectedRevisionId || !allData || !uploadResult?.data) { console.log('🔄 Full refresh') router.refresh() return } try { const newAttachments: AttachmentInfo[] = uploadResult.data.uploadedFiles?.map((file: any) => ({ id: file.id, revisionId: selectedRevisionId, fileName: file.fileName, filePath: file.filePath, dolceFilePath: null, fileSize: file.fileSize, fileType: file.fileType || null, createdAt: new Date(), updatedAt: new Date(), })) || [] const updatedData = allData.map(doc => { const updatedDoc = { ...doc } if (updatedDoc.allStages) { const stages = [...updatedDoc.allStages as StageInfo[]] for (const stage of stages) { const revisionIndex = stage.revisions.findIndex(r => r.id === selectedRevisionId) if (revisionIndex !== -1) { stage.revisions[revisionIndex] = { ...stage.revisions[revisionIndex], attachments: [...stage.revisions[revisionIndex].attachments, ...newAttachments] } updatedDoc.allStages = stages break } } } return updatedDoc }) setAllData(updatedData) console.log('✅ AttachmentTable update complete') setTimeout(() => { router.refresh() }, 1500) } catch (error) { console.error('❌ AttachmentTable update failed:', error) router.refresh() } }, [selectedRevisionId, allData, setAllData, router]) return (
Attachments {selectedRevisionId && selectedRevisionInfo && ( )}
File Name Actions {!selectedRevisionId || attachments.length === 0 ? (
{!selectedRevisionId ? 'Please select a revision' : 'No attached files'} {selectedRevisionId && selectedRevisionInfo && ( )}
) : ( attachments.map((file) => (
{file.fileName}
{file.fileSize ? file.fileSize >= 1024 * 1024 ? `${(file.fileSize / 1024 / 1024).toFixed(1)}MB` : `${(file.fileSize / 1024).toFixed(1)}KB` : '-'} {file.dolceFilePath && file.dolceFilePath.trim() !== '' && ( Processed )}
)) )}
) } // SubTables 컴포넌트 - 컨테이너 너비 제한 강화 function SubTables() { const router = useRouter() const { selectedDocumentId, selectedRevisionId, setSelectedRevisionId, allData, setAllData } = React.useContext(DocumentSelectionContextAll) // PDF 뷰어 상태 관리 const [viewerOpen, setViewerOpen] = React.useState(false) const [selectedRevision, setSelectedRevision] = React.useState(null) const [instance, setInstance] = React.useState(null) const [viewerLoading, setViewerLoading] = React.useState(true) const [fileSetLoading, setFileSetLoading] = React.useState(true) const viewer = React.useRef(null) const initialized = React.useRef(false) const isCancelled = React.useRef(false) const selectedDocument = React.useMemo(() => { if (!selectedDocumentId || !allData) return null return allData.find((d) => d.documentId === selectedDocumentId) || null }, [selectedDocumentId, allData]) const allRevisions = React.useMemo(() => { if (!selectedDocument?.allStages) return [] const revisions: RevisionInfo[] = [] for (const stage of selectedDocument.allStages as StageInfo[]) { const stageRevisions = stage.revisions.map(revision => ({ ...revision, stageName: stage.stageName })) revisions.push(...stageRevisions) } return revisions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ) }, [selectedDocument]) const selectedRevisionData = React.useMemo(() => { if (!selectedRevisionId) return null return allRevisions.find(r => r.id === selectedRevisionId) || null }, [selectedRevisionId, allRevisions]) const cleanupHtmlStyle = React.useCallback(() => { const htmlElement = window.document.documentElement const originalStyle = htmlElement.getAttribute("style") || "" const colorSchemeStyle = originalStyle .split(";") .map((s) => s.trim()) .find((s) => s.startsWith("color-scheme:")) if (colorSchemeStyle) { htmlElement.setAttribute("style", colorSchemeStyle + ";") } else { htmlElement.removeAttribute("style") } }, []) const handleViewRevision = React.useCallback((revision: RevisionInfo) => { setSelectedRevision(revision) setViewerOpen(true) setViewerLoading(true) setFileSetLoading(true) initialized.current = false }, []) const handleDownloadFile = React.useCallback(async (attachment: AttachmentInfo) => { try { // 파일 경로 처리 let downloadPath = attachment.filePath // 공용 다운로드 함수 사용 (보안 검증, 파일 체크 모두 포함) const result = await downloadFile(downloadPath, attachment.fileName, { action: 'download', showToast: true, showSuccessToast: true, }) if (!result.success) { throw new Error(result.error || 'Download failed') } } catch (error) { console.error('File download error:', error) // fallback: API 엔드포인트를 통한 다운로드 시도 try { const queryParam = attachment.id ? `id=${encodeURIComponent(attachment.id)}` : `path=${encodeURIComponent(attachment.filePath)}` const response = await fetch(`/api/document-download?${queryParam}`) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.error || 'Failed to download file.') } const blob = await response.blob() const url = window.URL.createObjectURL(blob) const link = window.document.createElement('a') link.href = url link.download = attachment.fileName window.document.body.appendChild(link) link.click() window.document.body.removeChild(link) window.URL.revokeObjectURL(url) } catch (fallbackError) { console.error('Fallback download also failed:', fallbackError) alert(`File download failed: ${fallbackError instanceof Error ? fallbackError.message : 'Unknown error'}`) } } }, []) // WebViewer 초기화 React.useEffect(() => { if (viewerOpen && !initialized.current) { initialized.current = true isCancelled.current = false requestAnimationFrame(() => { if (viewer.current && !isCancelled.current) { import("@pdftron/webviewer").then(({ default: WebViewer }) => { if (isCancelled.current) { console.log("WebViewer initialization cancelled (Dialog closed)") return } WebViewer( { path: "/pdftronWeb", licenseKey: "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd", fullAPI: true, css: "/globals.css", }, viewer.current as HTMLDivElement ).then(async (instance: WebViewerInstance) => { if (!isCancelled.current) { setInstance(instance) instance.UI.enableFeatures([instance.UI.Feature.MultiTab]) instance.UI.disableElements(["addTabButton", "multiTabsEmptyPage"]) setViewerLoading(false) } }) }) } }) } return () => { if (instance) { instance.UI.dispose() } setTimeout(() => cleanupHtmlStyle(), 500) } }, [viewerOpen, cleanupHtmlStyle, instance]) // 문서 로드 React.useEffect(() => { const loadDocument = async () => { if (instance && selectedRevision?.attachments?.length) { const { UI } = instance const tabIds = [] for (const attachment of selectedRevision.attachments) { try { const response = await fetch(attachment.filePath) const blob = await response.blob() const options = { filename: attachment.fileName, ...(attachment.fileType?.includes("xlsx") && { officeOptions: { formatOptions: { applyPageBreaksToSheet: true, }, }, }), } const tab = await UI.TabManager.addTab(blob, options) tabIds.push(tab) } catch (error) { console.error("File load failed:", attachment.filePath, error) } } if (tabIds.length > 0) { await UI.TabManager.setActiveTab(tabIds[0]) } setFileSetLoading(false) } } loadDocument() }, [instance, selectedRevision]) const handleCloseViewer = React.useCallback(async () => { if (!fileSetLoading) { isCancelled.current = true if (instance) { try { await instance.UI.dispose() setInstance(null) } catch (e) { console.warn("dispose error", e) } } setViewerLoading(false) setViewerOpen(false) setTimeout(() => cleanupHtmlStyle(), 1000) } }, [fileSetLoading, instance, cleanupHtmlStyle]) if (!selectedDocument) return null return ( <> {/* 컨테이너 너비 제한 강화 */}
{/* 문서 뷰어 다이얼로그 */} Document Preview Revision {selectedRevision?.revision} attachments
{viewerLoading && (

Loading document viewer...

)}
) } /* ------------------------------------------------------------------------------------------------- * High‑level Selected Document Summary * -----------------------------------------------------------------------------------------------*/ function SelectedDocumentInfo() { const { selectedDocumentId, selectedRevisionId, allData } = React.useContext(DocumentSelectionContextAll) if (!selectedDocumentId || !allData) return null const doc = allData.find((d) => d.documentId === selectedDocumentId) if (!doc) return null const totalRevisions = doc.allStages ? (doc.allStages as StageInfo[]).reduce( (acc, s) => acc + s.revisions.length, 0, ) : 0 let selectedRevision: RevisionInfo | null = null if (selectedRevisionId && doc.allStages) { for (const stage of doc.allStages as StageInfo[]) { const rev = stage.revisions.find((r) => r.id === selectedRevisionId) if (rev) { selectedRevision = rev break } } } return (
Document: {doc.docNumber} {doc.title}
Total {totalRevisions} revisions {selectedRevision && ( <> Selected revision: {selectedRevision.revision} ({selectedRevision.attachments.length} files) )}
) } /* ------------------------------------------------------------------------------------------------- * Main Exported Component * -----------------------------------------------------------------------------------------------*/ export function UserVendorALLDocumentDisplay({ allPromises, }: UserVendorDocumentDisplayProps) { const [selectedDocumentId, setSelectedDocumentId] = React.useState(null) const [selectedStageId, setSelectedStageId] = React.useState( null, ) const [selectedRevisionId, setSelectedRevisionId] = React.useState(null) const [allData, setAllData] = React.useState(null) const handleDocumentSelect = React.useCallback((id: number | null) => { setSelectedDocumentId(id) setSelectedStageId(null) setSelectedRevisionId(null) }, []) const ctx = React.useMemo( () => ({ selectedDocumentId, selectedStageId, selectedRevisionId, setSelectedDocumentId: handleDocumentSelect, setSelectedStageId, setSelectedRevisionId, allData, setAllData, }), [ selectedDocumentId, selectedStageId, selectedRevisionId, handleDocumentSelect, allData, setAllData, ], ) if (!allPromises) { return (

Unable to load data.

) } return (
) }