파일을 업로드한 현황입니다. SHI가 파일 접수 여부를 응답할 예정입니다.
@@ -47,7 +47,7 @@ export function SwpUploadHelpDialog() {
- VDR Documents (Received) 탭
+ DOCUMENT LIST TAB
파일을 업로드한 뒤, SHI가 업로드한 파일을 수락하면 Rev, Activity No가 만들어지며, 해당 테이블에 추가됩니다.
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({