summaryrefslogtreecommitdiff
path: root/lib/swp/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/swp/table')
-rw-r--r--lib/swp/table/swp-help-dialog.tsx4
-rw-r--r--lib/swp/table/swp-inbox-table.tsx459
-rw-r--r--lib/swp/table/swp-table-columns.tsx36
-rw-r--r--lib/swp/table/swp-table-toolbar.tsx18
-rw-r--r--lib/swp/table/swp-table.tsx24
5 files changed, 349 insertions, 192 deletions
diff --git a/lib/swp/table/swp-help-dialog.tsx b/lib/swp/table/swp-help-dialog.tsx
index 3aa7d6dc..7b18c100 100644
--- a/lib/swp/table/swp-help-dialog.tsx
+++ b/lib/swp/table/swp-help-dialog.tsx
@@ -37,7 +37,7 @@ export function SwpUploadHelpDialog() {
<div className="rounded-lg border p-4 space-y-3">
<div>
<Badge variant="default" className="mb-2">
- SBOX (ALL) 탭
+ DOCUMENT REGISTRATION TAB
</Badge>
<p className="text-sm text-muted-foreground">
파일을 업로드한 현황입니다. SHI가 파일 접수 여부를 응답할 예정입니다.
@@ -47,7 +47,7 @@ export function SwpUploadHelpDialog() {
<div className="border-t pt-3">
<Badge variant="default" className="mb-2">
- VDR Documents (Received) 탭
+ DOCUMENT LIST TAB
</Badge>
<p className="text-sm text-muted-foreground">
파일을 업로드한 뒤, 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<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>
diff --git a/lib/swp/table/swp-table-columns.tsx b/lib/swp/table/swp-table-columns.tsx
index 14a8e002..91c811c3 100644
--- a/lib/swp/table/swp-table-columns.tsx
+++ b/lib/swp/table/swp-table-columns.tsx
@@ -28,7 +28,9 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [
</Badge>
);
},
- size: 120,
+ size: 150,
+ minSize: 150,
+ maxSize: 150,
},
{
accessorKey: "OWN_DOC_NO",
@@ -37,6 +39,8 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [
<div className="font-mono text-sm">{row.original.OWN_DOC_NO || "-"}</div>
),
size: 250,
+ minSize: 250,
+ maxSize: 250,
},
{
accessorKey: "DOC_NO",
@@ -45,16 +49,20 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [
<div className="font-mono text-sm">{row.original.DOC_NO}</div>
),
size: 250,
+ minSize: 250,
+ maxSize: 250,
},
{
accessorKey: "DOC_TITLE",
header: "문서제목",
cell: ({ row }) => (
- <div className="max-w-md truncate" title={row.original.DOC_TITLE}>
+ <div className="truncate" title={row.original.DOC_TITLE}>
{row.original.DOC_TITLE}
</div>
),
size: 300,
+ minSize: 300,
+ maxSize: 300,
},
{
accessorKey: "PROJ_NO",
@@ -63,19 +71,23 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [
<div>
<div className="font-medium">{row.original.PROJ_NO}</div>
{row.original.PROJ_NM && (
- <div className="text-xs text-muted-foreground max-w-[150px] truncate">
+ <div className="text-xs text-muted-foreground truncate">
{row.original.PROJ_NM}
</div>
)}
</div>
),
size: 150,
+ minSize: 150,
+ maxSize: 150,
},
{
accessorKey: "PKG_NO",
header: "패키지",
cell: ({ row }) => row.original.PKG_NO || "-",
size: 100,
+ minSize: 100,
+ maxSize: 100,
},
{
accessorKey: "VNDR_CD",
@@ -86,13 +98,15 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [
<div className="text-xs text-muted-foreground">{row.original.VNDR_CD}</div>
)}
{row.original.CPY_NM && (
- <div className="text-sm truncate max-w-[120px]" title={row.original.CPY_NM}>
+ <div className="text-sm truncate" title={row.original.CPY_NM}>
{row.original.CPY_NM}
</div>
)}
</div>
),
size: 120,
+ minSize: 120,
+ maxSize: 120,
},
{
accessorKey: "STAGE",
@@ -112,13 +126,17 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [
</Badge>
);
},
- size: 80,
+ size: 100,
+ minSize: 100,
+ maxSize: 100,
},
{
accessorKey: "LTST_REV_NO",
header: "최신 REV",
cell: ({ row }) => row.original.LTST_REV_NO || "-",
- size: 80,
+ size: 100,
+ minSize: 100,
+ maxSize: 100,
},
{
id: "stats",
@@ -136,6 +154,8 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [
</div>
),
size: 100,
+ minSize: 100,
+ maxSize: 100,
},
{
id: "actions",
@@ -221,6 +241,8 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [
</Button>
);
},
- size: 80,
+ size: 180,
+ minSize: 180,
+ maxSize: 180,
},
];
diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx
index 08eda3ef..276eca14 100644
--- a/lib/swp/table/swp-table-toolbar.tsx
+++ b/lib/swp/table/swp-table-toolbar.tsx
@@ -44,6 +44,7 @@ interface SwpTableToolbarProps {
onFilesProcessed?: () => void;
documents?: DocumentListItem[]; // 업로드 권한 검증 + DOC_CLS (Document Class) 확인용 문서 목록
userId?: string; // 파일 취소 시 필요
+ requiredDocs?: Array<{ vendorDocNumber: string; title: string; buyerSystemComment: string | null }>; // DB Completed 상태 업로드 필요 문서
}
export function SwpTableToolbar({
@@ -60,6 +61,7 @@ export function SwpTableToolbar({
onFilesProcessed,
documents = [],
userId,
+ requiredDocs = [],
}: SwpTableToolbarProps) {
const [isUploading, startUpload] = useTransition();
const [localFilters, setLocalFilters] = useState(filters);
@@ -93,9 +95,11 @@ export function SwpTableToolbar({
/**
* 업로드 가능한 문서번호 목록 추출 (OWN_DOC_NO 기준)
* SWP API의 OWN_DOC_NO가 EVCP DB의 vendorDocNumber와 매핑되는지 확인
+ * + DB Completed 상태 문서(requiredDocs)의 vendorDocNumber도 포함
*/
const availableDocNos = useMemo(() => {
- return documents
+ // 1. documents의 OWN_DOC_NO (EVCP DB에 등록된 것만)
+ const fromDocuments = documents
.map(doc => doc.OWN_DOC_NO)
.filter((ownDocNo): ownDocNo is string => {
// OWN_DOC_NO가 있고, EVCP DB에 등록된 문서인지 확인
@@ -103,7 +107,17 @@ export function SwpTableToolbar({
ownDocNo !== undefined &&
vendorDocNumberToDocClassMap[ownDocNo] !== undefined;
});
- }, [documents, vendorDocNumberToDocClassMap]);
+
+ // 2. requiredDocs의 vendorDocNumber (DB Completed 상태)
+ const fromRequiredDocs = requiredDocs
+ .map(doc => doc.vendorDocNumber)
+ .filter((vendorDocNumber): vendorDocNumber is string => {
+ return vendorDocNumber !== null && vendorDocNumber !== undefined;
+ });
+
+ // 3. 중복 제거하여 병합
+ return Array.from(new Set([...fromDocuments, ...fromRequiredDocs]));
+ }, [documents, requiredDocs, vendorDocNumberToDocClassMap]);
/**
* 벤더 모드 여부 (벤더 코드가 있으면 벤더 모드)
diff --git a/lib/swp/table/swp-table.tsx b/lib/swp/table/swp-table.tsx
index b6c3558b..6f810415 100644
--- a/lib/swp/table/swp-table.tsx
+++ b/lib/swp/table/swp-table.tsx
@@ -75,6 +75,8 @@ export function SwpTable({
data: filteredDocuments,
columns: swpDocumentColumns,
getCoreRowModel: getCoreRowModel(),
+ enableColumnResizing: false,
+ columnResizeMode: 'onChange',
});
// 문서 클릭 핸들러 - Dialog 열기
@@ -109,13 +111,20 @@ export function SwpTable({
</div>
{/* 테이블 */}
- <div className="rounded-md border">
- <Table>
+ <div className="rounded-md border overflow-x-auto">
+ <Table className="min-w-[1700px]">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
- <TableHead key={header.id}>
+ <TableHead
+ key={header.id}
+ style={{
+ width: header.column.getSize(),
+ minWidth: header.column.getSize(),
+ maxWidth: header.column.getSize(),
+ }}
+ >
{header.isPlaceholder
? null
: flexRender(
@@ -137,7 +146,14 @@ export function SwpTable({
onClick={() => handleDocumentClick(row.original)}
>
{row.getVisibleCells().map((cell) => (
- <TableCell key={cell.id}>
+ <TableCell
+ key={cell.id}
+ style={{
+ width: cell.column.getSize(),
+ minWidth: cell.column.getSize(),
+ maxWidth: cell.column.getSize(),
+ }}
+ >
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}