diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-29 15:59:04 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-29 15:59:04 +0900 |
| commit | 2ecdac866c19abea0b5389708fcdf5b3889c969a (patch) | |
| tree | e02a02cfa0890691fb28a7df3a96ef495b3d4b79 /lib/swp/vendor-actions.ts | |
| parent | 2fc9e5492e220041ba322d9a1479feb7803228cf (diff) | |
(김준회) SWP 파일 업로드 취소 기능 추가, 업로드 파일명 검증로직에서 파일명 비필수로 변경
Diffstat (limited to 'lib/swp/vendor-actions.ts')
| -rw-r--r-- | lib/swp/vendor-actions.ts | 513 |
1 files changed, 145 insertions, 368 deletions
diff --git a/lib/swp/vendor-actions.ts b/lib/swp/vendor-actions.ts index c96cf055..f65ed007 100644 --- a/lib/swp/vendor-actions.ts +++ b/lib/swp/vendor-actions.ts @@ -6,13 +6,16 @@ import db from "@/db/db"; import { vendors } from "@/db/schema/vendors"; import { contracts } from "@/db/schema/contract"; import { projects } from "@/db/schema/projects"; -import { swpDocumentRevisions, swpDocumentFiles } from "@/db/schema/SWP/swp-documents"; -import { eq, sql, and } from "drizzle-orm"; -import { fetchSwpDocuments, type SwpTableParams } from "./actions"; -import { fetchGetExternalInboxList } from "./api-client"; -import type { SwpFileApiResponse } from "./sync-service"; -import fs from "fs/promises"; -import path from "path"; +import { eq } 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"; // ============================================================================ @@ -110,11 +113,11 @@ export async function fetchVendorProjects() { } // ============================================================================ -// 벤더 필터링된 문서 목록 조회 +// 벤더 필터링된 문서 목록 조회 (Full API 기반) // ============================================================================ -export async function fetchVendorDocuments(params: SwpTableParams) { - debugProcess("벤더 문서 목록 조회 시작", { page: params.page, pageSize: params.pageSize }); +export async function fetchVendorDocuments(projNo?: string): Promise<DocumentListItem[]> { + debugProcess("벤더 문서 목록 조회 시작", { projNo }); try { const vendorInfo = await getVendorSessionInfo(); @@ -124,22 +127,21 @@ export async function fetchVendorDocuments(params: SwpTableParams) { throw new Error("벤더 정보를 찾을 수 없습니다."); } - // 벤더 코드를 필터에 자동 추가 - const vendorParams: SwpTableParams = { - ...params, - filters: { - ...params.filters, - vndrCd: vendorInfo.vendorCode, // 벤더 코드 필터 강제 적용 - }, - }; + if (!projNo) { + debugWarn("프로젝트 번호 없음"); + return []; + } - debugLog("SWP 문서 조회 호출", { vendorCode: vendorInfo.vendorCode, filters: vendorParams.filters }); + debugLog("문서 목록 조회 시작", { + projNo, + vendorCode: vendorInfo.vendorCode + }); - // 기존 fetchSwpDocuments 재사용 - const result = await fetchSwpDocuments(vendorParams); + // document-service의 getDocumentList 사용 + const documents = await getDocumentList(projNo, vendorInfo.vendorCode); - debugSuccess("문서 목록 조회 성공", { total: result.total, dataCount: result.data.length }); - return result; + debugSuccess("문서 목록 조회 성공", { count: documents.length }); + return documents; } catch (error) { debugError("문서 목록 조회 실패", error); console.error("[fetchVendorDocuments] 오류:", error); @@ -148,104 +150,114 @@ export async function fetchVendorDocuments(params: SwpTableParams) { } // ============================================================================ -// 파일 업로드 +// 문서 상세 조회 (Rev-Activity-File 트리) // ============================================================================ -export interface FileUploadParams { - revisionId: number; - file: { - FILE_NM: string; - FILE_SEQ: string; - FILE_SZ: string; - FLD_PATH: string; - STAT?: string; - STAT_NM?: string; - }; - fileBuffer?: Buffer; // 실제 파일 데이터 추가 -} - -export async function uploadFileToRevision(params: FileUploadParams) { - debugProcess("파일 업로드 시작", { revisionId: params.revisionId, fileName: params.file.FILE_NM }); +export async function fetchVendorDocumentDetail( + projNo: string, + docNo: string +): Promise<DocumentDetail> { + debugProcess("벤더 문서 상세 조회 시작", { projNo, docNo }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { - debugError("벤더 정보 없음 - 파일 업로드 실패"); + debugError("벤더 정보 없음"); throw new Error("벤더 정보를 찾을 수 없습니다."); } - const { revisionId } = params; - debugLog("리비전 권한 확인 시작", { revisionId }); - - // 1. 해당 리비전이 벤더에게 제공된 문서인지 확인 - const revisionCheck = await db - .select({ - DOC_NO: swpDocumentRevisions.DOC_NO, - VNDR_CD: sql<string>`( - SELECT d."VNDR_CD" - FROM swp.swp_documents d - WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} - )`, - }) - .from(swpDocumentRevisions) - .where(eq(swpDocumentRevisions.id, revisionId)) - .limit(1); + debugLog("문서 상세 조회 시작", { projNo, docNo }); - debugLog("리비전 조회 결과", { found: !!revisionCheck[0], docNo: revisionCheck[0]?.DOC_NO }); + // document-service의 getDocumentDetail 사용 + const detail = await getDocumentDetail(projNo, docNo); - if (!revisionCheck[0]) { - debugError("리비전 없음", { revisionId }); - throw new Error("리비전을 찾을 수 없습니다."); - } + debugSuccess("문서 상세 조회 성공", { + docNo: detail.docNo, + revisions: detail.revisions.length, + }); - // 벤더 코드가 일치하는지 확인 - if (revisionCheck[0].VNDR_CD !== vendorInfo.vendorCode) { - debugError("권한 없음", { - expected: vendorInfo.vendorCode, - actual: revisionCheck[0].VNDR_CD, - docNo: revisionCheck[0].DOC_NO - }); - throw new Error("이 문서에 대한 권한이 없습니다."); + return detail; + } catch (error) { + debugError("문서 상세 조회 실패", error); + console.error("[fetchVendorDocumentDetail] 오류:", error); + throw new Error("문서 상세 조회 실패"); + } +} + +// ============================================================================ +// 파일 취소 +// ============================================================================ + +export async function cancelVendorFile( + boxSeq: string, + actvSeq: string +): Promise<void> { + debugProcess("벤더 파일 취소 시작", { boxSeq, actvSeq }); + + try { + const vendorInfo = await getVendorSessionInfo(); + + if (!vendorInfo) { + debugError("벤더 정보 없음"); + throw new Error("벤더 정보를 찾을 수 없습니다."); } - debugSuccess("리비전 권한 확인 성공"); + // vendorId를 문자열로 변환하여 사용 + await cancelStandbyFile(boxSeq, actvSeq, String(vendorInfo.vendorId)); - const { revisionId: revId, file, fileBuffer } = params; + debugSuccess("파일 취소 완료", { boxSeq, actvSeq }); + } catch (error) { + debugError("파일 취소 실패", error); + console.error("[cancelVendorFile] 오류:", error); + throw new Error("파일 취소 실패"); + } +} - // 1. SWP 마운트 경로에 파일 저장 - debugProcess("파일 저장 단계 시작"); - await saveFileToSwpNetwork(revId, { - FILE_NM: file.FILE_NM, - fileBuffer: fileBuffer, - }); +// ============================================================================ +// 파일 다운로드 +// ============================================================================ + +export async function downloadVendorFile( + projNo: string, + ownDocNo: string, + fileName: string +): Promise<DownloadFileResult> { + debugProcess("벤더 파일 다운로드 시작", { projNo, ownDocNo, fileName }); - // 2. 파일 저장 API 호출 (메타데이터 전송) - debugProcess("API 호출 단계 시작"); - await callSwpFileSaveApi(revId, file); + try { + const vendorInfo = await getVendorSessionInfo(); + + if (!vendorInfo) { + debugError("벤더 정보 없음"); + return { + success: false, + error: "벤더 정보를 찾을 수 없습니다.", + }; + } - // 3. 파일 리스트 API 호출 (업데이트된 파일 목록 조회) - debugProcess("파일 목록 조회 단계 시작"); - const updatedFiles = await fetchUpdatedFileList(revId); - debugLog("업데이트된 파일 목록", { count: updatedFiles.length }); + // document-service의 downloadDocumentFile 사용 + const result = await downloadDocumentFile(projNo, ownDocNo, fileName); - // 4. 파일 목록 DB 동기화 (새 파일들 추가) - debugProcess("DB 동기화 단계 시작"); - await syncSwpDocumentFiles(revId, updatedFiles); + if (result.success) { + debugSuccess("파일 다운로드 완료", { fileName }); + } else { + debugWarn("파일 다운로드 실패", { fileName, error: result.error }); + } - debugSuccess("파일 업로드 완료", { fileName: file.FILE_NM, revisionId }); - return { success: true, fileId: 0, action: "uploaded" }; + return result; } catch (error) { - debugError("파일 업로드 실패", error); - console.error("[uploadFileToRevision] 오류:", error); - throw new Error( - error instanceof Error ? error.message : "파일 업로드 실패" - ); + debugError("파일 다운로드 실패", error); + console.error("[downloadVendorFile] 오류:", error); + return { + success: false, + error: error instanceof Error ? error.message : "파일 다운로드 실패", + }; } } // ============================================================================ -// 벤더 통계 조회 +// 벤더 통계 조회 (Full API 기반) // ============================================================================ export async function fetchVendorSwpStats(projNo?: string) { @@ -259,48 +271,52 @@ export async function fetchVendorSwpStats(projNo?: string) { throw new Error("벤더 정보를 찾을 수 없습니다."); } - const whereConditions = [ - sql`d."VNDR_CD" = ${vendorInfo.vendorCode}`, - ]; + if (!projNo) { + debugWarn("프로젝트 번호 없음"); + return { + total_documents: 0, + total_revisions: 0, + total_files: 0, + uploaded_files: 0, + last_sync: null, + }; + } - if (projNo) { - whereConditions.push(sql`d."PROJ_NO" = ${projNo}`); + // API에서 문서 목록 조회 + const documents = await getDocumentList(projNo, vendorInfo.vendorCode); + + // 통계 계산 + let totalRevisions = 0; + let totalFiles = 0; + let uploadedFiles = 0; + + for (const doc of documents) { + totalFiles += doc.fileCount; + // standbyFileCount가 0이 아니면 업로드된 것으로 간주 + 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 + } + } } - debugLog("통계 SQL 실행", { vendorCode: vendorInfo.vendorCode, projNo }); - - const stats = await db.execute<{ - total_documents: number; - total_revisions: number; - total_files: number; - uploaded_files: number; - last_sync: Date | null; - }>(sql` - SELECT - COUNT(DISTINCT d."DOC_NO")::int as total_documents, - COUNT(DISTINCT r.id)::int as total_revisions, - COUNT(f.id)::int as total_files, - COUNT(CASE WHEN f."FLD_PATH" IS NOT NULL AND f."FLD_PATH" != '' THEN 1 END)::int as uploaded_files, - MAX(d.last_synced_at) as last_sync - FROM swp.swp_documents d - LEFT JOIN swp.swp_document_revisions r ON d."DOC_NO" = r."DOC_NO" - LEFT JOIN swp.swp_document_files f ON r.id = f.revision_id - WHERE ${sql.join(whereConditions, sql` AND `)} - `); - - const result = stats.rows[0] || { - total_documents: 0, - total_revisions: 0, - total_files: 0, - uploaded_files: 0, - last_sync: null, + 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 + uploaded: result.uploaded_files, }); return result; @@ -318,245 +334,6 @@ export async function fetchVendorSwpStats(projNo?: string) { } // ============================================================================ -// SWP 파일 업로드 헬퍼 함수들 +// 주의: 파일 업로드는 /api/swp/upload 라우트에서 처리됩니다 // ============================================================================ -/** - * 1. SWP 마운트 경로에 파일 저장 - */ -async function saveFileToSwpNetwork( - revisionId: number, - fileInfo: { FILE_NM: string; fileBuffer?: Buffer } -): Promise<string> { - debugProcess("네트워크 파일 저장 시작", { revisionId, fileName: fileInfo.FILE_NM }); - - // 리비전 정보 조회 - const revisionInfo = await db - .select({ - DOC_NO: swpDocumentRevisions.DOC_NO, - REV_NO: swpDocumentRevisions.REV_NO, - PROJ_NO: sql<string>`( - SELECT d."PROJ_NO" FROM swp.swp_documents d - WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} - )`, - VNDR_CD: sql<string>`( - SELECT d."VNDR_CD" FROM swp.swp_documents d - WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} - )`, - }) - .from(swpDocumentRevisions) - .where(eq(swpDocumentRevisions.id, revisionId)) - .limit(1); - - debugLog("리비전 정보 조회", { found: !!revisionInfo[0], docNo: revisionInfo[0]?.DOC_NO }); - - if (!revisionInfo[0]) { - debugError("리비전 정보 없음"); - throw new Error("리비전 정보를 찾을 수 없습니다"); - } - - const { PROJ_NO, VNDR_CD, DOC_NO, REV_NO } = revisionInfo[0]; - - // SWP 마운트 경로 생성 - const mountDir = process.env.SWP_MOUNT_DIR || "/mnt/swp-smb-dir"; - const targetDir = path.join(mountDir, PROJ_NO, VNDR_CD, DOC_NO, REV_NO); - - debugLog("파일 저장 경로 생성", { mountDir, targetDir }); - - // 디렉토리 생성 - await fs.mkdir(targetDir, { recursive: true }); - debugLog("디렉토리 생성 완료"); - - // 파일 저장 - const targetPath = path.join(targetDir, fileInfo.FILE_NM); - - if (fileInfo.fileBuffer) { - await fs.writeFile(targetPath, fileInfo.fileBuffer); - debugSuccess("파일 저장 완료", { fileName: fileInfo.FILE_NM, targetPath, size: fileInfo.fileBuffer.length }); - } else { - debugWarn("파일 버퍼 없음", { fileName: fileInfo.FILE_NM }); - } - - return targetPath; -} - -/** - * 2. 파일 저장 API 호출 (메타데이터 전송) - */ -async function callSwpFileSaveApi( - revisionId: number, - fileInfo: FileUploadParams['file'] -): Promise<void> { - debugProcess("SWP 파일 저장 API 호출 시작", { revisionId, fileName: fileInfo.FILE_NM }); - - // TODO: SWP 파일 저장 API 구현 - // buyer-system의 sendToInBox 패턴 참고 - debugLog("메타데이터 전송", { - fileName: fileInfo.FILE_NM, - fileSeq: fileInfo.FILE_SEQ, - filePath: fileInfo.FLD_PATH - }); - - // 임시 구현: 실제로는 SWP SaveFile API 등을 호출해야 함 - // 예: SaveFile, UploadFile API 등 - debugWarn("SWP 파일 저장 API가 아직 구현되지 않음 - 임시 스킵"); -} - -/** - * 3. 파일 리스트 API 호출 (업데이트된 파일 목록 조회) - */ -async function fetchUpdatedFileList(revisionId: number): Promise<SwpFileApiResponse[]> { - debugProcess("업데이트된 파일 목록 조회 시작", { revisionId }); - - // 리비전 정보 조회 - const revisionInfo = await db - .select({ - DOC_NO: swpDocumentRevisions.DOC_NO, - PROJ_NO: sql<string>`( - SELECT d."PROJ_NO" FROM swp.swp_documents d - WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} - )`, - VNDR_CD: sql<string>`( - SELECT d."VNDR_CD" FROM swp.swp_documents d - WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} - )`, - }) - .from(swpDocumentRevisions) - .where(eq(swpDocumentRevisions.id, revisionId)) - .limit(1); - - debugLog("리비전 정보 조회", { found: !!revisionInfo[0], docNo: revisionInfo[0]?.DOC_NO }); - - if (!revisionInfo[0]) { - debugError("리비전 정보 없음"); - throw new Error("리비전 정보를 찾을 수 없습니다"); - } - - const { PROJ_NO, VNDR_CD } = revisionInfo[0]; - - debugLog("SWP 파일 목록 API 호출", { projNo: PROJ_NO, vndrCd: VNDR_CD }); - - // SWP API에서 업데이트된 파일 목록 조회 - const files = await fetchGetExternalInboxList({ - projNo: PROJ_NO, - vndrCd: VNDR_CD, - }); - - debugSuccess("파일 목록 조회 완료", { count: files.length }); - return files; -} - -/** - * 4. 파일 목록 DB 동기화 (새 파일들 추가) - */ -async function syncSwpDocumentFiles( - revisionId: number, - apiFiles: SwpFileApiResponse[] -): Promise<void> { - debugProcess("DB 동기화 시작", { revisionId, fileCount: apiFiles.length }); - - // 리비전 정보에서 DOC_NO 가져오기 - const revisionInfo = await db - .select({ - DOC_NO: swpDocumentRevisions.DOC_NO, - }) - .from(swpDocumentRevisions) - .where(eq(swpDocumentRevisions.id, revisionId)) - .limit(1); - - debugLog("리비전 DOC_NO 조회", { found: !!revisionInfo[0], docNo: revisionInfo[0]?.DOC_NO }); - - if (!revisionInfo[0]) { - debugError("리비전 정보 없음"); - throw new Error("리비전 정보를 찾을 수 없습니다"); - } - - const { DOC_NO } = revisionInfo[0]; - let processedCount = 0; - let updatedCount = 0; - let insertedCount = 0; - - for (const apiFile of apiFiles) { - try { - processedCount++; - - // 기존 파일 확인 - const existingFile = await db - .select({ id: swpDocumentFiles.id }) - .from(swpDocumentFiles) - .where( - and( - eq(swpDocumentFiles.revision_id, revisionId), - eq(swpDocumentFiles.FILE_SEQ, apiFile.FILE_SEQ || "1") - ) - ) - .limit(1); - - const fileData = { - DOC_NO: DOC_NO, - FILE_NM: apiFile.FILE_NM, - FILE_SEQ: apiFile.FILE_SEQ || "1", - FILE_SZ: apiFile.FILE_SZ || "0", - FLD_PATH: apiFile.FLD_PATH, - STAT: apiFile.STAT || null, - STAT_NM: apiFile.STAT_NM || null, - ACTV_NO: apiFile.ACTV_NO || null, - IDX: apiFile.IDX || null, - CRTER: apiFile.CRTER || null, - CRTE_DTM: apiFile.CRTE_DTM || null, - CHGR: apiFile.CHGR || null, - CHG_DTM: apiFile.CHG_DTM || null, - sync_status: 'synced' as const, - last_synced_at: new Date(), - updated_at: new Date(), - }; - - if (existingFile[0]) { - // 기존 파일 업데이트 - await db - .update(swpDocumentFiles) - .set({ - ...fileData, - updated_at: new Date(), - }) - .where(eq(swpDocumentFiles.id, existingFile[0].id)); - updatedCount++; - debugLog("파일 업데이트", { fileName: apiFile.FILE_NM, fileSeq: apiFile.FILE_SEQ }); - } else { - // 새 파일 추가 - await db.insert(swpDocumentFiles).values({ - revision_id: revisionId, - DOC_NO: DOC_NO, - FILE_NM: apiFile.FILE_NM, - FILE_SEQ: apiFile.FILE_SEQ || "1", - FILE_SZ: apiFile.FILE_SZ || "0", - FLD_PATH: apiFile.FLD_PATH, - STAT: apiFile.STAT || null, - STAT_NM: apiFile.STAT_NM || null, - ACTV_NO: apiFile.ACTV_NO || null, - IDX: apiFile.IDX || null, - CRTER: apiFile.CRTER || null, - CRTE_DTM: apiFile.CRTE_DTM || null, - CHGR: apiFile.CHGR || null, - CHG_DTM: apiFile.CHG_DTM || null, - sync_status: 'synced' as const, - last_synced_at: new Date(), - updated_at: new Date(), - }); - insertedCount++; - debugLog("파일 추가", { fileName: apiFile.FILE_NM, fileSeq: apiFile.FILE_SEQ }); - } - } catch (error) { - debugError("파일 동기화 실패", { fileName: apiFile.FILE_NM, error }); - console.error(`파일 동기화 실패: ${apiFile.FILE_NM}`, error); - // 개별 파일 실패는 전체 프로세스를 중단하지 않음 - } - } - - debugSuccess("DB 동기화 완료", { - processed: processedCount, - updated: updatedCount, - inserted: insertedCount - }); -} - |
