From 935fd22e17afc034a472bc2d159de7b9f5e5dcae Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 20 Nov 2025 19:36:01 +0900 Subject: (김준회) PO, POS, swp - PO: 발주서출력기능 초안 - 벤더측 POS 다운로드 기능 추가 - Contract 생성시 Status 설정 (mapper) - swp document registration table 로직 리팩터링 - swp: 입력가능 문서번호 validation 추가 (리스트 메뉴에서 Completed 된 건) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/swp/table/swp-inbox-table.tsx | 459 +++++++++++++++++++++++--------------- 1 file changed, 282 insertions(+), 177 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 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(); - - 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(); + 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(); - - 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({ ) : (
- +
- + 0 && selectedFiles.size === selectableFiles.length} onCheckedChange={handleSelectAll} disabled={selectableFiles.length === 0} /> - Upload ID - Document No - PKG NO - Rev No - Stage - Status - Activity - Upload ID (User) - DC Note - Attachment File - Upload Date + Upload ID + Document No + PKG NO + Rev No + Stage + Status + Activity + Upload ID (User) + DC Note + Attachment File + Upload Date @@ -500,7 +573,7 @@ export function SwpInboxTable({ onClick={() => handleRowClick(row.docNo)} > {/* Select Checkbox */} - e.stopPropagation()}> + e.stopPropagation()}> {canSelect ? ( - {/* Upload ID - Upload의 첫 파일에만 표시 */} - {row.isFirstInUpload ? ( - + {/* Upload ID */} + {row.spans.uploadId > 0 ? ( + {row.uploadId || -} ) : null} - {/* Document No - Document의 첫 파일에만 표시 */} - {row.isFirstInDoc ? ( - + {/* Document No */} + {row.spans.docNo > 0 ? ( + {row.docNo} ) : null} - {/* PKG NO - Document의 첫 파일에만 표시 */} - {row.isFirstInDoc ? ( - + {/* PKG NO */} + {row.spans.pkgNo > 0 ? ( + {row.pkgNo || -} ) : null} - {/* Rev No - Rev의 첫 파일에만 표시 */} - {row.isFirstInRev ? ( - + {/* Rev No */} + {row.spans.revNo > 0 ? ( + {row.revNo || -} ) : null} - {/* Stage - Rev의 첫 파일에만 표시 */} - {row.isFirstInRev ? ( - + {/* Stage */} + {row.spans.stage > 0 ? ( + {row.stage || -} ) : null} - {/* Status - Rev의 첫 파일에만 표시 */} - {row.isFirstInRev ? ( - + {/* Status */} + {row.spans.status > 0 ? ( + {getStatusBadge(row.status, row.statusNm)} ) : null} - {/* Activity - Activity의 첫 파일에만 표시 */} - {row.isFirstInActivity ? ( - + {/* Activity */} + {row.spans.actvNo > 0 ? ( + {row.actvNo || -} ) : null} - {/* CRTER (Upload ID User) - Activity의 첫 파일에만 표시 */} - {row.isFirstInActivity ? ( - + {/* CRTER (Upload ID User) */} + {row.spans.crter > 0 ? ( + {row.crter || -} ) : null} - {/* DC Note - Activity의 첫 파일에만 표시 */} - {row.isFirstInActivity ? ( + {/* DC Note */} + {row.spans.note1 > 0 ? ( { if (row.note1) { @@ -589,8 +694,8 @@ export function SwpInboxTable({ ) : null} - {/* Attachment File - 각 파일마다 표시 (줄바꿈 허용) */} - + {/* Attachment File - 병합하지 않음 */} + {row.file ? (
@@ -613,8 +718,8 @@ export function SwpInboxTable({ )} - {/* Upload Date - 각 파일마다 표시 */} - + {/* Upload Date - 병합하지 않음 */} + {row.uploadDate ? formatSwpDate(row.uploadDate) : -} -- cgit v1.2.3