From b9f575f6110faabc7b062f84bf491d69d88a10a5 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 17 Nov 2025 15:42:29 +0900 Subject: (김준회) swp: GetExternalInboxList를 메인 테이블로 변경 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/swp/table/swp-inbox-document-detail-dialog.tsx | 450 +++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 lib/swp/table/swp-inbox-document-detail-dialog.tsx (limited to 'lib/swp/table/swp-inbox-document-detail-dialog.tsx') diff --git a/lib/swp/table/swp-inbox-document-detail-dialog.tsx b/lib/swp/table/swp-inbox-document-detail-dialog.tsx new file mode 100644 index 00000000..ca7fcf1b --- /dev/null +++ b/lib/swp/table/swp-inbox-document-detail-dialog.tsx @@ -0,0 +1,450 @@ +"use client"; + +import React, { useState, useMemo } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + ChevronDown, + ChevronRight, + Download, + FileIcon, + XCircle, + AlertCircle, +} from "lucide-react"; +import { + cancelVendorFile, + downloadVendorFile, +} from "@/lib/swp/vendor-actions"; +import type { SwpFileApiResponse } from "@/lib/swp/api-client"; +import type { InboxDocumentItem } from "./swp-inbox-table"; +import { toast } from "sonner"; + +interface SwpInboxDocumentDetailDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + document: InboxDocumentItem | null; + projNo: string; + vendorCode: string; + userId: string; +} + +// 리비전별 그룹 타입 +interface RevisionGroup { + revNo: string; + stage: string; + activities: ActivityGroup[]; + totalFiles: number; +} + +// Activity별 그룹 타입 (activity가 null일 수 있음) +interface ActivityGroup { + actvNo: string | null; + files: SwpFileApiResponse[]; +} + +export function SwpInboxDocumentDetailDialog({ + open, + onOpenChange, + document, + projNo, +}: SwpInboxDocumentDetailDialogProps) { + const [expandedRevisions, setExpandedRevisions] = useState>(new Set()); + const [expandedActivities, setExpandedActivities] = useState>(new Set()); + const [isAllExpanded, setIsAllExpanded] = useState(true); + + // 파일들을 리비전 > Activity 구조로 그룹핑 + const revisionGroups = useMemo(() => { + if (!document) return []; + + const revMap = new Map(); + + document.files.forEach((file) => { + const revKey = `${file.REV_NO}|${file.STAGE}`; + if (!revMap.has(revKey)) { + revMap.set(revKey, []); + } + revMap.get(revKey)!.push(file); + }); + + const result: RevisionGroup[] = []; + + revMap.forEach((revFiles, revKey) => { + const [revNo, stage] = revKey.split("|"); + + // Activity별로 그룹핑 (null 가능) + const actMap = new Map(); + + revFiles.forEach((file) => { + const actvNo = file.ACTV_NO || null; + if (!actMap.has(actvNo)) { + actMap.set(actvNo, []); + } + actMap.get(actvNo)!.push(file); + }); + + const activities: ActivityGroup[] = []; + actMap.forEach((files, actvNo) => { + activities.push({ actvNo, files }); + }); + + // Activity가 없는 것을 먼저, 있는 것을 나중에 정렬 + activities.sort((a, b) => { + if (a.actvNo === null && b.actvNo !== null) return -1; + if (a.actvNo !== null && b.actvNo === null) return 1; + if (a.actvNo === null && b.actvNo === null) return 0; + return (a.actvNo || "").localeCompare(b.actvNo || ""); + }); + + result.push({ + revNo, + stage, + activities, + totalFiles: revFiles.length, + }); + }); + + // 리비전 번호로 정렬 (최신이 위로) + return result.sort((a, b) => b.revNo.localeCompare(a.revNo)); + }, [document]); + + // Dialog가 열릴 때 모두 펼치기 + React.useEffect(() => { + if (open && revisionGroups.length > 0) { + const allRevKeys = new Set(); + const allActKeys = new Set(); + + revisionGroups.forEach((revision) => { + const revKey = revision.revNo; + allRevKeys.add(revKey); + + revision.activities.forEach((activity) => { + const actKey = `${revKey}|${activity.actvNo || "NO_ACTIVITY"}`; + allActKeys.add(actKey); + }); + }); + + setExpandedRevisions(allRevKeys); + setExpandedActivities(allActKeys); + setIsAllExpanded(true); + } + }, [open, revisionGroups]); + + const toggleRevision = (revKey: string) => { + setExpandedRevisions((prev) => { + const newSet = new Set(prev); + if (newSet.has(revKey)) { + newSet.delete(revKey); + } else { + newSet.add(revKey); + } + return newSet; + }); + }; + + const toggleActivity = (actKey: string) => { + setExpandedActivities((prev) => { + const newSet = new Set(prev); + if (newSet.has(actKey)) { + newSet.delete(actKey); + } else { + newSet.add(actKey); + } + return newSet; + }); + }; + + // 일괄 열기/닫기 + const handleToggleAll = () => { + if (isAllExpanded) { + // 모두 닫기 + setExpandedRevisions(new Set()); + setExpandedActivities(new Set()); + setIsAllExpanded(false); + } else { + // 모두 열기 + const allRevKeys = new Set(); + const allActKeys = new Set(); + + revisionGroups.forEach((revision) => { + const revKey = revision.revNo; + allRevKeys.add(revKey); + + revision.activities.forEach((activity) => { + const actKey = `${revKey}|${activity.actvNo || "NO_ACTIVITY"}`; + allActKeys.add(actKey); + }); + }); + + setExpandedRevisions(allRevKeys); + setExpandedActivities(allActKeys); + setIsAllExpanded(true); + } + }; + + const handleCancelFile = async (boxSeq: string, actvSeq: string, fileName: string) => { + try { + await cancelVendorFile(boxSeq, actvSeq); + toast.success(`파일 취소 완료: ${fileName}`); + + // Dialog를 닫고 부모 컴포넌트가 새로고침하도록 함 + onOpenChange(false); + } catch (error) { + console.error("파일 취소 실패:", error); + toast.error("파일 취소에 실패했습니다"); + } + }; + + const handleDownloadFile = async (fileName: string, ownDocNo: string) => { + try { + toast.info("파일 다운로드 중..."); + const result = await downloadVendorFile(projNo, ownDocNo, fileName); + + if (!result.success || !result.data) { + toast.error(result.error || "파일 다운로드 실패"); + return; + } + + // Blob 생성 및 다운로드 + const blob = new Blob([Buffer.from(result.data)], { type: result.mimeType }); + const url = URL.createObjectURL(blob); + const link = window.document.createElement("a"); + link.href = url; + link.download = result.fileName || fileName; + window.document.body.appendChild(link); + link.click(); + window.document.body.removeChild(link); + URL.revokeObjectURL(url); + + toast.success(`파일 다운로드 완료: ${fileName}`); + } catch (error) { + console.error("파일 다운로드 실패:", error); + toast.error("파일 다운로드에 실패했습니다"); + } + }; + + return ( + + + + 업로드 파일 상세 + {document && ( + + {document.ownDocNo} + + )} + + + {document && ( +
+ {/* 문서 정보 */} +
+
+ OWN_DOC_NO: +
{document.ownDocNo}
+
+
+ 최신 스테이지: +
{document.latestStage || "-"}
+
+
+ 최신 리비전: +
{document.latestRevNo || "-"}
+
+
+ 최신 REV 파일: +
{document.latestRevFileCount}개
+
+
+ + {/* 리비전 및 액티비티 트리 */} + {revisionGroups.length > 0 ? ( +
+ {/* 일괄 열기/닫기 버튼 */} +
+ +
+ {revisionGroups.map((revision) => { + const revKey = revision.revNo; + const isRevExpanded = expandedRevisions.has(revKey); + + return ( +
+ {/* 리비전 헤더 */} +
toggleRevision(revKey)} + > +
+ {isRevExpanded ? ( + + ) : ( + + )} + + REV {revision.revNo} + + + {revision.stage} + + + {revision.activities.length}개 그룹 / {revision.totalFiles}개 파일 + +
+
+ + {/* 액티비티 목록 (또는 Activity 없는 파일들) */} + {isRevExpanded && ( +
+ {revision.activities.map((activity) => { + const actKey = `${revKey}|${activity.actvNo || "NO_ACTIVITY"}`; + const isActExpanded = expandedActivities.has(actKey); + + return ( +
+ {/* 액티비티 헤더 */} +
toggleActivity(actKey)} + > +
+ {isActExpanded ? ( + + ) : ( + + )} + {activity.actvNo ? ( + <> + + Activity + + + {activity.actvNo} + + + ) : ( + + Activity 없음 + + )} + + {activity.files.length}개 파일 + +
+
+ + {/* 파일 목록 */} + {isActExpanded && ( +
+ {activity.files.map((file, idx) => ( +
+
+ + {file.FILE_NM} + {file.FILE_SZ && ( + + ({formatFileSize(file.FILE_SZ)}) + + )} + {file.STAT && ( + + {file.STAT_NM || file.STAT} + + )} +
+
+ {file.STAT === "SCW01" && file.BOX_SEQ && file.ACTV_SEQ && ( + + )} + {file.FLD_PATH && ( + + )} +
+
+ ))} +
+ )} +
+ ); + })} +
+ )} +
+ ); + })} +
+ ) : ( +
+ +

파일 정보가 없습니다

+
+ )} +
+ )} +
+
+ ); +} + +function formatFileSize(sizeStr: string): string { + const bytes = parseInt(sizeStr, 10); + if (isNaN(bytes)) return sizeStr; + + const kb = bytes / 1024; + const mb = kb / 1024; + + return mb >= 1 ? `${mb.toFixed(2)} MB` : `${kb.toFixed(2)} KB`; +} + -- cgit v1.2.3