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-inbox-table.tsx | 703 ++++++++++++++++++++++++++++----------
 1 file changed, 514 insertions(+), 189 deletions(-)

(limited to 'lib/swp/table/swp-inbox-table.tsx')

diff --git a/lib/swp/table/swp-inbox-table.tsx b/lib/swp/table/swp-inbox-table.tsx
index c3f3a243..a2fedeed 100644
--- a/lib/swp/table/swp-inbox-table.tsx
+++ b/lib/swp/table/swp-inbox-table.tsx
@@ -1,11 +1,6 @@
 "use client";
 
-import React, { useState, useMemo } from "react";
-import {
-  useReactTable,
-  getCoreRowModel,
-  flexRender,
-} from "@tanstack/react-table";
+import React, { useMemo, useState } from "react";
 import {
   Table,
   TableBody,
@@ -15,230 +10,560 @@ import {
   TableRow,
 } from "@/components/ui/table";
 import { Badge } from "@/components/ui/badge";
-import { ColumnDef } from "@tanstack/react-table";
-import { SwpInboxDocumentDetailDialog } from "./swp-inbox-document-detail-dialog";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Download, XCircle } from "lucide-react";
+import { cancelVendorUploadedFile } from "@/lib/swp/vendor-actions";
 import type { SwpFileApiResponse } from "@/lib/swp/api-client";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+import { formatSwpDate } from "@/lib/swp/utils";
+import { SwpInboxHistoryDialog } from "./swp-inbox-history-dialog";
+
+// 업로드 필요 문서 타입 (DB stageDocuments에서 조회)
+interface RequiredDocument {
+  vendorDocNumber: string;
+  title: string;
+  buyerSystemComment: string | null;
+}
 
 interface SwpInboxTableProps {
   files: SwpFileApiResponse[];
+  requiredDocs: RequiredDocument[];
   projNo: string;
   vendorCode: string;
   userId: string;
 }
 
-// 문서별로 그룹핑된 데이터 타입
-export interface InboxDocumentItem {
-  ownDocNo: string;
-  latestRevFileCount: number; // 최신 REV의 파일 개수
-  latestStage: string;
-  latestRevNo: string;
-  latestStatus: string | null;
-  latestStatusNm: string | null;
-  files: SwpFileApiResponse[];
+// 테이블 행 데이터 (플랫하게 펼침)
+interface TableRowData {
+  uploadId: string | null; // 업로드 필요 문서는 null
+  docNo: string;
+  revNo: string | null;
+  stage: string | null;
+  status: string | null;
+  statusNm: string | null;
+  actvNo: string | null;
+  crter: string | null; // CRTER (그대로 표시)
+  note: string | null; // 첫 번째 파일의 note 또는 buyerSystemComment
+  file: SwpFileApiResponse | null; // 업로드 필요 문서는 null
+  uploadDate: string | null;
+  // 각 행이 속한 그룹의 정보
+  isFirstFileInRev: boolean;
+  fileCountInRev: number;
+  // 업로드 필요 문서 여부
+  isRequiredDoc: boolean;
 }
 
-// 테이블 컬럼 정의
-const inboxDocumentColumns: ColumnDef<InboxDocumentItem>[] = [
-  {
-    accessorKey: "latestStatusNm",
-    header: "상태",
-    cell: ({ row }) => {
-      const statNm = row.original.latestStatusNm;
-      const stat = row.original.latestStatus;
-      const displayStatus = statNm || stat || "-";
-      
-      if (!stat) return displayStatus;
-
-      // STAT 코드 기반 색상 결정
-      const color =
-        stat === "SCW03" || stat === "SCW08" ? "bg-green-100 text-green-800" :      // Complete, Checked
-        stat === "SCW02" ? "bg-blue-100 text-blue-800" :                            // Processing
-        stat === "SCW01" ? "bg-yellow-100 text-yellow-800" :                        // Standby
-        stat === "SCW04" || stat === "SCW05" || stat === "SCW06" ? "bg-red-100 text-red-800" : // Reject, Error Zip, Error Meta
-        stat === "SCW07" ? "bg-purple-100 text-purple-800" :                        // Send for Eng Verification
-        stat === "SCW09" ? "bg-gray-100 text-gray-800" :                            // Cancelled
-        stat === "SCW00" ? "bg-orange-100 text-orange-800" :                        // Upload
-        "bg-gray-100 text-gray-800";                                                 // 기타
-
-      return (
-        <Badge variant="outline" className={color}>
-          {displayStatus}
-        </Badge>
-      );
-    },
-    size: 120,
-  },
-  {
-    accessorKey: "ownDocNo",
-    header: "OWN_DOC_NO",
-    cell: ({ row }) => (
-      <div className="font-mono text-sm">{row.original.ownDocNo}</div>
-    ),
-    size: 300,
-  },
-  {
-    accessorKey: "latestStage",
-    header: "최신 스테이지",
-    cell: ({ row }) => {
-      const stage = row.original.latestStage;
-      if (!stage) return "-";
-
-      const color =
-        stage === "IFC" ? "bg-green-100 text-green-800" :
-        stage === "IFA" ? "bg-blue-100 text-blue-800" :
-        "bg-gray-100 text-gray-800";
-
-      return (
-        <Badge variant="outline" className={color}>
-          {stage}
-        </Badge>
-      );
-    },
-    size: 120,
-  },
-  {
-    accessorKey: "latestRevNo",
-    header: "최신 REV",
-    cell: ({ row }) => row.original.latestRevNo || "-",
-    size: 100,
-  },
-  {
-    accessorKey: "latestRevFileCount",
-    header: "최신 REV 파일 수",
-    cell: ({ row }) => (
-      <div className="text-center">
-        <div className="text-sm font-medium">
-          {row.original.latestRevFileCount}개
-        </div>
-      </div>
-    ),
-    size: 100,
-  },
-];
+// Status 집계 타입
+interface StatusCount {
+  status: string;
+  statusNm: string;
+  count: number;
+  color: string;
+}
 
 export function SwpInboxTable({
   files,
+  requiredDocs,
   projNo,
-  vendorCode,
   userId,
 }: SwpInboxTableProps) {
-  const [dialogOpen, setDialogOpen] = useState(false);
-  const [selectedDocument, setSelectedDocument] = useState<InboxDocumentItem | null>(null);
-
-  // 파일들을 문서별로 그룹핑
-  const documents = useMemo(() => {
-    const docMap = new Map<string, SwpFileApiResponse[]>();
+  const [selectedStatus, setSelectedStatus] = useState<string | null>(null);
+  const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()); // 선택된 파일 (fileKey)
+  const [historyDialogOpen, setHistoryDialogOpen] = useState(false);
+  const [selectedDocNo, setSelectedDocNo] = useState<string | null>(null);
 
+  // Status 집계 (API 응답 + 업로드 필요 문서)
+  const statusCounts = useMemo(() => {
+    const statusMap = new Map<string, { statusNm: string; count: number }>();
+    
+    // API 응답 파일 집계
     files.forEach((file) => {
-      const docNo = file.OWN_DOC_NO;
-      if (!docMap.has(docNo)) {
-        docMap.set(docNo, []);
+      const status = file.STAT || "UNKNOWN";
+      const statusNm = file.STAT_NM || status;
+      
+      if (statusMap.has(status)) {
+        statusMap.get(status)!.count++;
+      } else {
+        statusMap.set(status, { statusNm, count: 1 });
       }
-      docMap.get(docNo)!.push(file);
     });
 
-    const result: InboxDocumentItem[] = [];
+    // 업로드 필요 문서 집계
+    if (requiredDocs.length > 0) {
+      const status = "UPLOAD_REQUIRED";
+      const statusNm = "Upload Required";
+      statusMap.set(status, { statusNm, count: requiredDocs.length });
+    }
 
-    docMap.forEach((docFiles, ownDocNo) => {
-      // 최신 REV 찾기 (REV_NO 기준으로 정렬)
-      const sortedByRev = [...docFiles].sort((a, b) => 
-        (b.REV_NO || "").localeCompare(a.REV_NO || "")
-      );
-      const latestRevNo = sortedByRev[0].REV_NO || "";
+    const counts: StatusCount[] = [];
+    statusMap.forEach((value, status) => {
+      const color =
+        status === "SCW03" || status === "SCW08" ? "bg-green-100 text-green-800 hover:bg-green-200" :
+        status === "SCW02" ? "bg-blue-100 text-blue-800 hover:bg-blue-200" :
+        status === "SCW01" ? "bg-yellow-100 text-yellow-800 hover:bg-yellow-200" :
+        status === "SCW04" || status === "SCW05" || status === "SCW06" ? "bg-red-100 text-red-800 hover:bg-red-200" :
+        status === "SCW07" ? "bg-purple-100 text-purple-800 hover:bg-purple-200" :
+        status === "SCW09" ? "bg-gray-100 text-gray-800 hover:bg-gray-200" :
+        status === "SCW00" ? "bg-orange-100 text-orange-800 hover:bg-orange-200" :
+        status === "UPLOAD_REQUIRED" ? "bg-amber-100 text-amber-800 hover:bg-amber-200" :
+        "bg-gray-100 text-gray-800 hover:bg-gray-200";
 
-      // 최신 REV의 파일들만 필터링
-      const latestRevFiles = docFiles.filter(file => file.REV_NO === latestRevNo);
+      counts.push({
+        status,
+        statusNm: value.statusNm,
+        count: value.count,
+        color,
+      });
+    });
 
-      // 최신 REV 내에서 가장 최근 생성된 파일 찾기 (상태 표시용)
-      const sortedLatestRevFiles = [...latestRevFiles].sort((a, b) => 
-        (b.CRTE_DTM || "").localeCompare(a.CRTE_DTM || "")
-      );
-      const latestFile = sortedLatestRevFiles[0];
-
-      result.push({
-        ownDocNo,
-        latestRevFileCount: latestRevFiles.length, // 최신 REV의 파일 개수
-        latestStage: latestFile.STAGE || "",
-        latestRevNo: latestRevNo,
-        latestStatus: latestFile.STAT,
-        latestStatusNm: latestFile.STAT_NM,
-        files: docFiles, // 전체 파일 목록 (상세보기용)
+    // 개수 순으로 정렬 (Upload Required를 맨 앞으로)
+    return counts.sort((a, b) => {
+      if (a.status === "UPLOAD_REQUIRED") return -1;
+      if (b.status === "UPLOAD_REQUIRED") return 1;
+      return b.count - a.count;
+    });
+  }, [files, requiredDocs]);
+
+  // 데이터 그룹화 및 플랫 변환 (API 응답 + 업로드 필요 문서)
+  const tableRows = useMemo(() => {
+    const rows: TableRowData[] = [];
+
+    // 1. API 응답 파일 처리
+    // Status 필터링
+    let filteredFiles = files;
+    if (selectedStatus && selectedStatus !== "UPLOAD_REQUIRED") {
+      filteredFiles = files.filter((file) => file.STAT === selectedStatus);
+    }
+
+    // BOX_SEQ 기준으로 그룹화
+    const uploadGroups = new Map<string, SwpFileApiResponse[]>();
+    
+    if (!selectedStatus || selectedStatus !== "UPLOAD_REQUIRED") {
+      filteredFiles.forEach((file) => {
+        const uploadId = file.BOX_SEQ || "NO_UPLOAD_ID";
+        if (!uploadGroups.has(uploadId)) {
+          uploadGroups.set(uploadId, []);
+        }
+        uploadGroups.get(uploadId)!.push(file);
+      });
+    }
+
+    uploadGroups.forEach((uploadFiles, uploadId) => {
+      // 2. Document No 기준으로 그룹화
+      const docGroups = new Map<string, SwpFileApiResponse[]>();
+      
+      uploadFiles.forEach((file) => {
+        const docNo = file.OWN_DOC_NO;
+        if (!docGroups.has(docNo)) {
+          docGroups.set(docNo, []);
+        }
+        docGroups.get(docNo)!.push(file);
+      });
+
+      docGroups.forEach((docFiles, docNo) => {
+        // 3. 최신 RevNo 찾기 (CRTE_DTM 기준)
+        const sortedByDate = [...docFiles].sort((a, b) =>
+          (b.CRTE_DTM || "").localeCompare(a.CRTE_DTM || "")
+        );
+        const latestRevNo = sortedByDate[0]?.REV_NO || "";
+
+        // 4. 최신 Rev의 파일들만 필터링
+        const latestRevFiles = docFiles.filter(
+          (file) => file.REV_NO === latestRevNo
+        );
+
+        // 5. Upload Date 기준 DESC 정렬
+        const sortedFiles = latestRevFiles.sort((a, b) =>
+          (b.CRTE_DTM || "").localeCompare(a.CRTE_DTM || "")
+        );
+
+        // 6. 최신 파일의 정보로 Rev 메타데이터 설정 (첫 번째 파일의 crter와 note 사용)
+        const latestFile = sortedFiles[0];
+        if (!latestFile) return;
+
+        // 7. 각 파일을 테이블 행으로 변환 (crter와 note는 첫 번째 파일 것으로 통일)
+        sortedFiles.forEach((file, idx) => {
+          rows.push({
+            uploadId,
+            docNo,
+            revNo: latestRevNo,
+            stage: latestFile.STAGE,
+            status: latestFile.STAT,
+            statusNm: latestFile.STAT_NM,
+            actvNo: latestFile.ACTV_NO,
+            crter: latestFile.CRTER, // CRTER 그대로
+            note: latestFile.NOTE || null, // 첫 번째 파일의 note
+            file,
+            uploadDate: file.CRTE_DTM,
+            isFirstFileInRev: idx === 0,
+            fileCountInRev: sortedFiles.length,
+            isRequiredDoc: false,
+          });
+        });
       });
     });
 
-    return result.sort((a, b) => a.ownDocNo.localeCompare(b.ownDocNo));
-  }, [files]);
+    // 2. 업로드 필요 문서 추가 (Upload Required 필터일 때만 또는 필터 없을 때)
+    if (!selectedStatus || selectedStatus === "UPLOAD_REQUIRED") {
+      requiredDocs.forEach((doc) => {
+        rows.push({
+          uploadId: null,
+          docNo: doc.vendorDocNumber,
+          revNo: null,
+          stage: null,
+          status: "UPLOAD_REQUIRED",
+          statusNm: "Upload Required",
+          actvNo: null,
+          crter: null,
+          note: doc.buyerSystemComment,
+          file: null,
+          uploadDate: null,
+          isFirstFileInRev: true,
+          fileCountInRev: 1,
+          isRequiredDoc: true,
+        });
+      });
+    }
+
+    // Upload Date 기준 전체 정렬 (null은 맨 뒤로)
+    return rows.sort((a, b) => {
+      if (!a.uploadDate) return 1;
+      if (!b.uploadDate) return -1;
+      return b.uploadDate.localeCompare(a.uploadDate);
+    });
+  }, [files, requiredDocs, selectedStatus]);
+
+  // 선택 가능한 파일들 (Standby 상태만)
+  const selectableFiles = useMemo(() => {
+    return tableRows
+      .filter((row) => row.file && row.status === "SCW01")
+      .map((row) => `${row.file!.BOX_SEQ}_${row.file!.FILE_SEQ}`);
+  }, [tableRows]);
+
+  // 전체 선택/해제
+  const handleSelectAll = (checked: boolean) => {
+    if (checked) {
+      setSelectedFiles(new Set(selectableFiles));
+    } else {
+      setSelectedFiles(new Set());
+    }
+  };
+
+  // 개별 선택/해제
+  const handleSelectFile = (fileKey: string, checked: boolean | "indeterminate") => {
+    const isChecked = checked === true;
+    setSelectedFiles((prev) => {
+      const newSet = new Set(prev);
+      if (isChecked) {
+        newSet.add(fileKey);
+      } else {
+        newSet.delete(fileKey);
+      }
+      return newSet;
+    });
+  };
+
+  // 선택된 파일 일괄 취소
+  const handleBulkCancel = async () => {
+    if (selectedFiles.size === 0) {
+      toast.error("취소할 파일을 선택해주세요");
+      return;
+    }
 
-  const table = useReactTable({
-    data: documents,
-    columns: inboxDocumentColumns,
-    getCoreRowModel: getCoreRowModel(),
-  });
+    const filesToCancel = tableRows.filter((row) => 
+      row.file && selectedFiles.has(`${row.file.BOX_SEQ}_${row.file.FILE_SEQ}`)
+    );
 
-  // 문서 클릭 핸들러
-  const handleDocumentClick = (document: InboxDocumentItem) => {
-    setSelectedDocument(document);
-    setDialogOpen(true);
+    if (filesToCancel.length === 0) {
+      toast.error("취소할 파일이 없습니다");
+      return;
+    }
+
+    try {
+      toast.info(`${filesToCancel.length}개 파일 취소 중...`);
+
+      // 병렬 취소
+      const cancelPromises = filesToCancel.map((row) =>
+        cancelVendorUploadedFile({
+          boxSeq: row.file!.BOX_SEQ!,
+          actvSeq: row.file!.ACTV_SEQ!,
+          userId,
+        })
+      );
+
+      await Promise.all(cancelPromises);
+
+      toast.success(`${filesToCancel.length}개 파일 취소 완료`);
+      setSelectedFiles(new Set()); // 선택 초기화
+      
+      // 페이지 리프레시
+      window.location.reload();
+    } catch (error) {
+      console.error("일괄 취소 실패:", error);
+      toast.error("일부 파일 취소에 실패했습니다");
+    }
+  };
+
+
+  const handleDownloadFile = async (file: SwpFileApiResponse) => {
+    try {
+      toast.info("파일 다운로드 준비 중...");
+      
+      // 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.FILE_NM}`);
+    } catch (error) {
+      console.error("파일 다운로드 실패:", error);
+      toast.error("파일 다운로드에 실패했습니다");
+    }
   };
 
+
+  // 행 클릭 핸들러 (Document No 기준 전체 이력 보기)
+  const handleRowClick = (docNo: string) => {
+    setSelectedDocNo(docNo);
+    setHistoryDialogOpen(true);
+  };
+
+  const getStatusBadge = (status: string | null, statusNm: string | null) => {
+    const displayStatus = statusNm || status || "-";
+    
+    if (!status) return <span className="text-muted-foreground">{displayStatus}</span>;
+
+    const color =
+      status === "SCW03" || status === "SCW08" ? "bg-green-100 text-green-800" :
+      status === "SCW02" ? "bg-blue-100 text-blue-800" :
+      status === "SCW01" ? "bg-yellow-100 text-yellow-800" :
+      status === "SCW04" || status === "SCW05" || status === "SCW06" ? "bg-red-100 text-red-800" :
+      status === "SCW07" ? "bg-purple-100 text-purple-800" :
+      status === "SCW09" ? "bg-gray-100 text-gray-800" :
+      status === "SCW00" ? "bg-orange-100 text-orange-800" :
+      "bg-gray-100 text-gray-800";
+
+    return (
+      <Badge variant="outline" className={color}>
+        {displayStatus}
+      </Badge>
+    );
+  };
+
+  if (files.length === 0 && requiredDocs.length === 0) {
+    return (
+      <div className="border rounded-lg p-8 text-center text-muted-foreground">
+        업로드한 파일이 없습니다.
+      </div>
+    );
+  }
+
   return (
     <div className="space-y-4">
+      {/* Status 필터 UI */}
+      <div className="flex items-center justify-between gap-4">
+        <div className="flex flex-wrap gap-2 p-4 bg-muted/30 rounded-lg flex-1">
+          <Button
+            variant={selectedStatus === null ? "default" : "outline"}
+            size="sm"
+            onClick={() => setSelectedStatus(null)}
+            className="h-9"
+          >
+            전체 ({files.length + requiredDocs.length})
+          </Button>
+          {statusCounts.map((statusCount) => (
+            <Button
+              key={statusCount.status}
+              variant="outline"
+              size="sm"
+              onClick={() => setSelectedStatus(statusCount.status)}
+              className={cn(
+                "h-9",
+                selectedStatus === statusCount.status ? statusCount.color : "",
+                selectedStatus !== statusCount.status && "hover:opacity-80"
+              )}
+            >
+              {statusCount.statusNm} ({statusCount.count})
+            </Button>
+          ))}
+        </div>
+
+        {/* 선택된 파일 정보 및 일괄 취소 버튼 */}
+        {selectedFiles.size > 0 && (
+          <div className="flex items-center gap-2">
+            <span className="text-sm text-muted-foreground">
+              {selectedFiles.size}개 선택됨
+            </span>
+            <Button
+              variant="destructive"
+              size="sm"
+              onClick={handleBulkCancel}
+            >
+              <XCircle className="h-4 w-4 mr-1" />
+              Cancel
+            </Button>
+          </div>
+        )}
+      </div>
+
       {/* 테이블 */}
-      <div className="rounded-md border">
-        <Table>
-          <TableHeader>
-            {table.getHeaderGroups().map((headerGroup) => (
-              <TableRow key={headerGroup.id}>
-                {headerGroup.headers.map((header) => (
-                  <TableHead key={header.id}>
-                    {header.isPlaceholder
-                      ? null
-                      : flexRender(
-                          header.column.columnDef.header,
-                          header.getContext()
-                        )}
-                  </TableHead>
-                ))}
-              </TableRow>
-            ))}
-          </TableHeader>
-          <TableBody>
-            {table.getRowModel().rows?.length ? (
-              table.getRowModel().rows.map((row) => (
-                <TableRow
-                  key={row.id}
-                  data-state={row.getIsSelected() && "selected"}
-                  className="hover:bg-muted/50 cursor-pointer"
-                  onClick={() => handleDocumentClick(row.original)}
-                >
-                  {row.getVisibleCells().map((cell) => (
-                    <TableCell key={cell.id}>
-                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
-                    </TableCell>
-                  ))}
-                </TableRow>
-              ))
-            ) : (
+      {tableRows.length === 0 ? (
+        <div className="border rounded-lg p-8 text-center text-muted-foreground">
+          해당 상태의 파일이 없습니다.
+        </div>
+      ) : (
+        <div className="rounded-md border overflow-x-auto">
+          <Table>
+            <TableHeader>
               <TableRow>
-                <TableCell colSpan={inboxDocumentColumns.length} className="h-24 text-center">
-                  업로드한 파일이 없습니다.
-                </TableCell>
+                <TableHead className="w-[50px]">
+                  <Checkbox
+                    checked={selectableFiles.length > 0 && selectedFiles.size === selectableFiles.length}
+                    onCheckedChange={handleSelectAll}
+                    disabled={selectableFiles.length === 0}
+                  />
+                </TableHead>
+                <TableHead className="w-[100px]">Upload ID</TableHead>
+                <TableHead className="w-[200px]">Document No</TableHead>
+                <TableHead className="w-[80px]">Rev No</TableHead>
+                <TableHead className="w-[80px]">Stage</TableHead>
+                <TableHead className="w-[120px]">Status</TableHead>
+                <TableHead className="w-[100px]">Activity</TableHead>
+                <TableHead className="w-[120px]">Upload ID (User)</TableHead>
+                <TableHead className="w-[150px]">Note</TableHead>
+                <TableHead className="w-[400px]">Attachment File</TableHead>
+                <TableHead className="w-[180px]">Upload Date</TableHead>
               </TableRow>
-            )}
-          </TableBody>
-        </Table>
-      </div>
+            </TableHeader>
+            <TableBody>
+              {tableRows.map((row, idx) => {
+                const fileKey = row.file ? `${row.file.BOX_SEQ}_${row.file.FILE_SEQ}` : `required_${idx}`;
+                const canSelect = row.status === "SCW01" && row.file; // Standby이고 file이 있는 경우만 선택 가능
+                const isSelected = canSelect && selectedFiles.has(fileKey);
+
+                return (
+                  <TableRow 
+                    key={`${row.uploadId}_${row.docNo}_${row.revNo}_${idx}`}
+                    className="cursor-pointer hover:bg-muted/50"
+                    onClick={() => handleRowClick(row.docNo)}
+                  >
+                    {/* Select Checkbox */}
+                    <TableCell onClick={(e) => e.stopPropagation()}>
+                      {canSelect ? (
+                        <Checkbox
+                          checked={!!isSelected}
+                          onCheckedChange={(checked) => handleSelectFile(fileKey, checked)}
+                        />
+                      ) : null}
+                    </TableCell>
+
+                    {/* Upload ID - 같은 Rev의 첫 파일에만 표시 */}
+                    {row.isFirstFileInRev ? (
+                      <TableCell rowSpan={row.fileCountInRev} className="font-mono text-sm align-top">
+                        {row.uploadId || <span className="text-muted-foreground">-</span>}
+                      </TableCell>
+                    ) : null}
+
+                    {/* Document No - 같은 Rev의 첫 파일에만 표시 */}
+                    {row.isFirstFileInRev ? (
+                      <TableCell rowSpan={row.fileCountInRev} className="font-mono text-xs align-top">
+                        {row.docNo}
+                      </TableCell>
+                    ) : null}
+
+                    {/* Rev No - 같은 Rev의 첫 파일에만 표시 */}
+                    {row.isFirstFileInRev ? (
+                      <TableCell rowSpan={row.fileCountInRev} className="align-top">
+                        {row.revNo || <span className="text-muted-foreground">-</span>}
+                      </TableCell>
+                    ) : null}
 
-      {/* 문서 상세 Dialog */}
-      <SwpInboxDocumentDetailDialog
-        open={dialogOpen}
-        onOpenChange={setDialogOpen}
-        document={selectedDocument}
+                    {/* Stage - 같은 Rev의 첫 파일에만 표시 (텍스트로만 표시) */}
+                    {row.isFirstFileInRev ? (
+                      <TableCell rowSpan={row.fileCountInRev} className="align-top text-sm">
+                        {row.stage || <span className="text-muted-foreground">-</span>}
+                      </TableCell>
+                    ) : null}
+
+                    {/* Status - 같은 Rev의 첫 파일에만 표시 */}
+                    {row.isFirstFileInRev ? (
+                      <TableCell rowSpan={row.fileCountInRev} className="align-top">
+                        {getStatusBadge(row.status, row.statusNm)}
+                      </TableCell>
+                    ) : null}
+
+                    {/* Activity - 같은 Rev의 첫 파일에만 표시 */}
+                    {row.isFirstFileInRev ? (
+                      <TableCell rowSpan={row.fileCountInRev} className="font-mono text-xs align-top">
+                        {row.actvNo || <span className="text-muted-foreground">-</span>}
+                      </TableCell>
+                    ) : null}
+
+                    {/* CRTER - 같은 Rev의 첫 파일에만 표시 */}
+                    {row.isFirstFileInRev ? (
+                      <TableCell rowSpan={row.fileCountInRev} className="text-sm font-mono align-top">
+                        {row.crter || <span className="text-muted-foreground">-</span>}
+                      </TableCell>
+                    ) : null}
+
+                    {/* Note - 같은 Rev의 첫 파일에만 표시 (개행문자 처리) */}
+                    {row.isFirstFileInRev ? (
+                      <TableCell 
+                        rowSpan={row.fileCountInRev} 
+                        className="text-xs max-w-[150px] align-top whitespace-pre-wrap"
+                      >
+                        {row.note || <span className="text-muted-foreground">-</span>}
+                      </TableCell>
+                    ) : null}
+
+                    {/* Attachment File - 각 파일마다 표시 (줄바꿈 허용) */}
+                    <TableCell className="max-w-[400px]">
+                      {row.file ? (
+                        <div className="flex items-center gap-2">
+                          <span className="text-sm font-mono break-words" style={{ wordBreak: "break-all" }}>
+                            {row.file.FILE_NM}
+                          </span>
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            onClick={(e) => {
+                              e.stopPropagation();
+                              handleDownloadFile(row.file!);
+                            }}
+                            className="h-7 w-7 p-0 flex-shrink-0"
+                          >
+                            <Download className="h-4 w-4" />
+                          </Button>
+                        </div>
+                      ) : (
+                        <span className="text-muted-foreground">-</span>
+                      )}
+                    </TableCell>
+
+                    {/* Upload Date - 각 파일마다 표시 */}
+                    <TableCell className="text-xs">
+                      {row.uploadDate ? formatSwpDate(row.uploadDate) : <span className="text-muted-foreground">-</span>}
+                    </TableCell>
+                  </TableRow>
+                );
+              })}
+            </TableBody>
+          </Table>
+        </div>
+      )}
+
+      {/* Document 전체 이력 Dialog */}
+      <SwpInboxHistoryDialog
+        open={historyDialogOpen}
+        onOpenChange={setHistoryDialogOpen}
+        docNo={selectedDocNo}
+        files={files}
         projNo={projNo}
-        vendorCode={vendorCode}
         userId={userId}
       />
     </div>
   );
 }
-
-- 
cgit v1.2.3

