diff options
Diffstat (limited to 'lib/swp/table/swp-inbox-table.tsx')
| -rw-r--r-- | lib/swp/table/swp-inbox-table.tsx | 459 |
1 files changed, 282 insertions, 177 deletions
diff --git a/lib/swp/table/swp-inbox-table.tsx b/lib/swp/table/swp-inbox-table.tsx index 430447f4..d070f2fd 100644 --- a/lib/swp/table/swp-inbox-table.tsx +++ b/lib/swp/table/swp-inbox-table.tsx @@ -38,29 +38,33 @@ interface SwpInboxTableProps { // 테이블 행 데이터 (플랫하게 펼침) interface TableRowData { - uploadId: string | null; // 업로드 필요 문서는 null + uploadId: string | null; docNo: string; revNo: string | null; stage: string | null; status: string | null; statusNm: string | null; actvNo: string | null; - crter: string | null; // CRTER (그대로 표시) - note1: string | null; // DC Note (Activity의 NOTE1 또는 buyerSystemComment) - pkgNo: string | null; // PKG_NO - file: SwpFileApiResponse | null; // 업로드 필요 문서는 null + actvSeq: string | null; // 정렬용 + crter: string | null; + note1: string | null; + pkgNo: string | null; + file: SwpFileApiResponse | null; uploadDate: string | null; - // 각 행이 속한 그룹의 정보 (계층적 rowSpan 처리) - isFirstInUpload: boolean; - fileCountInUpload: number; - isFirstInDoc: boolean; - fileCountInDoc: number; - isFirstInRev: boolean; - fileCountInRev: number; - isFirstInActivity: boolean; - fileCountInActivity: number; - // 업로드 필요 문서 여부 isRequiredDoc: boolean; + + // Row Span 정보 (0이면 렌더링 안함) + spans: { + uploadId: number; + docNo: number; + pkgNo: number; + revNo: number; + stage: number; + status: number; + actvNo: number; + crter: number; + note1: number; + }; } // Status 집계 타입 @@ -136,117 +140,47 @@ export function SwpInboxTable({ }); }, [files, requiredDocs]); - // 데이터 그룹화 및 플랫 변환 (API 응답 + 업로드 필요 문서) + // 데이터 그룹화 및 플랫 변환 (Sorting logic applied) const tableRows = useMemo(() => { const rows: TableRowData[] = []; // 1. API 응답 파일 처리 - // Status 필터링 - let filteredFiles = files; - if (selectedStatus && selectedStatus !== "UPLOAD_REQUIRED") { - filteredFiles = files.filter((file) => file.STAT === selectedStatus); - } - - // 1단계: BOX_SEQ (Upload ID) 기준으로 그룹화 - 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); - }); - } - - // Upload ID별로 처리 - uploadGroups.forEach((uploadFiles, uploadId) => { - // 2단계: Document No 기준으로 그룹화 - const docGroups = new Map<string, SwpFileApiResponse[]>(); + files.forEach((file) => { + // Status 필터링 + if (selectedStatus && file.STAT !== selectedStatus) { + return; + } - uploadFiles.forEach((file) => { - const docNo = file.OWN_DOC_NO; - if (!docGroups.has(docNo)) { - docGroups.set(docNo, []); + rows.push({ + uploadId: file.BOX_SEQ || null, + docNo: file.OWN_DOC_NO, + revNo: file.REV_NO || null, + stage: file.STAGE || null, + status: file.STAT || null, + statusNm: file.STAT_NM || null, + actvNo: file.ACTV_NO || null, + actvSeq: file.ACTV_SEQ || null, + crter: file.CRTER || null, + note1: file.NOTE1 || null, + pkgNo: file.PKG_NO || null, + file, + uploadDate: file.CRTE_DTM || null, + isRequiredDoc: false, + spans: { + uploadId: 1, + docNo: 1, + pkgNo: 1, + revNo: 1, + stage: 1, + status: 1, + actvNo: 1, + crter: 1, + note1: 1, } - docGroups.get(docNo)!.push(file); - }); - - // 전체 Upload ID의 파일 수 계산 - const totalUploadFileCount = uploadFiles.length; - let isFirstInUpload = true; - - // Document No별로 처리 - docGroups.forEach((docFiles, docNo) => { - const totalDocFileCount = docFiles.length; - let isFirstInDoc = true; - - // Document의 첫 번째 파일에서 PKG_NO 가져오기 - const firstDocFile = docFiles[0]; - const docPkgNo = firstDocFile?.PKG_NO || null; - - // 3단계: ACTV_SEQ 기준으로 그룹화 (최신 Rev 필터링 제거) - const activityGroups = new Map<string, SwpFileApiResponse[]>(); - - docFiles.forEach((file) => { - const actvSeq = file.ACTV_SEQ || "NO_ACTIVITY"; - if (!activityGroups.has(actvSeq)) { - activityGroups.set(actvSeq, []); - } - activityGroups.get(actvSeq)!.push(file); - }); - - // Activity별로 처리 - activityGroups.forEach((activityFiles) => { - // 4단계: Upload Date 기준 DESC 정렬 - const sortedFiles = activityFiles.sort((a, b) => - (b.CRTE_DTM || "").localeCompare(a.CRTE_DTM || "") - ); - - const totalActivityFileCount = sortedFiles.length; - - // Activity의 첫 번째 파일에서 메타데이터 가져오기 - const firstActivityFile = sortedFiles[0]; - if (!firstActivityFile) return; - - // 5단계: 각 파일을 테이블 행으로 변환 - sortedFiles.forEach((file, idx) => { - rows.push({ - uploadId, - docNo, - revNo: file.REV_NO || null, - stage: file.STAGE || null, - status: file.STAT || null, - statusNm: file.STAT_NM || null, - actvNo: file.ACTV_NO || null, - crter: firstActivityFile.CRTER, // Activity 첫 파일의 CRTER - note1: firstActivityFile.NOTE1 || null, // Activity 첫 파일의 DC Note - pkgNo: docPkgNo, // Document 레벨의 PKG_NO - file, - uploadDate: file.CRTE_DTM, - isFirstInUpload, - fileCountInUpload: totalUploadFileCount, - isFirstInDoc, - fileCountInDoc: totalDocFileCount, - isFirstInRev: idx === 0, - fileCountInRev: totalActivityFileCount, // Activity 내 파일 수 - isFirstInActivity: idx === 0, - fileCountInActivity: totalActivityFileCount, - isRequiredDoc: false, - }); - - // 첫 번째 플래그들 업데이트 - if (idx === 0) { - isFirstInUpload = false; - isFirstInDoc = false; - } - }); - }); }); }); - // 2. 업로드 필요 문서 추가 (Upload Required 필터일 때만 또는 필터 없을 때) + // 2. 업로드 필요 문서 처리 if (!selectedStatus || selectedStatus === "UPLOAD_REQUIRED") { requiredDocs.forEach((doc) => { rows.push({ @@ -257,30 +191,169 @@ export function SwpInboxTable({ status: "UPLOAD_REQUIRED", statusNm: "Upload Required", actvNo: null, + actvSeq: null, crter: null, - note1: doc.buyerSystemComment, // DB의 comment를 DC Note에 매핑 + note1: doc.buyerSystemComment, pkgNo: null, file: null, uploadDate: null, - isFirstInUpload: true, - fileCountInUpload: 1, - isFirstInDoc: true, - fileCountInDoc: 1, - isFirstInRev: true, - fileCountInRev: 1, - isFirstInActivity: true, - fileCountInActivity: 1, isRequiredDoc: true, + spans: { + uploadId: 1, + docNo: 1, + pkgNo: 1, + revNo: 1, + stage: 1, + status: 1, + actvNo: 1, + crter: 1, + note1: 1, + } }); }); } - // Upload Date 기준 전체 정렬 (null은 맨 뒤로) - return rows.sort((a, b) => { - if (!a.uploadDate) return 1; - if (!b.uploadDate) return -1; - return b.uploadDate.localeCompare(a.uploadDate); + // 3. 정렬 적용 + // 1) BOX_SEQ (DESC) -> 2) OWN_DOC_NO (DESC) -> 3) REV_NO (DESC) -> 4) ACTV_SEQ (DESC) -> 5) CRTE_DTM (DESC) + rows.sort((a, b) => { + // 1) BOX_SEQ + const uploadIdA = a.uploadId || ""; + const uploadIdB = b.uploadId || ""; + if (uploadIdA !== uploadIdB) { + return uploadIdB.localeCompare(uploadIdA); + } + + // 2) OWN_DOC_NO + const docNoA = a.docNo || ""; + const docNoB = b.docNo || ""; + if (docNoA !== docNoB) { + return docNoB.localeCompare(docNoA); + } + + // 3) REV_NO + const revNoA = a.revNo || ""; + const revNoB = b.revNo || ""; + if (revNoA !== revNoB) { + return revNoB.localeCompare(revNoA); + } + + // 4) ACTV_SEQ + const actvSeqA = a.actvSeq || ""; + const actvSeqB = b.actvSeq || ""; + if (actvSeqA !== actvSeqB) { + return actvSeqB.localeCompare(actvSeqA); + } + + // 5) CRTE_DTM + const dateA = a.uploadDate || ""; + const dateB = b.uploadDate || ""; + return dateB.localeCompare(dateA); }); + + // 4. Row Span 계산 (간단 로직: 위와 같으면 합침) + // 정렬된 상태에서 위에서 아래로 내려가며, 이전 행과 값이 같으면 현재 행의 span을 0으로 만들고 leader 행의 span을 증가시킴 + + // 각 컬럼별 리더 행의 인덱스를 추적 + const leaders = { + uploadId: 0, + docNo: 0, + pkgNo: 0, + revNo: 0, + stage: 0, + status: 0, + actvNo: 0, + crter: 0, + note1: 0, + }; + + for (let i = 1; i < rows.length; i++) { + const prev = rows[i - 1]; + const curr = rows[i]; + + // 1) Upload ID (최상위) + const mergeUploadId = curr.uploadId === prev.uploadId; + if (mergeUploadId) { + rows[leaders.uploadId].spans.uploadId++; + curr.spans.uploadId = 0; + } else { + leaders.uploadId = i; + } + + // 2) Document No (Upload ID에 종속) + const mergeDocNo = mergeUploadId && (curr.docNo === prev.docNo); + if (mergeDocNo) { + rows[leaders.docNo].spans.docNo++; + curr.spans.docNo = 0; + } else { + leaders.docNo = i; + } + + // 3) PKG NO (Document에 종속) + const mergePkgNo = mergeDocNo && (curr.pkgNo === prev.pkgNo); + if (mergePkgNo) { + rows[leaders.pkgNo].spans.pkgNo++; + curr.spans.pkgNo = 0; + } else { + leaders.pkgNo = i; + } + + // 4) Rev No (Document에 종속) + const mergeRevNo = mergeDocNo && (curr.revNo === prev.revNo); + if (mergeRevNo) { + rows[leaders.revNo].spans.revNo++; + curr.spans.revNo = 0; + } else { + leaders.revNo = i; + } + + // 5) Stage (Rev No에 종속) + const mergeStage = mergeRevNo && (curr.stage === prev.stage); + if (mergeStage) { + rows[leaders.stage].spans.stage++; + curr.spans.stage = 0; + } else { + leaders.stage = i; + } + + // 6) Activity (Rev No에 종속 - 보통 Activity는 Rev 안에 있음) + const mergeActvNo = mergeRevNo && (curr.actvSeq === prev.actvSeq); + if (mergeActvNo) { + rows[leaders.actvNo].spans.actvNo++; + curr.spans.actvNo = 0; + } else { + leaders.actvNo = i; + } + + // 7) Status (Activity 내 파일들 - Activity에 종속) + // Activity가 같고 Status 값도 같으면 합침 + const mergeStatus = mergeActvNo && (curr.status === prev.status); + if (mergeStatus) { + rows[leaders.status].spans.status++; + curr.spans.status = 0; + } else { + leaders.status = i; + } + + // 8) CRTER (Activity에 종속) + const mergeCrter = mergeActvNo && (curr.crter === prev.crter); + if (mergeCrter) { + rows[leaders.crter].spans.crter++; + curr.spans.crter = 0; + } else { + leaders.crter = i; + } + + // 9) DC Note (Activity에 종속) + const mergeNote1 = mergeActvNo && (curr.note1 === prev.note1); + if (mergeNote1) { + rows[leaders.note1].spans.note1++; + curr.spans.note1 = 0; + } else { + leaders.note1 = i; + } + } + + return rows; }, [files, requiredDocs, selectedStatus]); // 선택 가능한 파일들 (Standby 상태만) @@ -464,27 +537,27 @@ export function SwpInboxTable({ </div> ) : ( <div className="rounded-md border overflow-x-auto"> - <Table> + <Table className="min-w-[1700px]"> <TableHeader> <TableRow> - <TableHead className="w-[50px]"> + <TableHead className="w-[50px] min-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-[120px]">PKG 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]">DC Note</TableHead> - <TableHead className="w-[400px]">Attachment File</TableHead> - <TableHead className="w-[180px]">Upload Date</TableHead> + <TableHead className="w-[100px] min-w-[100px]">Upload ID</TableHead> + <TableHead className="w-[200px] min-w-[200px]">Document No</TableHead> + <TableHead className="w-[120px] min-w-[120px]">PKG NO</TableHead> + <TableHead className="w-[80px] min-w-[80px]">Rev No</TableHead> + <TableHead className="w-[80px] min-w-[80px]">Stage</TableHead> + <TableHead className="w-[120px] min-w-[120px]">Status</TableHead> + <TableHead className="w-[100px] min-w-[100px]">Activity</TableHead> + <TableHead className="w-[120px] min-w-[120px]">Upload ID (User)</TableHead> + <TableHead className="w-[150px] min-w-[150px]">DC Note</TableHead> + <TableHead className="w-[800px] min-w-[400px]">Attachment File</TableHead> + <TableHead className="w-[180px] min-w-[180px]">Upload Date</TableHead> </TableRow> </TableHeader> <TableBody> @@ -500,7 +573,7 @@ export function SwpInboxTable({ onClick={() => handleRowClick(row.docNo)} > {/* Select Checkbox */} - <TableCell onClick={(e) => e.stopPropagation()}> + <TableCell className="w-[50px] min-w-[50px]" onClick={(e) => e.stopPropagation()}> {canSelect ? ( <Checkbox checked={!!isSelected} @@ -509,67 +582,99 @@ export function SwpInboxTable({ ) : null} </TableCell> - {/* Upload ID - Upload의 첫 파일에만 표시 */} - {row.isFirstInUpload ? ( - <TableCell rowSpan={row.fileCountInUpload} className="font-mono text-sm align-top" style={{ verticalAlign: "top" }}> + {/* Upload ID */} + {row.spans.uploadId > 0 ? ( + <TableCell + rowSpan={row.spans.uploadId} + className="w-[100px] min-w-[100px] font-mono text-sm align-top" + style={{ verticalAlign: "top" }} + > {row.uploadId || <span className="text-muted-foreground">-</span>} </TableCell> ) : null} - {/* Document No - Document의 첫 파일에만 표시 */} - {row.isFirstInDoc ? ( - <TableCell rowSpan={row.fileCountInDoc} className="font-mono text-xs align-top" style={{ verticalAlign: "top" }}> + {/* Document No */} + {row.spans.docNo > 0 ? ( + <TableCell + rowSpan={row.spans.docNo} + className="w-[200px] min-w-[200px] font-mono text-xs align-top" + style={{ verticalAlign: "top" }} + > {row.docNo} </TableCell> ) : null} - {/* PKG NO - Document의 첫 파일에만 표시 */} - {row.isFirstInDoc ? ( - <TableCell rowSpan={row.fileCountInDoc} className="font-mono text-sm align-top" style={{ verticalAlign: "top" }}> + {/* PKG NO */} + {row.spans.pkgNo > 0 ? ( + <TableCell + rowSpan={row.spans.pkgNo} + className="w-[120px] min-w-[120px] font-mono text-sm align-top" + style={{ verticalAlign: "top" }} + > {row.pkgNo || <span className="text-muted-foreground">-</span>} </TableCell> ) : null} - {/* Rev No - Rev의 첫 파일에만 표시 */} - {row.isFirstInRev ? ( - <TableCell rowSpan={row.fileCountInRev} className="align-top" style={{ verticalAlign: "top" }}> + {/* Rev No */} + {row.spans.revNo > 0 ? ( + <TableCell + rowSpan={row.spans.revNo} + className="w-[80px] min-w-[80px] align-top" + style={{ verticalAlign: "top" }} + > {row.revNo || <span className="text-muted-foreground">-</span>} </TableCell> ) : null} - {/* Stage - Rev의 첫 파일에만 표시 */} - {row.isFirstInRev ? ( - <TableCell rowSpan={row.fileCountInRev} className="align-top text-sm" style={{ verticalAlign: "top" }}> + {/* Stage */} + {row.spans.stage > 0 ? ( + <TableCell + rowSpan={row.spans.stage} + className="w-[80px] min-w-[80px] align-top text-sm" + style={{ verticalAlign: "top" }} + > {row.stage || <span className="text-muted-foreground">-</span>} </TableCell> ) : null} - {/* Status - Rev의 첫 파일에만 표시 */} - {row.isFirstInRev ? ( - <TableCell rowSpan={row.fileCountInRev} className="align-top" style={{ verticalAlign: "top" }}> + {/* Status */} + {row.spans.status > 0 ? ( + <TableCell + rowSpan={row.spans.status} + className="w-[120px] min-w-[120px] align-top" + style={{ verticalAlign: "top" }} + > {getStatusBadge(row.status, row.statusNm)} </TableCell> ) : null} - {/* Activity - Activity의 첫 파일에만 표시 */} - {row.isFirstInActivity ? ( - <TableCell rowSpan={row.fileCountInActivity} className="font-mono text-xs align-top" style={{ verticalAlign: "top" }}> + {/* Activity */} + {row.spans.actvNo > 0 ? ( + <TableCell + rowSpan={row.spans.actvNo} + className="w-[100px] min-w-[100px] font-mono text-xs align-top" + style={{ verticalAlign: "top" }} + > {row.actvNo || <span className="text-muted-foreground">-</span>} </TableCell> ) : null} - {/* CRTER (Upload ID User) - Activity의 첫 파일에만 표시 */} - {row.isFirstInActivity ? ( - <TableCell rowSpan={row.fileCountInActivity} className="text-sm font-mono align-top" style={{ verticalAlign: "top" }}> + {/* CRTER (Upload ID User) */} + {row.spans.crter > 0 ? ( + <TableCell + rowSpan={row.spans.crter} + className="w-[120px] min-w-[120px] text-sm font-mono align-top" + style={{ verticalAlign: "top" }} + > {row.crter || <span className="text-muted-foreground">-</span>} </TableCell> ) : null} - {/* DC Note - Activity의 첫 파일에만 표시 */} - {row.isFirstInActivity ? ( + {/* DC Note */} + {row.spans.note1 > 0 ? ( <TableCell - rowSpan={row.fileCountInActivity} - className="text-xs max-w-[150px] align-top" + rowSpan={row.spans.note1} + className="w-[150px] min-w-[150px] text-xs align-top" style={{ verticalAlign: "top" }} onClick={(e) => { if (row.note1) { @@ -589,8 +694,8 @@ export function SwpInboxTable({ </TableCell> ) : null} - {/* Attachment File - 각 파일마다 표시 (줄바꿈 허용) */} - <TableCell className="max-w-[400px]"> + {/* Attachment File - 병합하지 않음 */} + <TableCell className="w-[400px] min-w-[400px]"> {row.file ? ( <div className="flex items-center justify-between gap-2"> <span className="text-sm font-mono break-words" style={{ wordBreak: "break-all" }}> @@ -613,8 +718,8 @@ export function SwpInboxTable({ )} </TableCell> - {/* Upload Date - 각 파일마다 표시 */} - <TableCell className="text-xs"> + {/* Upload Date - 병합하지 않음 */} + <TableCell className="w-[180px] min-w-[180px] text-xs"> {row.uploadDate ? formatSwpDate(row.uploadDate) : <span className="text-muted-foreground">-</span>} </TableCell> </TableRow> |
