"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`; }