// 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 } from "lucide-react" import { SimplifiedDocumentsTable } from "@/lib/vendor-document-list/ship/enhanced-documents-table" import { getUserVendorDocuments, getUserVendorDocumentStats, } from "@/lib/vendor-document-list/enhanced-document-service" import { SimplifiedDocumentsView } from "@/db/schema" import { WebViewerInstance } from "@pdftron/webviewer" import { NewRevisionDialog } from "./new-revision-dialog" import { useRouter } from 'next/navigation' import { AddAttachmentDialog } from "./add-attachment-dialog" // ✅ import 추가 /* ------------------------------------------------------------------------------------------------- * 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 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 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 DocumentSelectionContext = 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 * -----------------------------------------------------------------------------------------------*/ // user-vendor-document-display.tsx의 RevisionTable 컴포넌트 수정 // B3 용도 타입 축약 표시 함수 추가 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, onNewRevision }: { revisions: RevisionInfo[] onViewRevision: (revision: RevisionInfo) => void onNewRevision: () => void }) { const { selectedRevisionId, setSelectedRevisionId } = React.useContext(DocumentSelectionContext) const toggleSelect = (revisionId: number) => { setSelectedRevisionId(revisionId === selectedRevisionId ? null : revisionId) } return (
Revisions
Select Revision Category Usage Type {/* ✅ usageType 컬럼 */} Status Uploader Comment Upload Date Files Actions {revisions.map((revision) => ( toggleSelect(revision.id)} className="h-4 w-4 cursor-pointer" /> {revision.revision} {revision.uploaderType === "vendor" ? "To SHI" : "From SHI"} {revision.usage || '-'} {/* ✅ usageType 표시 */} {revision.usageType ? revision.usageType : ( - )} {revision.revisionStatus} {revision.uploaderName || '-'} {revision.comment ? (

{revision.comment}

) : ( - )}
{revision.uploadedAt ? new Date(revision.uploadedAt).toLocaleDateString() : '-'} {revision.attachments.length} {revision.attachments.length > 0 && ( )}
))}
) } function AttachmentTable({ attachments, onDownloadFile }: { attachments: AttachmentInfo[] onDownloadFile: (attachment: AttachmentInfo) => void }) { const { selectedRevisionId, allData, setAllData } = React.useContext(DocumentSelectionContext) 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 handleAttachmentUploadSuccess = React.useCallback((uploadResult?: any) => { if (!selectedRevisionId || !allData || !uploadResult?.data) { console.log('🔄 Full refresh') router.refresh() return } try { // 새로운 첨부파일들을 AttachmentInfo 형태로 변환 const newAttachments: AttachmentInfo[] = uploadResult.data.uploadedFiles?.map((file: any) => ({ id: file.id, revisionId: selectedRevisionId, fileName: file.fileName, filePath: file.filePath, fileSize: file.fileSize, fileType: file.fileType || null, createdAt: new Date(), updatedAt: new Date(), })) || [] // allData에서 해당 리비전을 찾아서 첨부파일 추가 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` : '-'}
)) )}
{/* ✅ AddAttachmentDialog 추가 */} {selectedRevisionInfo && ( )} ) } /* ------------------------------------------------------------------------------------------------- * Derived Sub Tables Wrapper * -----------------------------------------------------------------------------------------------*/ function SubTables() { const router = useRouter() const { selectedDocumentId, selectedRevisionId, allData, setAllData } = // ✅ setAllData 추가 React.useContext(DocumentSelectionContext) // 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 [newRevisionDialogOpen, setNewRevisionDialogOpen] = React.useState(false) const handleNewRevision = React.useCallback(() => { setNewRevisionDialogOpen(true) }, []) const handleRevisionUploadSuccess = React.useCallback(async (uploadResult?: any) => { if (!selectedDocumentId || !allData || !uploadResult?.data) { // fallback: 전체 새로고침 window.location.reload() return } try { // 새로 업로드된 리비전 정보 구성 const newRevision: RevisionInfo = { id: uploadResult.data.revisionId, issueStageId: uploadResult.data.issueStageId, revision: uploadResult.data.revision, uploaderType: "vendor", uploaderId: null, uploaderName: uploadResult.data.uploaderName || null, comment: uploadResult.data.comment || null, // ✅ comment도 포함 usage: uploadResult.data.usage, usageType: uploadResult.data.usageType || null, revisionStatus: "UPLOADED", submittedDate: null, approvedDate: null, uploadedAt: new Date().toISOString().slice(0, 10), reviewStartDate: null, rejectedDate: null, reviewerId: null, reviewerName: null, reviewComments: null, createdAt: new Date(), updatedAt: new Date(), stageName: uploadResult.data.stage, attachments: uploadResult.data.uploadedFiles?.map((file: any) => ({ id: file.id, revisionId: uploadResult.data.revisionId, fileName: file.fileName, filePath: file.filePath, fileSize: file.fileSize, fileType: file.fileType || null, createdAt: new Date(), updatedAt: new Date(), })) || [] } // allData에서 해당 문서 찾아서 업데이트 const updatedData = allData.map(doc => { if (doc.documentId === selectedDocumentId) { const updatedDoc = { ...doc } // allStages가 있으면 해당 stage에 새 revision 추가 if (updatedDoc.allStages) { const stages = [...updatedDoc.allStages as StageInfo[]] // ✅ 배열 복사 const targetStage = stages.find(stage => stage.stageName === uploadResult.data.stage || stage.stageName === uploadResult.data.usage ) if (targetStage) { // 기존 revision과 중복 체크 (같은 revision, usage, usageType) const isDuplicate = targetStage.revisions.some(rev => rev.revision === newRevision.revision && rev.usage === newRevision.usage && rev.usageType === newRevision.usageType ) if (!isDuplicate) { targetStage.revisions = [newRevision, ...targetStage.revisions] updatedDoc.allStages = stages // ✅ 업데이트된 stages 할당 } } else { // 첫 번째 stage에 추가 (fallback) if (stages.length > 0) { stages[0].revisions = [newRevision, ...stages[0].revisions] updatedDoc.allStages = stages } } } return updatedDoc } return doc }) // State 업데이트 setAllData(updatedData) console.log('✅ RevisionTable data update complete') } catch (error) { console.error('❌ RevisionTable update failed:', error) // 실패 시 전체 새로고침 window.location.reload() } setTimeout(() => { router.refresh() // 서버 컴포넌트 재렌더링으로 최신 데이터 가져오기 }, 1500) // 1.5초 후 새로고침 (사용자가 업데이트를 확인할 시간) }, [selectedDocumentId, allData, setAllData]) 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]) // PDF 뷰어 정리 함수 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 { 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 (error) { console.error('File download error:', error) alert(`File download failed: ${error instanceof Error ? error.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]) // 문서 로드 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(DocumentSelectionContext) 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 UserVendorDocumentDisplay({ allPromises, }: UserVendorDocumentDisplayProps) { /** * Selection state */ 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 (
) }