"use client"; import { useState, useTransition, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; import { useToast } from "@/hooks/use-toast"; import { FileText, ChevronRight, ChevronDown, X, Loader2, RefreshCw } from "lucide-react"; import { fetchVendorUploadedFiles, cancelVendorUploadedFile } from "../vendor-actions"; import type { SwpFileApiResponse } from "../api-client"; interface SwpUploadedFilesDialogProps { projNo: string; vndrCd: string; // UI 표시용으로만 사용 userId: string; } interface FileTreeNode { files: SwpFileApiResponse[]; } interface RevisionTreeNode { revNo: string; files: FileTreeNode; } interface DocumentTreeNode { docNo: string; revisions: Map; } export function SwpUploadedFilesDialog({ projNo, vndrCd, userId }: SwpUploadedFilesDialogProps) { const [open, setOpen] = useState(false); const [files, setFiles] = useState([]); const [isLoading, startLoading] = useTransition(); const [expandedDocs, setExpandedDocs] = useState>(new Set()); const [expandedRevs, setExpandedRevs] = useState>(new Set()); const [cancellingFiles, setCancellingFiles] = useState>(new Set()); const { toast } = useToast(); // 파일 목록을 트리 구조로 변환 const fileTree = useMemo(() => { const tree = new Map(); files.forEach((file) => { const docNo = file.OWN_DOC_NO; const revNo = file.REV_NO; if (!tree.has(docNo)) { tree.set(docNo, { docNo, revisions: new Map(), }); } const docNode = tree.get(docNo)!; if (!docNode.revisions.has(revNo)) { docNode.revisions.set(revNo, { revNo, files: { files: [] }, }); } const revNode = docNode.revisions.get(revNo)!; revNode.files.files.push(file); }); return tree; }, [files]); // 다이얼로그 열릴 때 파일 목록 조회 const handleOpenChange = (newOpen: boolean) => { setOpen(newOpen); if (newOpen) { loadFiles(); } }; // 파일 목록 조회 const loadFiles = () => { if (!projNo) { toast({ variant: "destructive", title: "조회 불가", description: "프로젝트 정보가 필요합니다.", }); return; } startLoading(async () => { try { // vndrCd는 서버에서 세션으로 자동 조회 const result = await fetchVendorUploadedFiles(projNo); setFiles(result); toast({ title: "조회 완료", description: `${result.length}개의 파일을 조회했습니다.`, }); } catch (error) { console.error("파일 목록 조회 실패:", error); toast({ variant: "destructive", title: "조회 실패", description: error instanceof Error ? error.message : "알 수 없는 오류", }); } }); }; // 파일 취소 const handleCancelFile = async (file: SwpFileApiResponse) => { if (!file.BOX_SEQ || !file.ACTV_SEQ) { toast({ variant: "destructive", title: "취소 불가", description: "파일 정보가 올바르지 않습니다.", }); return; } const fileKey = `${file.BOX_SEQ}_${file.ACTV_SEQ}`; setCancellingFiles((prev) => new Set(prev).add(fileKey)); try { await cancelVendorUploadedFile({ boxSeq: file.BOX_SEQ, actvSeq: file.ACTV_SEQ, userId, }); toast({ title: "취소 완료", description: `${file.FILE_NM} 파일이 취소되었습니다.`, }); // 목록 새로고침 loadFiles(); } catch (error) { console.error("파일 취소 실패:", error); toast({ variant: "destructive", title: "취소 실패", description: error instanceof Error ? error.message : "알 수 없는 오류", }); } finally { setCancellingFiles((prev) => { const newSet = new Set(prev); newSet.delete(fileKey); return newSet; }); } }; // 문서 토글 const toggleDoc = (docNo: string) => { setExpandedDocs((prev) => { const newSet = new Set(prev); if (newSet.has(docNo)) { newSet.delete(docNo); } else { newSet.add(docNo); } return newSet; }); }; // 리비전 토글 const toggleRev = (docNo: string, revNo: string) => { const key = `${docNo}_${revNo}`; setExpandedRevs((prev) => { const newSet = new Set(prev); if (newSet.has(key)) { newSet.delete(key); } else { newSet.add(key); } return newSet; }); }; return ( 업로드한 파일 목록 프로젝트: {projNo} | 업체: {vndrCd}
{/* 액션 바 */}
총 {files.length}개 파일
{/* 파일 트리 */} {isLoading && files.length === 0 ? (
파일 목록을 조회하는 중...
) : files.length === 0 ? (
업로드한 파일이 없습니다.
) : (
{Array.from(fileTree.entries()).map(([docNo, docNode]) => (
{/* 문서번호 */}
toggleDoc(docNo)} > {expandedDocs.has(docNo) ? ( ) : ( )} {docNo} {docNode.revisions.size}개 리비전
{/* 리비전 목록 */} {expandedDocs.has(docNo) && (
{Array.from(docNode.revisions.entries()).map(([revNo, revNode]) => { const revKey = `${docNo}_${revNo}`; return (
{/* 리비전 번호 */}
toggleRev(docNo, revNo)} > {expandedRevs.has(revKey) ? ( ) : ( )} Rev: {revNo} {revNode.files.files.length}개 파일
{/* 파일 목록 */} {expandedRevs.has(revKey) && (
{revNode.files.files.map((file, idx) => { const fileKey = `${file.BOX_SEQ}_${file.ACTV_SEQ}`; const isCancellable = file.STAT === "SCW01"; const isCancelling = cancellingFiles.has(fileKey); return (
{file.FILE_NM}
Stage: {file.STAGE} 상태: {file.STAT_NM || file.STAT || "알 수 없음"}
); })}
)}
); })}
)}
))}
)}
{/* 안내 메시지 */}

ℹ️ 접수 전(SCW01) 상태의 파일만 취소할 수 있습니다.

ℹ️ 취소된 파일은 목록에서 제거됩니다.

); }