summaryrefslogtreecommitdiff
path: root/lib/swp/table/swp-inbox-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/swp/table/swp-inbox-table.tsx')
-rw-r--r--lib/swp/table/swp-inbox-table.tsx703
1 files changed, 514 insertions, 189 deletions
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>
);
}
-