summaryrefslogtreecommitdiff
path: root/components/ship-vendor-document/user-vendor-document-table-container.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-13 07:11:18 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-13 07:11:18 +0000
commit0fddf148402fd6b99a1b3800d73679899bcb2ed3 (patch)
treeeb51c02e6fa6037ddcc38a3b57d10d8c739125cf /components/ship-vendor-document/user-vendor-document-table-container.tsx
parentc72d0897f7b37843109c86f61d97eba05ba3ca0d (diff)
(대표님) 20250613 16시 10분 global css, b-rfq, document 등
Diffstat (limited to 'components/ship-vendor-document/user-vendor-document-table-container.tsx')
-rw-r--r--components/ship-vendor-document/user-vendor-document-table-container.tsx1037
1 files changed, 948 insertions, 89 deletions
diff --git a/components/ship-vendor-document/user-vendor-document-table-container.tsx b/components/ship-vendor-document/user-vendor-document-table-container.tsx
index 0ede3e19..17af5436 100644
--- a/components/ship-vendor-document/user-vendor-document-table-container.tsx
+++ b/components/ship-vendor-document/user-vendor-document-table-container.tsx
@@ -1,129 +1,988 @@
+// user-vendor-document-display.tsx
"use client"
import React from "react"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
-import { Building, FileText, AlertCircle } from "lucide-react"
+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 {
+ 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<ReturnType<typeof getUserVendorDocuments>>,
- Awaited<ReturnType<typeof getUserVendorDocumentStats>>
+ Awaited<ReturnType<typeof getUserVendorDocuments>>, // 문서 목록
+ Awaited<ReturnType<typeof getUserVendorDocumentStats>>, // 통계 데이터
]>
}
-// DrawingKind별 설명 매핑
-const DRAWING_KIND_INFO = {
- B3: {
- title: "B3 승인 도면",
- description: "Approval → Work 단계로 진행되는 승인 중심 도면",
- color: "bg-blue-50 text-blue-700 border-blue-200"
- },
- B4: {
- title: "B4 작업 도면",
- description: "Pre → Work 단계로 진행되는 DOLCE 연동 도면",
- color: "bg-green-50 text-green-700 border-green-200"
+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<DocumentSelectionContextType>(
+ {
+ selectedDocumentId: null,
+ selectedStageId: null,
+ selectedRevisionId: null,
+ setSelectedDocumentId: (_id: number | null) => { },
+ setSelectedStageId: (_id: number | null) => { },
+ setSelectedRevisionId: (_id: number | null) => { },
+ allData: null,
+ setAllData: (_data: SimplifiedDocumentsView[]) => { }, // ✅ 추가
},
- B5: {
- title: "B5 단계 도면",
- description: "First → Second 단계로 진행되는 순차적 도면",
- color: "bg-purple-50 text-purple-700 border-purple-200"
+)
+
+/* -------------------------------------------------------------------------------------------------
+ * Revision & Attachment Tables
+ * -----------------------------------------------------------------------------------------------*/
+// user-vendor-document-display.tsx의 RevisionTable 컴포넌트 수정
+// B3 용도 타입 축약 표시 함수 추가
+
+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',
}
-} as const
+
+ return abbreviations[usageType] || usageType
+}
-export function UserVendorDocumentDisplay({
- allPromises
-}: UserVendorDocumentDisplayProps) {
- // allPromises가 제대로 전달되었는지 확인
- if (!allPromises) {
- return (
- <Card>
- <CardContent className="flex items-center justify-center py-8">
- <div className="text-center">
- <AlertCircle className="w-8 h-8 text-gray-400 mx-auto mb-2" />
- <p className="text-gray-600">데이터를 불러올 수 없습니다.</p>
+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 (
+ <Card className="flex-1">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle className="text-lg">리비전</CardTitle>
+ </div>
+ <Button
+ onClick={onNewRevision}
+ size="sm"
+ className="flex items-center gap-2"
+ >
+ <Plus className="h-4 w-4" />
+ 새 리비전
+ </Button>
+ </div>
+ </CardHeader>
+ <CardContent>
+ <div className="overflow-x-auto">
+ <Table className="tbl-compact">
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-12">선택</TableHead>
+ <TableHead>리비전</TableHead>
+ <TableHead>카테고리</TableHead>
+ <TableHead>용도</TableHead>
+ <TableHead>타입</TableHead> {/* ✅ usageType 컬럼 */}
+ <TableHead>상태</TableHead>
+ <TableHead>업로더</TableHead>
+ <TableHead>코멘트</TableHead>
+ <TableHead>업로드일</TableHead>
+ <TableHead className="text-center">파일 수</TableHead>
+ <TableHead>액션</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {revisions.map((revision) => (
+ <TableRow
+ key={revision.id}
+ className={`revision-table-row ${
+ selectedRevisionId === revision.id ? 'selected' : ''
+ }`}
+ >
+ <TableCell>
+ <input
+ type="checkbox"
+ checked={selectedRevisionId === revision.id}
+ onChange={() => toggleSelect(revision.id)}
+ className="h-4 w-4 cursor-pointer"
+ />
+ </TableCell>
+ <TableCell className="font-mono font-medium">
+ {revision.revision}
+ </TableCell>
+ <TableCell className="text-sm">
+ {revision.uploaderType === "vendor" ? "To SHI" : "From SHI"}
+ </TableCell>
+ <TableCell>
+ <span className="text-sm">
+ {revision.usage || '-'}
+ </span>
+ </TableCell>
+ {/* ✅ usageType 표시 */}
+ <TableCell>
+ <span className="text-sm">
+ {revision.usageType ?
+
+ revision.usageType
+
+ : (
+ <span className="text-gray-400 text-xs">-</span>
+ )}
+ </span>
+ </TableCell>
+ <TableCell>
+ <Badge
+ variant={
+ revision.revisionStatus === 'APPROVED'
+ ? 'default'
+ : 'secondary'
+ }
+ className="text-xs"
+ >
+ {revision.revisionStatus}
+ </Badge>
+ </TableCell>
+ <TableCell>
+ <span className="text-sm">{revision.uploaderName || '-'}</span>
+ </TableCell>
+ <TableCell className="py-1 px-2">
+ {revision.comment ? (
+ <div className="max-w-24">
+ <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>
+ <span className="text-sm">
+ {revision.uploadedAt
+ ? new Date(revision.uploadedAt).toLocaleDateString()
+ : '-'}
+ </span>
+ </TableCell>
+ <TableCell className="text-center">
+ {revision.attachments.length}
+ </TableCell>
+ <TableCell>
+ {revision.attachments.length > 0 && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => onViewRevision(revision)}
+ className="h-8 px-2"
+ >
+ <Eye className="h-4 w-4" />
+ </Button>
+ )}
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </div>
+ </CardContent>
+ </Card>
+ )
+}
+
+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('🔄 전체 새로고침')
+ 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 업데이트 완료')
+
+ // 메인 테이블도 업데이트 (약간의 지연 후)
+ setTimeout(() => {
+ router.refresh()
+ }, 1500)
+
+ } catch (error) {
+ console.error('❌ AttachmentTable 업데이트 실패:', error)
+ router.refresh()
+ }
+ }, [selectedRevisionId, allData, setAllData, router])
+
+ return (
+ <>
+ <Card className="w-96 flex-shrink-0">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <CardTitle className="text-lg">첨부파일</CardTitle>
+ {/* ✅ + 버튼 추가 */}
+ {selectedRevisionId && selectedRevisionInfo && (
+ <Button
+ onClick={handleAddAttachment}
+ size="sm"
+ variant="outline"
+ className="flex items-center gap-2"
+ >
+ <Plus className="h-4 w-4" />
+ 추가
+ </Button>
+ )}
</div>
+ </CardHeader>
+ <CardContent>
+ <Table className="tbl-compact">
+ <TableHeader>
+ <TableRow>
+ <TableHead>파일명</TableHead>
+ <TableHead>액션</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-8 w-8" />
+ <span>
+ {!selectedRevisionId
+ ? '리비전을 선택해주세요'
+ : '첨부된 파일이 없습니다'}
+ </span>
+ {/* ✅ 리비전이 선택된 경우 추가 버튼 표시 */}
+ {selectedRevisionId && selectedRevisionInfo && (
+ <Button
+ onClick={handleAddAttachment}
+ size="sm"
+ variant="outline"
+ className="mt-2"
+ >
+ <Plus className="h-4 w-4 mr-2" />
+ 첫 번째 파일 추가
+ </Button>
+ )}
+ </div>
+ </TableCell>
+ </TableRow>
+ ) : (
+ attachments.map((file) => (
+ <TableRow key={file.id}>
+ <TableCell className="font-medium">
+ <div>
+ <div className="truncate max-w-[180px]" title={file.fileName}>
+ {file.fileName}
+ </div>
+ <div className="text-xs text-muted-foreground">
+ {file.fileSize
+ ? file.fileSize >= 1024 * 1024
+ ? `${(file.fileSize / 1024 / 1024).toFixed(1)}MB`
+ : `${(file.fileSize / 1024).toFixed(1)}KB`
+ : '-'}
+ </div>
+ </div>
+ </TableCell>
+ <TableCell>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => onDownloadFile(file)}
+ className="h-8 px-2"
+ >
+ <Download className="h-4 w-4" />
+ </Button>
+ </TableCell>
+ </TableRow>
+ ))
+ )}
+ </TableBody>
+ </Table>
</CardContent>
</Card>
+
+ {/* ✅ AddAttachmentDialog 추가 */}
+ {selectedRevisionInfo && (
+ <AddAttachmentDialog
+ open={addAttachmentDialogOpen}
+ onOpenChange={setAddAttachmentDialogOpen}
+ revisionId={selectedRevisionId!}
+ revisionName={selectedRevisionInfo.revision}
+ onSuccess={handleAttachmentUploadSuccess}
+ />
+ )}
+ </>
+ )
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * 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<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 [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 데이터 업데이트 완료')
+
+ } catch (error) {
+ console.error('❌ RevisionTable 업데이트 실패:', 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])
- // Promise.all로 감싸진 Promise를 사용해서 데이터 가져오기
- const [documentResult, statsResult] = React.use(allPromises)
+ const selectedRevisionData = React.useMemo(() => {
+ if (!selectedRevisionId) return null
+ return allRevisions.find(r => r.id === selectedRevisionId) || null
+ }, [selectedRevisionId, allRevisions])
- const { data, pageCount, total, drawingKind, vendorInfo } = documentResult
- const { stats, totalDocuments, primaryDrawingKind } = statsResult
+ // 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 (total === 0) {
- return (
- <Card>
- <CardContent className="flex items-center justify-center py-8">
- <div className="text-center">
- <FileText className="w-8 h-8 text-gray-400 mx-auto mb-2" />
- <p className="text-gray-600">등록된 문서가 없습니다.</p>
+ 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 || '파일 다운로드에 실패했습니다.')
+ }
+
+ 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('파일 다운로드 오류:', error)
+ alert(`파일 다운로드 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`)
+ }
+ }, [])
+
+ // 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 초기화 취소됨 (Dialog 닫힘)")
+ 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("파일 로드 실패:", 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="flex gap-4">
+ <RevisionTable
+ revisions={allRevisions}
+ onViewRevision={handleViewRevision}
+ onNewRevision={handleNewRevision}
+ />
+ <AttachmentTable
+ attachments={selectedRevisionData?.attachments || []}
+ onDownloadFile={handleDownloadFile}
+ />
+ </div>
+
+ {/* 통합된 문서 뷰어 다이얼로그 */}
+ <Dialog open={viewerOpen} onOpenChange={handleCloseViewer}>
+ <DialogContent className="w-[90vw] h-[90vh]" style={{ maxWidth: "none" }}>
+ <DialogHeader className="h-[38px]">
+ <DialogTitle>문서 미리보기</DialogTitle>
+ <DialogDescription>
+ 리비전 {selectedRevision?.revision} 첨부파일
+ </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">
+ 문서 뷰어 로딩 중...
+ </p>
+ </div>
+ )}
</div>
- </CardContent>
- </Card>
+ </DialogContent>
+ </Dialog>
+
+ <NewRevisionDialog
+ open={newRevisionDialogOpen}
+ onOpenChange={setNewRevisionDialogOpen}
+ documentId={selectedDocument.documentId}
+ documentTitle={selectedDocument.title}
+ drawingKind={selectedDocument.drawingKind || 'B4'}
+ onSuccess={handleRevisionUploadSuccess}
+ />
+ </>
+ )
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * 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
+ }
+ }
}
- // 실제 데이터의 drawingKind 또는 주요 drawingKind 사용
- const activeDrawingKind = drawingKind || primaryDrawingKind
-
- if (!activeDrawingKind) {
+ return (
+ <div className="flex flex-wrap items-center gap-3 rounded-lg bg-gray-50 p-4">
+ <div className="flex items-center gap-2">
+ <Badge variant="secondary" className="text-sm">
+ 문서: {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>총 {totalRevisions}개 리비전</span>
+ {selectedRevision && (
+ <>
+ <span>•</span>
+ <Badge variant="outline" className="text-sm">
+ 선택된 리비전: {selectedRevision.revision}
+ </Badge>
+ <span>({selectedRevision.attachments.length}개 파일)</span>
+ </>
+ )}
+ </div>
+ </div>
+ )
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * Main Exported Component
+ * -----------------------------------------------------------------------------------------------*/
+export function UserVendorDocumentDisplay({
+ allPromises,
+}: UserVendorDocumentDisplayProps) {
+ /**
+ * Selection state
+ */
+ 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="w-8 h-8 text-gray-400 mx-auto mb-2" />
- <p className="text-gray-600">문서 유형을 확인할 수 없습니다.</p>
+ <AlertCircle className="mx-auto mb-2 h-8 w-8 text-gray-400" />
+ <p className="text-gray-600">데이터를 불러올 수 없습니다.</p>
</div>
</CardContent>
</Card>
)
}
- // SimplifiedDocumentsTable에 전달할 promise (단일 객체로 변경)
- const tablePromise = Promise.resolve({ data, pageCount, total })
-
- const kindInfo = DRAWING_KIND_INFO[activeDrawingKind]
-
return (
- <div className="space-y-6">
- {/* 벤더 정보 헤더 */}
+ <DocumentSelectionContext.Provider value={ctx}>
+ <div className="space-y-4">
<Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Building className="w-5 h-5" />
- {vendorInfo?.vendorName || "내 회사"} 문서 관리
- </CardTitle>
- <CardDescription>
- {vendorInfo?.vendorCode && `코드: ${vendorInfo.vendorCode} • `}
- 총 {totalDocuments}개 문서
- </CardDescription>
- </CardHeader>
- <CardContent>
- <div className="flex gap-4">
- {Object.entries(stats).map(([kind, count]) => (
- <Badge
- key={kind}
- variant={kind === activeDrawingKind ? "default" : "outline"}
- className="flex items-center gap-1"
- >
- <FileText className="w-3 h-3" />
- {kind}: {count}개
- </Badge>
- ))}
- </div>
- </CardContent>
- </Card>
+ <CardContent className="flex items-center justify-center py-8">
+ <SimplifiedDocumentsTable
+ allPromises={allPromises}
+ onDataLoaded={setAllData}
+ onDocumentSelect={handleDocumentSelect}
+ />
+ </CardContent>
+ </Card>
+ <SelectedDocumentInfo />
-
- <SimplifiedDocumentsTable promises={tablePromise} />
-
- </div>
+ <SubTables />
+ </div>
+ </DocumentSelectionContext.Provider>
)
} \ No newline at end of file