From de4c6593f0cc91c6e0c1a4e2bf9581f11f4f5c98 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Sun, 2 Nov 2025 14:03:34 +0900 Subject: (김준회) SWP 리스트 관리 파트 오류 수정 및 요구사항 반영, 동적 상태 리스트 필터링 변경, null은 동기화 전(전송 전)으로 간주, 선택된 것만 보내도록 변경 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plant/document-stage-dialogs.tsx | 36 +-- .../plant/document-stage-toolbar.tsx | 14 +- .../plant/document-stages-columns.tsx | 3 +- .../plant/document-stages-service.ts | 4 +- .../plant/document-stages-table.tsx | 244 ++++++++++----------- .../plant/excel-import-stage.tsx | 26 +-- .../plant/shi-buyer-system-api.ts | 37 ++-- 7 files changed, 174 insertions(+), 190 deletions(-) (limited to 'lib/vendor-document-list') diff --git a/lib/vendor-document-list/plant/document-stage-dialogs.tsx b/lib/vendor-document-list/plant/document-stage-dialogs.tsx index 8a7dcbc4..6c8fa797 100644 --- a/lib/vendor-document-list/plant/document-stage-dialogs.tsx +++ b/lib/vendor-document-list/plant/document-stage-dialogs.tsx @@ -92,7 +92,6 @@ const getStatusText = (status: string) => { // Form validation schema const documentFormSchema = z.object({ documentClassId: z.string().min(1, "Document class is required"), - docClass: z.string().min(1, "Document class is required"), title: z.string().min(1, "Document title is required"), shiFieldValues: z.record(z.string()).optional(), cpyFieldValues: z.record(z.string()).optional(), @@ -133,7 +132,6 @@ export function AddDocumentDialog({ resolver: zodResolver(documentFormSchema), defaultValues: { documentClassId: '', - docClass: '', title: '', shiFieldValues: {}, cpyFieldValues: {}, @@ -376,11 +374,15 @@ export function AddDocumentDialog({ const shiDocNumber = shiType ? generateShiPreview() : '' const cpyDocNumber = cpyType ? generateCpyPreview() : '' + // 선택된 Document Class의 code 값을 가져오기 (SWP API의 DOC_CLASS로 사용) + const selectedDocClass = documentClasses.find(cls => String(cls.id) === data.documentClassId) + const docClassCode = selectedDocClass?.code || '' + try { const submitData = { contractId, documentClassId: Number(data.documentClassId), - docClass: data.docClass, + docClass: docClassCode, // 첫 번째 선택기의 code 값 사용 (A, B, C 등) title: data.title, docNumber: shiDocNumber, vendorDocNumber: cpyDocNumber, @@ -618,34 +620,6 @@ export function AddDocumentDialog({ )} - {/* Document Class Selection (B3, B4, B5) */} -
- - ( - - )} - /> - {form.formState.errors.docClass && ( -

- {form.formState.errors.docClass.message} -

- )} -
- {/* Document Class Options with Plan Dates */} {documentClassOptions.length > 0 && ( diff --git a/lib/vendor-document-list/plant/document-stage-toolbar.tsx b/lib/vendor-document-list/plant/document-stage-toolbar.tsx index 51767528..4a0b32c8 100644 --- a/lib/vendor-document-list/plant/document-stage-toolbar.tsx +++ b/lib/vendor-document-list/plant/document-stage-toolbar.tsx @@ -56,14 +56,24 @@ export function DocumentsTableToolbarActions({ }) async function handleSendToSHI() { + // 선택된 문서 가져오기 + const selectedRows = table.getFilteredSelectedRowModel().rows + const selectedDocumentIds = selectedRows.map(row => row.original.documentId) + + if (selectedDocumentIds.length === 0) { + toast.error("전송할 문서를 선택해주세요.") + return + } + setIsSending(true) try { - const result = await sendDocumentsToSHI(contractId) + const result = await sendDocumentsToSHI(contractId, selectedDocumentIds) if (result.success) { toast.success(result.message) + // 선택 해제 + table.toggleAllRowsSelected(false) router.refresh() - // 테이블 새로고침 } else { toast.error(result.message) } diff --git a/lib/vendor-document-list/plant/document-stages-columns.tsx b/lib/vendor-document-list/plant/document-stages-columns.tsx index f9cde264..c74f2d71 100644 --- a/lib/vendor-document-list/plant/document-stages-columns.tsx +++ b/lib/vendor-document-list/plant/document-stages-columns.tsx @@ -357,10 +357,11 @@ export function getDocumentStagesColumns({ ), cell: ({ row }) => { const doc = row.original + const displayStatus = doc.buyerSystemStatus || 'Before Sync' return (
- {getStatusText(doc.buyerSystemStatus || '')} + {displayStatus}
) }, diff --git a/lib/vendor-document-list/plant/document-stages-service.ts b/lib/vendor-document-list/plant/document-stages-service.ts index ae9ea314..47bc6ff8 100644 --- a/lib/vendor-document-list/plant/document-stages-service.ts +++ b/lib/vendor-document-list/plant/document-stages-service.ts @@ -1165,10 +1165,10 @@ export async function getDocumentsByStageStats(contractId: number) { } // Send to SHI 버튼 핸들러가 이 함수 호출 -export async function sendDocumentsToSHI(contractId: number) { +export async function sendDocumentsToSHI(contractId: number, selectedDocumentIds?: number[]) { try { const api = new ShiBuyerSystemAPI() - const result = await api.sendToSHI(contractId) + const result = await api.sendToSHI(contractId, selectedDocumentIds) // 캐시 무효화 revalidatePath(`/partners/document-list-only/${contractId}`) diff --git a/lib/vendor-document-list/plant/document-stages-table.tsx b/lib/vendor-document-list/plant/document-stages-table.tsx index 63f0eae6..cd23db2d 100644 --- a/lib/vendor-document-list/plant/document-stages-table.tsx +++ b/lib/vendor-document-list/plant/document-stages-table.tsx @@ -11,8 +11,8 @@ import { useDataTable } from "@/hooks/use-data-table" import { getDocumentStagesOnly } from "./document-stages-service" import type { StageDocumentsView } from "@/db/schema" import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" -import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { cn } from "@/lib/utils" import { FileText, Send, @@ -62,7 +62,7 @@ export function DocumentStagesTable({ // 상태 관리 const [rowAction, setRowAction] = React.useState | null>(null) const [expandedRows, setExpandedRows] = React.useState>(new Set()) - const [quickFilter, setQuickFilter] = React.useState<'all' | 'submitted' | 'under_review' | 'approved' | 'rejected'>('all') + const [quickFilter, setQuickFilter] = React.useState('all') // 다이얼로그 상태들 const [addDocumentOpen, setAddDocumentOpen] = React.useState(false) @@ -115,44 +115,88 @@ export function DocumentStagesTable({ [expandedRows, projectType, currentDomain] ) - // 문서 상태별 통계 계산 + // 문서 상태별 통계 계산 (동적) - buyerSystemStatus 기준 const stats = React.useMemo(() => { const totalDocs = data?.length || 0 - const submitted = data?.filter(doc => doc.status === 'SUBMITTED')?.length || 0 - const underReview = data?.filter(doc => doc.status === 'UNDER_REVIEW')?.length || 0 - const approved = data?.filter(doc => doc.status === 'APPROVED')?.length || 0 - const rejected = data?.filter(doc => doc.status === 'REJECTED')?.length || 0 - const notSubmitted = data?.filter(doc => - !doc.status || !['SUBMITTED', 'UNDER_REVIEW', 'APPROVED', 'REJECTED'].includes(doc.status) - )?.length || 0 + + // 모든 고유한 buyerSystemStatus 값 추출 + const statusCounts = new Map() + data?.forEach(doc => { + // buyerSystemStatus가 null이면 'beforeSync'로 처리 + const status = doc.buyerSystemStatus || 'beforeSync' + statusCounts.set(status, (statusCounts.get(status) || 0) + 1) + }) return { total: totalDocs, - submitted, - underReview, - approved, - rejected, - notSubmitted, - approvalRate: totalDocs > 0 - ? Math.round((approved / totalDocs) * 100) - : 0 + statusCounts, } }, [data]) - // 빠른 필터링 + // 상태별 메타 정보 (아이콘, 색상, 레이블) - buyerSystemStatus 기준 + const getStatusMeta = (status: string) => { + const metaMap: Record = { + 'beforeSync': { + icon: FileText, + color: 'border-gray-200 dark:border-gray-800', + textColor: 'text-gray-600 dark:text-gray-400', + label: 'Before Sync', + description: '동기화 전' + }, + '생성요청': { + icon: Send, + color: 'border-blue-200 dark:border-blue-800', + textColor: 'text-blue-600 dark:text-blue-400', + label: '생성요청', + description: 'SHI에 생성 요청됨' + }, + '검토중': { + icon: Search, + color: 'border-orange-200 dark:border-orange-800', + textColor: 'text-orange-600 dark:text-orange-400', + label: '검토중', + description: 'SHI 검토 진행중' + }, + '승인(DC)': { + icon: CheckCircle2, + color: 'border-green-200 dark:border-green-800', + textColor: 'text-green-600 dark:text-green-400', + label: '승인(DC)', + description: 'SHI 승인 완료' + }, + '반려': { + icon: XCircle, + color: 'border-red-200 dark:border-red-800', + textColor: 'text-red-600 dark:text-red-400', + label: '반려', + description: '재작업 필요' + } + } + + return metaMap[status] || { + icon: FileText, + color: 'border-gray-200 dark:border-gray-800', + textColor: 'text-gray-600 dark:text-gray-400', + label: status, + description: status + } + } + + // 빠른 필터링 - buyerSystemStatus 기준 const filteredData = React.useMemo(() => { - switch (quickFilter) { - case 'submitted': - return data.filter(doc => doc.status === 'SUBMITTED') - case 'under_review': - return data.filter(doc => doc.status === 'UNDER_REVIEW') - case 'approved': - return data.filter(doc => doc.status === 'APPROVED') - case 'rejected': - return data.filter(doc => doc.status === 'REJECTED') - default: - return data + if (quickFilter === 'all') { + return data } + return data.filter(doc => { + const status = doc.buyerSystemStatus || 'beforeSync' + return status === quickFilter + }) }, [data, quickFilter]) // 핸들러 함수들 @@ -231,112 +275,60 @@ export function DocumentStagesTable({ return (
- {/* 문서 상태 대시보드 */} + {/* 문서 상태 대시보드 - 동적 생성 */}
- {/* 전체 문서 */} - setQuickFilter('all')}> + {/* 전체 문서 카드 (항상 첫 번째) */} + setQuickFilter('all')} + > Total Documents
{stats.total}
-

- 전체 등록 문서 -

-
-
- - {/* 제출됨 */} - setQuickFilter('submitted')}> - - Submitted - - - -
{stats.submitted}
-

제출 대기중

-
-
- - {/* 검토중 */} - {/* setQuickFilter('under_review')}> - - Under Review - - - -
{stats.underReview}
-

검토 진행중

-
-
*/} - - {/* 승인됨 */} - setQuickFilter('approved')}> - - Approved - - - -
{stats.approved}
-

승인 완료 ({stats.approvalRate}%)

+

전체 등록 문서

- setQuickFilter('rejected')}> - - Rejected - - - -
{stats.rejected}
-

재작업 필요

-
-
-
- - {/* 빠른 필터 뱃지 */} -
- setQuickFilter('all')} - > - 전체 ({stats.total}) - - setQuickFilter('submitted')} - > - - 제출됨 ({stats.submitted}) - - setQuickFilter('under_review')} - > - - 검토중 ({stats.underReview}) - - setQuickFilter('approved')} - > - - 승인됨 ({stats.approved}) - - setQuickFilter('rejected')} - > - - 반려됨 ({stats.rejected}) - + {/* 동적으로 생성된 상태별 카드 */} + {Array.from(stats.statusCounts.entries()) + .sort(([a], [b]) => a.localeCompare(b)) // 알파벳 순 정렬 + .map(([status, count]) => { + const meta = getStatusMeta(status) + const IconComponent = meta.icon + const isSelected = quickFilter === status + + return ( + setQuickFilter(status)} + > + + {meta.label} + + + +
{count}
+

{meta.description}

+
+
+ ) + }) + }
{/* 메인 테이블 */} diff --git a/lib/vendor-document-list/plant/excel-import-stage.tsx b/lib/vendor-document-list/plant/excel-import-stage.tsx index 8dc85c51..53e12eeb 100644 --- a/lib/vendor-document-list/plant/excel-import-stage.tsx +++ b/lib/vendor-document-list/plant/excel-import-stage.tsx @@ -326,10 +326,10 @@ function FileFormatGuide({ projectType }: { projectType: "ship" | "plant" }) { 통합 Documents 시트:

    -
  • Document Number* (문서번호)
  • +
  • SHI Document Number* (SHI 문서번호)
  • Document Name* (문서명)
  • Document Class* (문서클래스 - 드롭다운 선택)
  • -
  • Project Doc No.* (프로젝트 문서번호)
  • +
  • Company / Owner Document Number (회사/소유주 문서번호)
  • 각 Stage Name 컬럼 (계획날짜 입력: YYYY-MM-DD)

@@ -440,10 +440,10 @@ async function createImportTemplate(projectType: "ship" | "plant", contractId: n const worksheet = workbook.addWorksheet("Documents") const headers = [ - "Document Number*", + "SHI Document Number*", "Document Name*", "Document Class*", - ...(projectType === "plant" ? ["Project Doc No.*"] : []), + ...(projectType === "plant" ? ["Company / Owner Document Number"] : []), ...allStageNames, ] const headerRow = worksheet.addRow(headers) @@ -466,7 +466,7 @@ async function createImportTemplate(projectType: "ship" | "plant", contractId: n ] worksheet.addRow(sampleRow) - const docNumberColIndex = 1; // A: Document Number* + const docNumberColIndex = 1; // A: SHI Document Number* const docNameColIndex = 2; // B: Document Name* const docNumberColLetter = getExcelColumnName(docNumberColIndex); const docNameColLetter = getExcelColumnName(docNameColIndex); @@ -569,7 +569,7 @@ worksheet.addConditionalFormatting({ ], }); -// ===== Project Doc No.* (Plant 전용): (이미 작성하신 코드 유지) ===== +// ===== Company / Owner Document Number (Plant 전용): (이미 작성하신 코드 유지) ===== if (projectType === "plant") { const vendorDocColIndex = 4; // D const vendorDocColLetter = getExcelColumnName(vendorDocColIndex); @@ -581,7 +581,7 @@ if (projectType === "plant") { allowBlank: false, showErrorMessage: true, errorTitle: "필수 입력", - error: "Project Doc No.는 필수 항목입니다.", + error: "Company / Owner Document Number는 필수 항목입니다.", }); worksheet.addConditionalFormatting({ @@ -608,7 +608,7 @@ if (projectType === "plant") { } if (projectType === "plant") { - const vendorDocColIndex = 4; // Document Number, Name, Class 다음이 Project Doc No.* + const vendorDocColIndex = 4; // Document Number, Name, Class 다음이 Company / Owner Document Number const vendorDocColLetter = getExcelColumnName(vendorDocColIndex); // 공백 불가: 글자수 > 0 @@ -619,7 +619,7 @@ if (projectType === "plant") { allowBlank: false, showErrorMessage: true, errorTitle: "필수 입력", - error: "Project Doc No.는 필수 항목입니다.", + error: "Company / Owner Document Number는 필수 항목입니다.", }); // UX: 비어있으면 빨간 배경으로 표시 (조건부 서식) @@ -673,10 +673,10 @@ if (projectType === "plant") { ["📋 통합 문서 임포트 가이드"], [""], ["1. 하나의 시트에서 모든 정보 관리"], - [" • Document Number*: 고유한 문서 번호"], + [" • SHI Document Number*: 고유한 문서 번호"], [" • Document Name*: 문서명"], [" • Document Class*: 드롭다운에서 선택"], - ...(projectType === "plant" ? [[" • Project Doc No.: 벤더 문서 번호"]] : []), + ...(projectType === "plant" ? [[" • Company / Owner Document Number: 벤더 문서 번호"]] : []), [" • Stage 컬럼들: 각 스테이지의 계획 날짜 (YYYY-MM-DD)"], [""], ["2. 스마트 검증 기능"], @@ -820,7 +820,7 @@ async function parseDocumentsWithStages( const docNumberIdx = headers.findIndex((h) => h.includes("Document Number")) const docNameIdx = headers.findIndex((h) => h.includes("Document Name")) const docClassIdx = headers.findIndex((h) => h.includes("Document Class")) - const vendorDocNoIdx = projectType === "plant" ? headers.findIndex((h) => h.includes("Project Doc No")) : -1 + const vendorDocNoIdx = projectType === "plant" ? headers.findIndex((h) => h.includes("Company / Owner Document Number")) : -1 if (docNumberIdx === -1 || docNameIdx === -1 || docClassIdx === -1) { errors.push("필수 헤더가 누락되었습니다") @@ -852,7 +852,7 @@ async function parseDocumentsWithStages( return } if (projectType === "plant" && !vendorDocNo) { - errors.push(`행 ${rowNumber}: Project Doc No.가 없습니다`) + errors.push(`행 ${rowNumber}: Company / Owner Document Number가 없습니다`) return } if (seenDocNumbers.has(docNumber)) { diff --git a/lib/vendor-document-list/plant/shi-buyer-system-api.ts b/lib/vendor-document-list/plant/shi-buyer-system-api.ts index b23bd269..21f28fac 100644 --- a/lib/vendor-document-list/plant/shi-buyer-system-api.ts +++ b/lib/vendor-document-list/plant/shi-buyer-system-api.ts @@ -281,10 +281,10 @@ export class ShiBuyerSystemAPI { return `\\\\60.100.91.61\\SBox\\${projNo}\\${cpyCode}\\${timestamp}\\${fileName}`; } - async sendToSHI(contractId: number) { + async sendToSHI(contractId: number, selectedDocumentIds?: number[]) { try { // 1. 전송할 문서 조회 - const documents = await this.getDocumentsToSend(contractId) + const documents = await this.getDocumentsToSend(contractId, selectedDocumentIds) if (documents.length === 0) { return { success: false, message: "전송할 문서가 없습니다." } @@ -317,8 +317,24 @@ export class ShiBuyerSystemAPI { } } - private async getDocumentsToSend(contractId: number): Promise { - // 1. 먼저 문서 목록을 가져옴 + private async getDocumentsToSend(contractId: number, selectedDocumentIds?: number[]): Promise { + // 1. 기본 WHERE 조건 구성 + const whereConditions = [ + eq(stageDocuments.contractId, contractId), + eq(stageDocuments.status, 'ACTIVE'), + // 승인되지 않은 문서만 (null이거나 "승인(DC)"가 아닌 것) + or( + isNull(stageDocuments.buyerSystemStatus), + ne(stageDocuments.buyerSystemStatus, "승인(DC)") + ) + ] + + // 2. 선택된 문서 ID가 있으면 추가 필터링 + if (selectedDocumentIds && selectedDocumentIds.length > 0) { + whereConditions.push(inArray(stageDocuments.id, selectedDocumentIds)) + } + + // 3. 문서 목록을 가져옴 const documents = await db .select({ documentId: stageDocuments.id, @@ -331,19 +347,10 @@ export class ShiBuyerSystemAPI { projectCode: sql`(SELECT code FROM projects WHERE id = ${stageDocuments.projectId})`, vendorCode: sql`(SELECT vendor_code FROM vendors WHERE id = ${stageDocuments.vendorId})`, vendorName: sql`(SELECT vendor_name FROM vendors WHERE id = ${stageDocuments.vendorId})`, + updatedAt: stageDocuments.updatedAt, }) .from(stageDocuments) - .where( - and( - eq(stageDocuments.contractId, contractId), - eq(stageDocuments.status, 'ACTIVE'), - // ne는 null을 포함하지 않음 - or( - isNull(stageDocuments.buyerSystemStatus), - ne(stageDocuments.buyerSystemStatus, "승인(DC)") - ) - ) - ) + .where(and(...whereConditions)) // 2. 각 문서에 대해 스테이지 정보를 별도로 조회 const documentsWithStages = await Promise.all( -- cgit v1.2.3