"use client"; import React, { useState, useMemo } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ChevronDown, ChevronRight, Download, FileIcon, Search, XCircle, Loader2, } from "lucide-react"; import { cancelVendorUploadedFile } from "@/lib/swp/vendor-actions"; import type { SwpFileApiResponse } from "@/lib/swp/api-client"; import { toast } from "sonner"; import { formatSwpDate } from "@/lib/swp/utils"; interface SwpInboxHistoryDialogProps { open: boolean; onOpenChange: (open: boolean) => void; docNo: string | null; files: SwpFileApiResponse[]; // 전체 파일 목록 projNo: string; userId: string; } // Rev별 그룹 타입 interface RevisionGroup { revNo: string; stage: string; activities: ActivityGroup[]; totalFiles: number; } // Activity별 그룹 타입 (activity가 null일 수 있음) interface ActivityGroup { actvNo: string | null; files: SwpFileApiResponse[]; } export function SwpInboxHistoryDialog({ open, onOpenChange, docNo, files, projNo, userId, }: SwpInboxHistoryDialogProps) { const [expandedRevisions, setExpandedRevisions] = useState>(new Set()); const [expandedActivities, setExpandedActivities] = useState>(new Set()); const [isAllExpanded, setIsAllExpanded] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const [cancellingFiles, setCancellingFiles] = useState>(new Set()); const [cancelledFiles, setCancelledFiles] = useState>(new Set()); // 취소된 파일 추적 // 해당 Document No의 모든 파일 필터링 const documentFiles = useMemo(() => { if (!docNo) return []; return files.filter((file) => file.OWN_DOC_NO === docNo); }, [docNo, files]); // 검색어 필터링 const filteredFiles = useMemo(() => { if (!searchQuery.trim()) return documentFiles; const query = searchQuery.toLowerCase(); return documentFiles.filter((file) => { return ( file.REV_NO?.toLowerCase().includes(query) || file.ACTV_NO?.toLowerCase().includes(query) || file.FILE_NM?.toLowerCase().includes(query) || file.STAGE?.toLowerCase().includes(query) ); }); }, [documentFiles, searchQuery]); // 파일들을 Rev > Activity 구조로 그룹핑 const revisionGroups = useMemo(() => { const revMap = new Map(); filteredFiles.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) => { // Upload Date 기준 정렬 const sortedFiles = [...files].sort((a, b) => (b.CRTE_DTM || "").localeCompare(a.CRTE_DTM || "") ); activities.push({ actvNo, files: sortedFiles }); }); // 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)); }, [filteredFiles]); // 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 (file: SwpFileApiResponse) => { if (!file.BOX_SEQ || !file.ACTV_SEQ) { toast.error("File cannot be canceled (Missing BOX_SEQ or ACTV_SEQ)"); return; } const fileKey = `${file.BOX_SEQ}_${file.FILE_SEQ}`; if (cancellingFiles.has(fileKey)) { return; // 이미 취소 중 } try { setCancellingFiles((prev) => new Set(prev).add(fileKey)); await cancelVendorUploadedFile({ boxSeq: file.BOX_SEQ, actvSeq: file.ACTV_SEQ, userId, }); toast.success(`File canceled: ${file.FILE_NM}`); // 취소된 파일로 마킹 (상태 변경) setCancelledFiles((prev) => new Set(prev).add(fileKey)); } catch (error) { console.error("File cancel failed:", error); toast.error("Failed to cancel file"); } finally { setCancellingFiles((prev) => { const newSet = new Set(prev); newSet.delete(fileKey); return newSet; }); } }; const handleDownloadFile = async (file: SwpFileApiResponse) => { try { toast.info("Preparing file download..."); // API route를 통해 다운로드 const downloadUrl = `/api/swp/download/${encodeURIComponent(file.OWN_DOC_NO)}?projNo=${encodeURIComponent(projNo)}&fileName=${encodeURIComponent(file.FILE_NM)}`; // 새 탭에서 다운로드 window.open(downloadUrl, "_blank"); toast.success(`File download started: ${file.FILE_NM}`); } catch (error) { console.error("File download failed:", error); toast.error("Failed to download file"); } }; const formatFileSize = (sizeStr: string | null): string => { if (!sizeStr) return "-"; 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`; }; return ( Document History {docNo && ( {docNo} - Total {documentFiles.length} files )} {docNo && (
{/* 검색 및 제어 */}
setSearchQuery(e.target.value)} className="pl-10" />
{/* 검색 결과 안내 */} {searchQuery && (
Search Results: {filteredFiles.length} files (Total {documentFiles.length})
)} {/* 리비전 및 액티비티 트리 */} {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} groups / {revision.totalFiles} files
{/* 액티비티 목록 */} {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} ) : ( No Activity )} {activity.files.length} files
{/* 파일 목록 */} {isActExpanded && (
{activity.files.map((file, idx) => { const fileKey = `${file.BOX_SEQ}_${file.FILE_SEQ}`; const isCancelling = cancellingFiles.has(fileKey); const isCancelled = cancelledFiles.has(fileKey); const currentStatus = isCancelled ? "SCW09" : file.STAT; const currentStatusNm = isCancelled ? "Cancelled" : file.STAT_NM; const canCancel = currentStatus === "SCW01"; // Standby만 취소 가능 return (
{file.FILE_NM}
{formatFileSize(file.FILE_SZ)} {formatSwpDate(file.CRTE_DTM)} {currentStatusNm && ( <> {currentStatusNm} )}
{canCancel && ( )}
); })}
)}
); })}
)}
); })}
) : (
{searchQuery ? "No search results" : "No file information"}
)}
)}
); }