From e6403e9610253b2bfc7a46e470c236e2ed10c626 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 17 Nov 2025 20:47:44 +0900 Subject: (김준회) swp: VxDCS 와 동일하게 변경, Complete된 리스트 중 없는 건이 있으면 DOCUMENT LIST에서 보여줌 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/swp/table/swp-document-detail-dialog.tsx | 567 +++++++++++++++------------ 1 file changed, 310 insertions(+), 257 deletions(-) (limited to 'lib/swp/table/swp-document-detail-dialog.tsx') diff --git a/lib/swp/table/swp-document-detail-dialog.tsx b/lib/swp/table/swp-document-detail-dialog.tsx index 418ddea9..d69e2986 100644 --- a/lib/swp/table/swp-document-detail-dialog.tsx +++ b/lib/swp/table/swp-document-detail-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { Dialog, DialogContent, @@ -12,20 +12,31 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Loader2, - ChevronDown, - ChevronRight, Download, FileIcon, - XCircle, AlertCircle, + ArrowDownToLine, + ArrowUpFromLine, } from "lucide-react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { - fetchVendorDocumentDetail, - cancelVendorFile, - downloadVendorFile, -} from "@/lib/swp/vendor-actions"; -import type { DocumentListItem, DocumentDetail } from "@/lib/swp/document-service"; + fetchGetRevTreeCompleteList, + parseRevisionTree, + fetchGetActivityFileList, + type ActivityFileApiResponse, +} from "@/lib/swp/api-client"; +import { downloadVendorFile } from "@/lib/swp/vendor-actions"; +import type { DocumentListItem } from "@/lib/swp/document-service"; import { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import { formatSwpDateShort, formatFileSize } from "@/lib/swp/utils"; interface SwpDocumentDetailDialogProps { open: boolean; @@ -36,22 +47,41 @@ interface SwpDocumentDetailDialogProps { userId: string; } +// Activity 행 데이터 +interface ActivityRow { + revNo: string; + revSeq: string; + stage: string; + actvNo: string; + inOut: "IN" | "OUT"; + statusCode: string; + statusName: string; + transmittalNo: string; + refActivityNo: string; + createDate: string; + createEmpNo: string; +} + export function SwpDocumentDetailDialog({ open, onOpenChange, document, projNo, }: SwpDocumentDetailDialogProps) { - const [detail, setDetail] = useState(null); + const [activities, setActivities] = useState([]); const [isLoading, setIsLoading] = useState(false); - const [expandedRevisions, setExpandedRevisions] = useState>(new Set()); - const [expandedActivities, setExpandedActivities] = useState>(new Set()); - const [isAllExpanded, setIsAllExpanded] = useState(true); // 기본값 true + const [selectedActivity, setSelectedActivity] = useState(null); + const [activityFiles, setActivityFiles] = useState([]); + const [isLoadingFiles, setIsLoadingFiles] = useState(false); // 문서 상세 로드 useEffect(() => { if (open && document) { loadDocumentDetail(); + } else { + // 다이얼로그 닫힐 때 초기화 + setSelectedActivity(null); + setActivityFiles([]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, document?.DOC_NO]); @@ -60,100 +90,79 @@ export function SwpDocumentDetailDialog({ if (!document) return; setIsLoading(true); + setSelectedActivity(null); + setActivityFiles([]); + try { - const detailData = await fetchVendorDocumentDetail(projNo, document.DOC_NO); - setDetail(detailData); - - // 모든 리비전 자동 펼치기 - const allRevKeys = new Set(); - const allActKeys = new Set(); - - detailData.revisions.forEach((revision) => { - const revKey = `${revision.revNo}|${revision.revSeq}`; - allRevKeys.add(revKey); - - // 모든 액티비티도 자동 펼치기 - revision.activities.forEach((activity) => { - const actKey = `${revKey}|${activity.actvNo}`; - allActKeys.add(actKey); + // GetRevTreeCompleteList 호출 + const tree = await fetchGetRevTreeCompleteList({ + proj_no: projNo, + doc_no: document.DOC_NO, + }); + + const parsed = await parseRevisionTree(tree); + + // Activity를 flat한 배열로 변환 (테이블용) + const flatActivities: ActivityRow[] = []; + parsed.revisions.forEach((rev) => { + rev.activities.forEach((act) => { + flatActivities.push({ + revNo: rev.revNo, + revSeq: rev.revSeq, + stage: rev.stage, + actvNo: act.actvNo, + inOut: act.inOut, + statusCode: act.statusCode, + statusName: act.statusName, + transmittalNo: act.transmittalNo, + refActivityNo: act.refActivityNo, + createDate: act.createDate, + createEmpNo: act.createEmpNo, + }); }); }); - - setExpandedRevisions(allRevKeys); - setExpandedActivities(allActKeys); - setIsAllExpanded(true); + + setActivities(flatActivities); } catch (error) { console.error("문서 상세 조회 실패:", error); - toast.error("문서 상세 정보를 불러오는데 실패했습니다"); + toast.error("문서 리비전 트리를 불러오는데 실패했습니다"); } finally { setIsLoading(false); } }; - 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; - }); - }; + // Activity 선택 및 파일 로드 + const handleActivityClick = async (activity: ActivityRow) => { + if (selectedActivity?.actvNo === activity.actvNo) { + // 같은 Activity 클릭 시 토글 + setSelectedActivity(null); + setActivityFiles([]); + return; + } - // 일괄 열기/닫기 - const handleToggleAll = () => { - if (!detail) return; + setSelectedActivity(activity); + setIsLoadingFiles(true); + setActivityFiles([]); - if (isAllExpanded) { - // 모두 닫기 - setExpandedRevisions(new Set()); - setExpandedActivities(new Set()); - setIsAllExpanded(false); - } else { - // 모두 열기 - const allRevKeys = new Set(); - const allActKeys = new Set(); - - detail.revisions.forEach((revision) => { - const revKey = `${revision.revNo}|${revision.revSeq}`; - allRevKeys.add(revKey); - - revision.activities.forEach((activity) => { - const actKey = `${revKey}|${activity.actvNo}`; - allActKeys.add(actKey); - }); + try { + // GetActivityFileList 호출 + const files = await fetchGetActivityFileList({ + proj_no: projNo, + doc_no: document?.DOC_NO || "", + rev_seq: activity.revSeq, }); - - setExpandedRevisions(allRevKeys); - setExpandedActivities(allActKeys); - setIsAllExpanded(true); - } - }; - const handleCancelFile = async (boxSeq: string, actvSeq: string, fileName: string) => { - try { - await cancelVendorFile(boxSeq, actvSeq); - toast.success(`파일 취소 완료: ${fileName}`); - - // 문서 상세 재로드 - await loadDocumentDetail(); + // 해당 Activity의 파일만 필터링 + const activitySpecificFiles = files.filter( + (f) => f.ACTV_NO === activity.actvNo + ); + + setActivityFiles(activitySpecificFiles); } catch (error) { - console.error("파일 취소 실패:", error); - toast.error("파일 취소에 실패했습니다"); + console.error("파일 목록 조회 실패:", error); + toast.error("파일 목록을 불러오는데 실패했습니다"); + } finally { + setIsLoadingFiles(false); } }; @@ -185,11 +194,24 @@ export function SwpDocumentDetailDialog({ } }; + // Revision별로 Activity 그룹핑 (rowspan용) + const groupedActivities = useMemo(() => { + const groups: Map = new Map(); + activities.forEach((activity) => { + const key = `${activity.revNo}|${activity.stage}`; + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(activity); + }); + return groups; + }, [activities]); + return ( - + - 문서 상세 + 문서 리비전 히스토리 {document && ( {document.DOC_NO} - {document.DOC_TITLE} @@ -198,9 +220,9 @@ export function SwpDocumentDetailDialog({ {document && ( -
+
{/* 문서 정보 */} -
+
프로젝트:
{document.PROJ_NO}
@@ -223,174 +245,216 @@ export function SwpDocumentDetailDialog({ 최신 리비전:
{document.LTST_REV_NO || "-"}
+
+ 총 Activity: +
{activities.length}개
+
- {/* 리비전 및 액티비티 트리 */} + {/* Activity 테이블 */} {isLoading ? (
- 문서 상세 로딩 중... + 리비전 트리 로딩 중...
- ) : detail && detail.revisions.length > 0 ? ( -
- {/* 일괄 열기/닫기 버튼 */} -
- -
- {detail.revisions.map((revision) => { - const revKey = `${revision.revNo}|${revision.revSeq}`; - const isRevExpanded = expandedRevisions.has(revKey); - - return ( -
- {/* 리비전 헤더 */} -
toggleRevision(revKey)} - > -
- {isRevExpanded ? ( - - ) : ( - - )} - - REV {revision.revNo} - - - {revision.stage} - - - {revision.activities.length}개 액티비티 / {revision.totalFiles}개 파일 - +
+
+ {selectedActivity ? ( + isLoadingFiles ? ( +
+ + 파일 로딩 중...
-
- - {/* 액티비티 목록 */} - {isRevExpanded && ( -
- {revision.activities.map((activity) => { - const actKey = `${revKey}|${activity.actvNo}`; - const isActExpanded = expandedActivities.has(actKey); - - // Activity 타입에 따른 색상 - const activityColor = - activity.type === "Receive" ? "bg-blue-100 text-blue-800" : - activity.type === "Send" ? "bg-green-100 text-green-800" : - "bg-purple-100 text-purple-800"; - - return ( -
- {/* 액티비티 헤더 */} -
toggleActivity(actKey)} - > -
- {isActExpanded ? ( - - ) : ( - - )} - - {activity.type} - - - {activity.actvNo} - - - {activity.toFrom} - - - {activity.files.length}개 파일 - + ) : activityFiles.length > 0 ? ( +
+ {activityFiles.map((file) => ( +
+
+ +
+
+ {file.FILE_NM}
+ {file.FILE_SZ && ( +
+ {formatFileSize(file.FILE_SZ)} +
+ )} + {file.STAT && ( + + {file.STAT} + + )}
- - {/* 파일 목록 */} - {isActExpanded && ( -
- {activity.files.map((file, idx) => ( -
-
- - {file.fileNm} - {file.fileSz && ( - - ({formatFileSize(file.fileSz)}) - - )} - {file.stat && ( - - {file.statNm || file.stat} - - )} -
-
- {file.canCancel && file.boxSeq && file.actvSeq && ( - - )} - -
-
- ))} -
- )}
- ); - })} + +
+ ))}
- )} -
- ); - })} -
+ ) : ( +
+
+ +

파일이 없습니다

+
+
+ ) + ) : ( +
+
+ +

Activity를 선택해주세요

+
+
+ )} +
+
+ ) : (
-

리비전 정보가 없습니다

+

Activity 정보가 없습니다

)}
@@ -399,14 +463,3 @@ export function SwpDocumentDetailDialog({
); } - -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