"use server"; /** * SWP Vendor Actions * * 벤더 페이지(제출)에서 사용하는 서버 액션 모음입니다. * - 다운로드 및 업로드는 서버액션의 데이터 직렬화 문제로, 별도의 API Route로 분리함 * - 간단한 API 호출은 서버 액션으로 관리 * * 1. 파일 메타정보 업로드 * 2. 파일 업로드 취소 * */ import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import db from "@/db/db"; import { vendors } from "@/db/schema/vendors"; import { contracts } from "@/db/schema/contract"; import { projects } from "@/db/schema/projects"; import { stageDocuments } from "@/db/schema/vendorDocu"; import { eq, and } from "drizzle-orm"; import { getDocumentList, getDocumentDetail, cancelStandbyFile, downloadDocumentFile, type DocumentListItem, type DocumentDetail, type DownloadFileResult } from "./document-service"; import { debugLog, debugError, debugSuccess, debugProcess, debugWarn } from "@/lib/debug-utils"; // ============================================================================ // 벤더 세션 정보 조회 // ============================================================================ interface VendorSessionInfo { vendorId: number; vendorCode: string; vendorName: string; companyId: number; } export async function getVendorSessionInfo(): Promise { debugProcess("벤더 세션 정보 조회 시작"); const session = await getServerSession(authOptions); debugLog("세션 조회 완료", { hasSession: !!session, hasCompanyId: !!session?.user?.companyId }); if (!session?.user?.companyId) { debugWarn("세션 또는 companyId 없음"); return null; } const companyId = typeof session.user.companyId === 'string' ? parseInt(session.user.companyId, 10) : session.user.companyId as number; debugLog("벤더 정보 조회 시작", { companyId }); // vendors 테이블에서 companyId로 벤더 정보 조회 const vendor = await db .select({ id: vendors.id, vendorCode: vendors.vendorCode, vendorName: vendors.vendorName, }) .from(vendors) .where(eq(vendors.id, companyId)) .limit(1); debugLog("벤더 정보 조회 완료", { found: !!vendor[0], vendorCode: vendor[0]?.vendorCode }); if (!vendor[0] || !vendor[0].vendorCode) { debugWarn("벤더 정보 또는 벤더 코드 없음", { vendor: vendor[0] }); return null; } const result = { vendorId: vendor[0].id, vendorCode: vendor[0].vendorCode, vendorName: vendor[0].vendorName, companyId, }; debugSuccess("벤더 세션 정보 조회 성공", { vendorCode: result.vendorCode }); return result; } // ============================================================================ // 벤더의 프로젝트 목록 조회 // ============================================================================ export async function fetchVendorProjects() { debugProcess("벤더 프로젝트 목록 조회 시작"); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음 - 프로젝트 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } debugLog("프로젝트 목록 DB 조회 시작", { vendorId: vendorInfo.vendorId }); // contracts 테이블에서 해당 벤더의 계약들의 프로젝트 조회 const vendorProjects = await db .selectDistinct({ PROJ_NO: projects.code, PROJ_NM: projects.name, }) .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) .where(eq(contracts.vendorId, vendorInfo.vendorId)) .orderBy(projects.code); debugSuccess("프로젝트 목록 조회 성공", { count: vendorProjects.length }); return vendorProjects; } catch (error) { debugError("프로젝트 목록 조회 실패", error); console.error("[fetchVendorProjects] 오류:", error); return []; } } // ============================================================================ // 벤더 필터링된 문서 목록 조회 (Full API 기반) // ============================================================================ export async function fetchVendorDocuments(projNo?: string): Promise { debugProcess("벤더 문서 목록 조회 시작", { projNo }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음 - 문서 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } if (!projNo) { debugWarn("프로젝트 번호 없음"); return []; } debugLog("문서 목록 조회 시작", { projNo, vendorCode: vendorInfo.vendorCode }); // document-service의 getDocumentList 사용 const documents = await getDocumentList(projNo, vendorInfo.vendorCode); debugSuccess("문서 목록 조회 성공", { count: documents.length }); return documents; } catch (error) { debugError("문서 목록 조회 실패", error); console.error("[fetchVendorDocuments] 오류:", error); throw new Error("문서 목록 조회 실패 [SWP 담당자에게 문의하세요]"); } } // ============================================================================ // 문서 상세 조회 (Rev-Activity-File 트리) // ============================================================================ export async function fetchVendorDocumentDetail( projNo: string, docNo: string ): Promise { debugProcess("벤더 문서 상세 조회 시작", { projNo, docNo }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음"); throw new Error("벤더 정보를 찾을 수 없습니다."); } debugLog("문서 상세 조회 시작", { projNo, docNo }); // document-service의 getDocumentDetail 사용 const detail = await getDocumentDetail(projNo, docNo); debugSuccess("문서 상세 조회 성공", { docNo: detail.docNo, revisions: detail.revisions.length, }); return detail; } catch (error) { debugError("문서 상세 조회 실패", error); console.error("[fetchVendorDocumentDetail] 오류:", error); throw new Error("문서 상세 조회 실패"); } } // ============================================================================ // 파일 취소 // ============================================================================ export async function cancelVendorFile( boxSeq: string, actvSeq: string ): Promise { debugProcess("벤더 파일 취소 시작", { boxSeq, actvSeq }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음"); throw new Error("벤더 정보를 찾을 수 없습니다."); } // vendorId를 문자열로 변환하여 사용 await cancelStandbyFile(boxSeq, actvSeq, String(vendorInfo.vendorId)); debugSuccess("파일 취소 완료", { boxSeq, actvSeq }); } catch (error) { debugError("파일 취소 실패", error); console.error("[cancelVendorFile] 오류:", error); throw new Error("파일 취소 실패"); } } // ============================================================================ // 파일 다운로드 // ============================================================================ export async function downloadVendorFile( projNo: string, ownDocNo: string, fileName: string ): Promise { debugProcess("벤더 파일 다운로드 시작", { projNo, ownDocNo, fileName }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음"); return { success: false, error: "벤더 정보를 찾을 수 없습니다.", }; } // document-service의 downloadDocumentFile 사용 const result = await downloadDocumentFile(projNo, ownDocNo, fileName); if (result.success) { debugSuccess("파일 다운로드 완료", { fileName }); } else { debugWarn("파일 다운로드 실패", { fileName, error: result.error }); } return result; } catch (error) { debugError("파일 다운로드 실패", error); console.error("[downloadVendorFile] 오류:", error); return { success: false, error: error instanceof Error ? error.message : "파일 다운로드 실패", }; } } // ============================================================================ // 벤더 통계 조회 (Full API 기반) // ============================================================================ export async function fetchVendorSwpStats(projNo?: string) { debugProcess("벤더 통계 조회 시작", { projNo }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음 - 통계 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } if (!projNo) { debugWarn("프로젝트 번호 없음"); return { total_documents: 0, total_revisions: 0, total_files: 0, uploaded_files: 0, last_sync: null, }; } // API에서 문서 목록 조회 const documents = await getDocumentList(projNo, vendorInfo.vendorCode); // 통계 계산 let totalRevisions = 0; let totalFiles = 0; let uploadedFiles = 0; for (const doc of documents) { // 파일 통계는 더 이상 계산하지 않음 (API 호출 제거됨) // totalFiles += doc.fileCount; // uploadedFiles += doc.fileCount - doc.standbyFileCount; // 리비전 수 추정 (LTST_REV_NO 기반) if (doc.LTST_REV_NO) { const revNum = parseInt(doc.LTST_REV_NO, 10); if (!isNaN(revNum)) { totalRevisions += revNum + 1; // Rev 00부터 시작이므로 +1 } } } const result = { total_documents: documents.length, total_revisions: totalRevisions, total_files: totalFiles, uploaded_files: uploadedFiles, last_sync: new Date(), // API 기반이므로 항상 최신 }; debugSuccess("통계 조회 성공", { documents: result.total_documents, revisions: result.total_revisions, files: result.total_files, uploaded: result.uploaded_files, }); return result; } catch (error) { debugError("통계 조회 실패", error); console.error("[fetchVendorSwpStats] 오류:", error); return { total_documents: 0, total_revisions: 0, total_files: 0, uploaded_files: 0, last_sync: null, }; } } // ============================================================================ // 벤더가 업로드한 파일 목록 조회 (Inbox) // // API 응답 파일 목록 + DB의 업로드 필요 문서 목록을 함께 반환 // - DB 조회: stageDocuments에서 buyerSystemStatus='Completed'인 문서 중 // 아직 업로드되지 않은 문서 (vendorDocNumber가 API 응답의 OWN_DOC_NO에 없는 것) // - 목적: 벤더에게 업로드를 위한 문서번호 기준(vendorDocNumber)을 제공 // ============================================================================ export async function fetchVendorUploadedFiles(projNo: string) { debugProcess("벤더 업로드 파일 목록 조회 시작", { projNo }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음 - 업로드 파일 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } if (!projNo) { debugWarn("프로젝트 번호 없음"); return { files: [], requiredDocs: [] }; } debugLog("업로드 파일 목록 조회 시작", { projNo, vendorCode: vendorInfo.vendorCode }); // 1. API에서 업로드된 파일 목록 조회 const { fetchGetExternalInboxList } = await import("./api-client"); const files = await fetchGetExternalInboxList({ projNo, vndrCd: vendorInfo.vendorCode, }); debugLog("API 파일 목록 조회 완료", { count: files.length }); // 2. 프로젝트 ID 조회 const project = await db .select({ id: projects.id }) .from(projects) .where(eq(projects.code, projNo)) .limit(1); if (!project[0]) { debugWarn("프로젝트를 찾을 수 없음", { projNo }); return { files, requiredDocs: [] }; } const projectId = project[0].id; // 3. stageDocuments에서 buyerSystemStatus='Completed'인 문서 조회 const completedDocs = await db .select({ vendorDocNumber: stageDocuments.vendorDocNumber, title: stageDocuments.title, buyerSystemComment: stageDocuments.buyerSystemComment, }) .from(stageDocuments) .where( and( eq(stageDocuments.projectId, projectId), eq(stageDocuments.vendorId, vendorInfo.vendorId), eq(stageDocuments.buyerSystemStatus, "Completed") ) ); debugLog("stageDocuments 조회 완료", { count: completedDocs.length }); // 4. API 응답에 이미 존재하는 vendorDocNumber 필터링 const uploadedDocNumbers = new Set( files.map((file) => file.OWN_DOC_NO).filter(Boolean) ); const requiredDocs = completedDocs .filter((doc) => doc.vendorDocNumber && !uploadedDocNumbers.has(doc.vendorDocNumber)) .map((doc) => ({ vendorDocNumber: doc.vendorDocNumber!, title: doc.title, buyerSystemComment: doc.buyerSystemComment || null, })); debugSuccess("업로드 파일 목록 조회 성공", { filesCount: files.length, requiredDocsCount: requiredDocs.length }); return { files, requiredDocs }; } catch (error) { debugError("업로드 파일 목록 조회 실패", error); console.error("[fetchVendorUploadedFiles] 오류:", error); throw new Error("업로드 파일 목록 조회 실패"); } } // ============================================================================ // 벤더가 업로드한 파일 취소 (userId 파라미터 버전) // ============================================================================ export interface CancelVendorUploadedFileParams { boxSeq: string; actvSeq: string; userId: string; } export async function cancelVendorUploadedFile(params: CancelVendorUploadedFileParams) { debugProcess("벤더 업로드 파일 취소 시작", params); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음"); throw new Error("벤더 정보를 찾을 수 없습니다."); } // api-client의 callSaveInBoxListCancelStatus 사용 const { callSaveInBoxListCancelStatus } = await import("./api-client"); const cancelCount = await callSaveInBoxListCancelStatus({ boxSeq: params.boxSeq, actvSeq: params.actvSeq, chgr: `evcp${params.userId}`, }); debugSuccess("업로드 파일 취소 완료", { ...params, cancelCount }); return { success: true, cancelCount }; } catch (error) { debugError("업로드 파일 취소 실패", error); console.error("[cancelVendorUploadedFile] 오류:", error); throw new Error("파일 취소 실패"); } } // ============================================================================ // 주의: 파일 업로드는 /api/swp/upload 라우트에서 처리됩니다 // ============================================================================