summaryrefslogtreecommitdiff
path: root/components/ship-vendor-document-all
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
commit02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch)
treee932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /components/ship-vendor-document-all
parentd78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff)
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'components/ship-vendor-document-all')
-rw-r--r--components/ship-vendor-document-all/user-vendor-document-table-container.tsx898
1 files changed, 898 insertions, 0 deletions
diff --git a/components/ship-vendor-document-all/user-vendor-document-table-container.tsx b/components/ship-vendor-document-all/user-vendor-document-table-container.tsx
new file mode 100644
index 00000000..157bdb03
--- /dev/null
+++ b/components/ship-vendor-document-all/user-vendor-document-table-container.tsx
@@ -0,0 +1,898 @@
+// 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'
+
+/* -------------------------------------------------------------------------------------------------
+ * Types & Constants
+ * -----------------------------------------------------------------------------------------------*/
+interface UserVendorDocumentDisplayProps {
+ allPromises: Promise<[
+ Awaited<ReturnType<typeof getUserVendorDocumentsAll>>, // 문서 목록
+ Awaited<ReturnType<typeof getUserVendorDocumentStatsAll>>, // 통계 데이터
+ ]>
+}
+
+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<DocumentSelectionContextType>(
+ {
+ 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<string, string> = {
+ '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 (
+ <Card className="flex-1 min-w-0 max-w-full">
+ <CardHeader className="pb-3">
+ <CardTitle className="text-lg">Revisions</CardTitle>
+ </CardHeader>
+ <CardContent className="p-2 overflow-hidden">
+ <div className="w-full overflow-x-auto">
+ <Table className="w-full" style={{ tableLayout: 'fixed', minWidth: '800px' }}>
+ <TableHeader>
+ <TableRow>
+ <TableHead style={{ width: '40px' }}>Sel</TableHead>
+ <TableHead style={{ width: '60px' }}>Serial No</TableHead>
+ <TableHead style={{ width: '80px' }}>Rev</TableHead>
+ <TableHead style={{ width: '60px' }}>Category</TableHead>
+ <TableHead style={{ width: '80px' }}>Usage</TableHead>
+ <TableHead style={{ width: '80px' }}>Type</TableHead>
+ <TableHead style={{ width: '90px' }}>Status</TableHead>
+ <TableHead style={{ width: '100px' }}>Uploader</TableHead>
+ <TableHead style={{ width: '120px' }}>Comment</TableHead>
+ <TableHead style={{ width: '100px' }}>Date</TableHead>
+ <TableHead style={{ width: '60px' }} className="text-center">Files</TableHead>
+ <TableHead style={{ width: '80px' }}>Actions</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {revisions.map((revision) => {
+ const canEdit = canEditRevision(revision)
+ const processStatus = getRevisionProcessStatus(revision)
+
+ return (
+ <TableRow
+ key={revision.id}
+ className={`revision-table-row ${selectedRevisionId === revision.id ? 'selected' : ''}`}
+ >
+ <TableCell style={{ width: '40px' }}>
+ <input
+ type="checkbox"
+ checked={selectedRevisionId === revision.id}
+ onChange={() => toggleSelect(revision.id)}
+ className="h-3 w-3 cursor-pointer"
+ />
+ </TableCell>
+ <TableCell style={{ width: '60px' }} className="font-mono font-medium">
+ {revision.serialNo || ''}
+ </TableCell>
+ <TableCell style={{ width: '80px' }} className="font-mono font-medium">
+ <div className="flex items-center gap-1">
+ <span className="truncate text-xs">{revision.revision}</span>
+ {processStatus === 'fully-processed' && (
+ <div
+ className="w-1.5 h-1.5 bg-blue-500 rounded-full flex-shrink-0"
+ title="All files processed"
+ />
+ )}
+ {processStatus === 'partially-processed' && (
+ <div
+ className="w-1.5 h-1.5 bg-yellow-500 rounded-full flex-shrink-0"
+ title="Some files processed"
+ />
+ )}
+ </div>
+ </TableCell>
+ <TableCell style={{ width: '60px' }} className="text-xs">
+ {revision.uploaderType === "vendor" ? "To" : "From"}
+ </TableCell>
+ <TableCell style={{ width: '80px' }}>
+ <span className="text-xs truncate block">
+ {revision.usage || '-'}
+ </span>
+ </TableCell>
+ <TableCell style={{ width: '80px' }}>
+ <span className="text-xs truncate block">
+ {revision.usageType ? (
+ getUsageTypeDisplay(revision.usageType)
+ ) : (
+ <span className="text-gray-400">-</span>
+ )}
+ </span>
+ </TableCell>
+ <TableCell style={{ width: '90px' }}>
+ <Badge
+ variant={
+ revision.revisionStatus === 'APPROVED'
+ ? 'default'
+ : 'secondary'
+ }
+ className="text-xs truncate max-w-full"
+ >
+ {revision.revisionStatus.slice(0, 8)}
+ </Badge>
+ </TableCell>
+ <TableCell style={{ width: '100px' }}>
+ <span className="text-xs truncate block">{revision.uploaderName || '-'}</span>
+ </TableCell>
+ <TableCell style={{ width: '120px' }}>
+ {revision.comment ? (
+ <div className="w-full">
+ <p className="text-xs text-gray-700 bg-gray-50 p-1 rounded truncate" title={revision.comment}>
+ {revision.comment}
+ </p>
+ </div>
+ ) : (
+ <span className="text-gray-400 text-xs">-</span>
+ )}
+ </TableCell>
+ <TableCell style={{ width: '100px' }}>
+ <span className="text-xs truncate block">
+ {revision.uploadedAt
+ ? new Date(revision.uploadedAt).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric'
+ })
+ : '-'}
+ </span>
+ </TableCell>
+ <TableCell style={{ width: '60px' }} className="text-center">
+ <div className="flex items-center justify-center">
+ <span className="text-xs">{revision.attachments.length}</span>
+ </div>
+ </TableCell>
+ <TableCell style={{ width: '80px' }}>
+ <div className="flex items-center justify-center">
+ {revision.attachments.length > 0 && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => onViewRevision(revision)}
+ className="h-6 w-6 p-0"
+ title="View attachments"
+ >
+ <Eye className="h-3 w-3" />
+ </Button>
+ )}
+ </div>
+ </TableCell>
+ </TableRow>
+ )
+ })}
+ </TableBody>
+ </Table>
+ </div>
+ </CardContent>
+ </Card>
+ )
+}
+
+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 (
+ <Card className="w-72 flex-shrink-0 max-w-full min-w-0">
+ <CardHeader className="pb-3">
+ <div className="flex items-center justify-between">
+ <CardTitle className="text-lg truncate">Attachments</CardTitle>
+ {selectedRevisionId && selectedRevisionInfo && (
+ <Button
+ onClick={handleAddAttachment}
+ size="sm"
+ variant="outline"
+ className="flex items-center gap-1 h-7 px-2 flex-shrink-0"
+ >
+ <Plus className="h-3 w-3" />
+ <span className="text-xs">Add</span>
+ </Button>
+ )}
+ </div>
+ </CardHeader>
+ <CardContent className="p-2 overflow-hidden">
+ <div className="w-full overflow-x-auto">
+ <Table className="w-full" style={{ tableLayout: 'fixed', minWidth: '280px' }}>
+ <TableHeader>
+ <TableRow>
+ <TableHead style={{ width: '200px' }}>File Name</TableHead>
+ <TableHead style={{ width: '80px' }}>Actions</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {!selectedRevisionId || attachments.length === 0 ? (
+ <TableRow>
+ <TableCell colSpan={2} className="h-24 text-center">
+ <div className="flex flex-col items-center gap-2 text-muted-foreground">
+ <FileText className="h-6 w-6" />
+ <span className="text-xs">
+ {!selectedRevisionId
+ ? 'Please select a revision'
+ : 'No attached files'}
+ </span>
+ {selectedRevisionId && selectedRevisionInfo && (
+ <Button
+ onClick={handleAddAttachment}
+ size="sm"
+ variant="outline"
+ className="mt-2 h-7 px-2"
+ >
+ <Plus className="h-3 w-3 mr-1" />
+ <span className="text-xs">Add First File</span>
+ </Button>
+ )}
+ </div>
+ </TableCell>
+ </TableRow>
+ ) : (
+ attachments.map((file) => (
+ <TableRow key={file.id}>
+ <TableCell style={{ width: '200px' }} className="font-medium">
+ <div className="min-w-0">
+ <div className="truncate text-xs" title={file.fileName}>
+ {file.fileName}
+ </div>
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
+ <span>
+ {file.fileSize
+ ? file.fileSize >= 1024 * 1024
+ ? `${(file.fileSize / 1024 / 1024).toFixed(1)}MB`
+ : `${(file.fileSize / 1024).toFixed(1)}KB`
+ : '-'}
+ </span>
+ {file.dolceFilePath && file.dolceFilePath.trim() !== '' && (
+ <span className="text-blue-600 font-medium">Processed</span>
+ )}
+ </div>
+ </div>
+ </TableCell>
+ <TableCell style={{ width: '80px' }}>
+ <div className="flex items-center justify-center">
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => onDownloadFile(file)}
+ className="h-6 w-6 p-0"
+ title="Download file"
+ >
+ <Download className="h-3 w-3" />
+ </Button>
+ </div>
+ </TableCell>
+ </TableRow>
+ ))
+ )}
+ </TableBody>
+ </Table>
+ </div>
+ </CardContent>
+ </Card>
+ )
+}
+
+// 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<RevisionInfo | null>(null)
+ const [instance, setInstance] = React.useState<WebViewerInstance | null>(null)
+ const [viewerLoading, setViewerLoading] = React.useState(true)
+ const [fileSetLoading, setFileSetLoading] = React.useState(true)
+ const viewer = React.useRef<HTMLDivElement>(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 {
+ 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, 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 (
+ <>
+ {/* 컨테이너 너비 제한 강화 */}
+ <div className="w-full max-w-full overflow-hidden">
+ <div className="flex flex-col lg:flex-row gap-4 min-w-0">
+ <RevisionTable
+ revisions={allRevisions}
+ onViewRevision={handleViewRevision}
+ />
+ <AttachmentTable
+ attachments={selectedRevisionData?.attachments || []}
+ onDownloadFile={handleDownloadFile}
+ />
+ </div>
+ </div>
+
+ {/* 문서 뷰어 다이얼로그 */}
+ <Dialog open={viewerOpen} onOpenChange={handleCloseViewer}>
+ <DialogContent className="w-[90vw] h-[90vh]" style={{ maxWidth: "none" }}>
+ <DialogHeader className="h-[38px]">
+ <DialogTitle>Document Preview</DialogTitle>
+ <DialogDescription>
+ Revision {selectedRevision?.revision} attachments
+ </DialogDescription>
+ </DialogHeader>
+ <div
+ ref={viewer}
+ style={{ height: "calc(90vh - 20px - 38px - 1rem - 48px)" }}
+ >
+ {viewerLoading && (
+ <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">
+ Loading document viewer...
+ </p>
+ </div>
+ )}
+ </div>
+ </DialogContent>
+ </Dialog>
+ </>
+ )
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * 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 (
+ <div className="w-full max-w-full overflow-hidden">
+ <div className="flex flex-wrap items-center gap-3 rounded-lg bg-gray-50 p-4">
+ <div className="flex items-center gap-2 min-w-0">
+ <Badge variant="secondary" className="text-sm flex-shrink-0">
+ Document: {doc.docNumber}
+ </Badge>
+ <span className="max-w-[300px] truncate text-sm font-medium text-gray-700">
+ {doc.title}
+ </span>
+ </div>
+ <div className="flex items-center gap-2 text-sm text-gray-600">
+ <span>•</span>
+ <span>Total {totalRevisions} revisions</span>
+ {selectedRevision && (
+ <>
+ <span>•</span>
+ <Badge variant="outline" className="text-sm">
+ Selected revision: {selectedRevision.revision}
+ </Badge>
+ <span>({selectedRevision.attachments.length} files)</span>
+ </>
+ )}
+ </div>
+ </div>
+ </div>
+ )
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * Main Exported Component
+ * -----------------------------------------------------------------------------------------------*/
+export function UserVendorALLDocumentDisplay({
+ allPromises,
+}: UserVendorDocumentDisplayProps) {
+ const [selectedDocumentId, setSelectedDocumentId] =
+ React.useState<number | null>(null)
+ const [selectedStageId, setSelectedStageId] = React.useState<number | null>(
+ null,
+ )
+ const [selectedRevisionId, setSelectedRevisionId] =
+ React.useState<number | null>(null)
+ const [allData, setAllData] =
+ React.useState<SimplifiedDocumentsView[] | null>(null)
+
+ const handleDocumentSelect = React.useCallback((id: number | null) => {
+ setSelectedDocumentId(id)
+ setSelectedStageId(null)
+ setSelectedRevisionId(null)
+ }, [])
+
+ const ctx = React.useMemo<DocumentSelectionContextType>(
+ () => ({
+ selectedDocumentId,
+ selectedStageId,
+ selectedRevisionId,
+ setSelectedDocumentId: handleDocumentSelect,
+ setSelectedStageId,
+ setSelectedRevisionId,
+ allData,
+ setAllData,
+ }),
+ [
+ selectedDocumentId,
+ selectedStageId,
+ selectedRevisionId,
+ handleDocumentSelect,
+ allData,
+ setAllData,
+ ],
+ )
+
+ if (!allPromises) {
+ return (
+ <Card>
+ <CardContent className="flex items-center justify-center py-8">
+ <div className="text-center">
+ <AlertCircle className="mx-auto mb-2 h-8 w-8 text-gray-400" />
+ <p className="text-gray-600">Unable to load data.</p>
+ </div>
+ </CardContent>
+ </Card>
+ )
+ }
+
+ return (
+ <DocumentSelectionContextAll.Provider value={ctx}>
+ <div className="space-y-4 w-full max-w-full overflow-hidden">
+ <Card className="w-full max-w-full">
+ <CardContent className="p-4 overflow-hidden">
+ <div className="w-full max-w-full">
+ <SimplifiedDocumentsTable
+ allPromises={allPromises}
+ onDataLoaded={setAllData}
+ onDocumentSelect={handleDocumentSelect}
+ />
+ </div>
+ </CardContent>
+ </Card>
+ <SelectedDocumentInfo />
+ <SubTables />
+ </div>
+ </DocumentSelectionContextAll.Provider>
+ )
+} \ No newline at end of file