// 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/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 추가 import { EditRevisionDialog } from "./edit-revision-dialog" // ✅ 추가 import { downloadFile } from "@/lib/file-download" // ✅ 공용 다운로드 함수 import import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" /* ------------------------------------------------------------------------------------------------- * 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 serialNo: 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 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, onEditRevision, // ✅ 수정 함수 prop 추가 }: { revisions: RevisionInfo[] onViewRevision: (revision: RevisionInfo) => void onNewRevision: () => void onEditRevision: (revision: RevisionInfo) => void // ✅ 수정 함수 타입 추가 }) { const { selectedRevisionId, setSelectedRevisionId } = React.useContext(DocumentSelectionContext) 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 } // 모든 첨부파일의 dolceFilePath가 null이거나 빈값이어야 수정 가능 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
Select Serial No Revision Category Usage Type Status Uploader Comment Upload Date Files Actions {revisions.map((revision) => { const canEdit = canEditRevision(revision) const processStatus = getRevisionProcessStatus(revision) return ( toggleSelect(revision.id)} className="h-4 w-4 cursor-pointer" /> {revision.serialNo || ''}
{revision.revision} {/* ✅ 처리 상태 인디케이터 */} {processStatus === 'fully-processed' && (
)} {processStatus === 'partially-processed' && (
)}
{revision.uploaderType === "vendor" ? "To SHI" : "From SHI"} {revision.usage || '-'} {revision.usageType ? ( revision.usageType ) : ( - )} {revision.revisionStatus} {revision.uploaderName || '-'} {revision.comment ? (

{revision.comment}

) : ( - )}
{revision.uploadedAt ? new Date(revision.uploadedAt).toLocaleDateString() : '-'}
{revision.attachments.length} {/* ✅ 처리된 파일 수 표시 */} {processStatus === 'partially-processed' && ( ({revision.attachments.filter(att => att.dolceFilePath && att.dolceFilePath.trim() !== '' ).length} processed) )}
{/* 보기 버튼 */} {revision.attachments.length > 0 && ( )} {/* ✅ 수정 버튼 */}
) })}
) } function AttachmentTable({ attachments, onDownloadFile, onDeleteFile, }: { attachments: AttachmentInfo[] onDownloadFile: (attachment: AttachmentInfo) => void onDeleteFile: (attachment: AttachmentInfo) => Promise }) { const { selectedRevisionId, allData, setAllData } = React.useContext(DocumentSelectionContext) const [addAttachmentDialogOpen, setAddAttachmentDialogOpen] = React.useState(false) const [deletingFileId, setDeletingFileId] = React.useState(null) const router = useRouter() // ✅ AlertDialog 상태 추가 const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false) const [fileToDelete, setFileToDelete] = React.useState(null) const [errorAlertOpen, setErrorAlertOpen] = React.useState(false) const [errorMessage, setErrorMessage] = React.useState('') // 선택된 리비전 정보 가져오기 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) => { // rejected 상태의 리비전에 속한 첨부파일은 무조건 삭제 가능 if (selectedRevisionInfo && selectedRevisionInfo.revisionStatus && selectedRevisionInfo.revisionStatus.toLowerCase() === 'rejected') { return true } // 그 외의 경우는 기존 로직대로: dolceFilePath가 없거나 빈값인 경우만 삭제 가능 return !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' }, [selectedRevisionInfo]) // ✅ 삭제 요청 핸들러 (확인 다이얼로그 표시) const handleDeleteRequest = React.useCallback((attachment: AttachmentInfo) => { if (!canDeleteFile(attachment)) { setErrorMessage('This file cannot be deleted because it has been processed by the system.') setErrorAlertOpen(true) return } setFileToDelete(attachment) setDeleteConfirmOpen(true) }, [canDeleteFile]) // ✅ 실제 삭제 수행 핸들러 const handleConfirmDelete = React.useCallback(async () => { if (!fileToDelete) return try { setDeletingFileId(fileToDelete.id) setDeleteConfirmOpen(false) await onDeleteFile(fileToDelete) } catch (error) { console.error('Delete file error:', error) setErrorMessage(`Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`) setErrorAlertOpen(true) } finally { setDeletingFileId(null) setFileToDelete(null) } }, [fileToDelete, onDeleteFile]) // 첨부파일 업로드 성공 핸들러 (기존 코드 유지) 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, dolceFilePath: null, 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` : '-'}
{file.dolceFilePath && file.dolceFilePath.trim() !== '' && (
Processed
)}
)) )}
{/* ✅ 삭제 확인 다이얼로그 */} Delete File Are you sure you want to delete "{fileToDelete?.fileName}"? This action cannot be undone. setFileToDelete(null)}> Cancel Delete {/* ✅ 에러 메시지 다이얼로그 */} Error {errorMessage} setErrorMessage('')}> OK {/* AddAttachmentDialog */} {selectedRevisionInfo && ( )} ) } // SubTables 컴포넌트 - 중복 정의 제거 및 통합 function SubTables() { const router = useRouter() const { selectedDocumentId, selectedRevisionId, setSelectedRevisionId, allData, 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 [editRevisionDialogOpen, setEditRevisionDialogOpen] = React.useState(false) const [editingRevision, setEditingRevision] = React.useState(null) const handleNewRevision = React.useCallback(() => { setNewRevisionDialogOpen(true) }, []) // ✅ 리비전 수정 핸들러 const handleEditRevision = React.useCallback((revision: RevisionInfo) => { setEditingRevision(revision) setEditRevisionDialogOpen(true) }, []) // ✅ 리비전 수정 성공 핸들러 const handleRevisionEditSuccess = React.useCallback((action: 'update' | 'delete', result?: any) => { if (!allData || !editingRevision) { // fallback: 전체 새로고침 setTimeout(() => router.refresh(), 500) return } try { if (action === 'delete') { // 리비전 삭제: 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 === editingRevision.id) if (revisionIndex !== -1) { // 해당 리비전 제거 stage.revisions.splice(revisionIndex, 1) updatedDoc.allStages = stages break } } } return updatedDoc }) setAllData(updatedData) // 삭제된 리비전이 선택되어 있었으면 선택 해제 if (selectedRevisionId === editingRevision.id) { setSelectedRevisionId(null) } console.log('✅ Revision deleted and state updated') } else if (action === 'update') { // 리비전 업데이트: 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 === editingRevision.id) if (revisionIndex !== -1) { // 해당 리비전 업데이트 stage.revisions[revisionIndex] = { ...stage.revisions[revisionIndex], comment: result?.updatedRevision?.comment || stage.revisions[revisionIndex].comment, usage: result?.updatedRevision?.usage || stage.revisions[revisionIndex].usage, usageType: result?.updatedRevision?.usageType || stage.revisions[revisionIndex].usageType, updatedAt: new Date(), } updatedDoc.allStages = stages break } } } return updatedDoc }) setAllData(updatedData) console.log('✅ Revision updated and state updated') } // 약간의 지연 후 서버 데이터 새로고침 setTimeout(() => { router.refresh() }, 1000) } catch (error) { console.error('❌ Revision edit state update failed:', error) // 실패 시 전체 새로고침 setTimeout(() => router.refresh(), 500) } }, [allData, editingRevision, setAllData, selectedRevisionId, setSelectedRevisionId, router]) // ✅ 파일 다운로드 함수 - 공용함수 사용 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'}`) } } }, []) // 파일 삭제 함수 const handleDeleteFile = React.useCallback(async (attachment: AttachmentInfo) => { try { const response = await fetch(`/api/attachment-delete`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ attachmentId: attachment.id, revisionId: attachment.revisionId, }), }) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.error || 'Failed to delete file.') } // 성공시 로컬 상태 업데이트 if (allData && selectedRevisionId) { 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.filter( att => att.id !== attachment.id ) } updatedDoc.allStages = stages break } } } return updatedDoc }) setAllData(updatedData) console.log('✅ File deleted and state updated') } // 약간의 지연 후 서버 데이터 새로고침 setTimeout(() => { router.refresh() }, 1000) } catch (error) { console.error('Delete file error:', error) throw error // AttachmentTable에서 에러 핸들링 } }, [allData, selectedRevisionId, setAllData, router]) 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, 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, dolceFilePath: null, 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 } } 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, router]) 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 }, []) // 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(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 (
) }