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.tsx459
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>