diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-22 21:10:24 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-22 21:10:24 +0900 |
| commit | 6b2a561265fb649398e1770f720365ee10f542e9 (patch) | |
| tree | 50ae6453939d8ce4be850d603450d03d31e05442 /lib/vendor-document-list/plant/shi-buyer-system-api.ts | |
| parent | 2ecf88af270c5d044a853793f72f3a4536e05b89 (diff) | |
(김준회) SWP 문서 업로드
Diffstat (limited to 'lib/vendor-document-list/plant/shi-buyer-system-api.ts')
| -rw-r--r-- | lib/vendor-document-list/plant/shi-buyer-system-api.ts | 356 |
1 files changed, 257 insertions, 99 deletions
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 f82151cd..39336c1d 100644 --- a/lib/vendor-document-list/plant/shi-buyer-system-api.ts +++ b/lib/vendor-document-list/plant/shi-buyer-system-api.ts @@ -85,21 +85,18 @@ interface ShiApiResponse { GetDwgInfoResult: ShiDocumentResponse[] } -// InBox 파일 정보 인터페이스 추가 +// InBox 파일 정보 인터페이스 (SaveInBoxList API 요청 형식) interface InBoxFileInfo { - PROJ_NO: string - SHI_DOC_NO: string - STAGE_NAME: string - REVISION_NO: string - VNDR_CD: string - VNDR_NM: string - FILE_NAME: string - FILE_SIZE: number - CONTENT_TYPE: string - UPLOAD_DATE: string - UPLOADED_BY: string - STATUS: string - COMMENT: string + CPY_CD: string // 회사 코드 (항상 "C00001" 고정, VNDR_CD와는 별개) + FILE_NM: string // 파일명: [OWNDOCNO]_[REVNO]_[STAGE].[extension] + OFDC_NO: string | null // null 가능 + PROJ_NO: string // 프로젝트 번호 + OWN_DOC_NO: string // 자사 문서번호 + REV_NO: string // 리비전 번호 + STAGE: string // 스테이지 (예: IFA, IFB 등) + STAT: string // 상태코드 (예: SCW03 - Completed) + FILE_SZ: string // 파일 크기 (byte, 문자열) + FLD_PATH: string // 폴더 경로: [ProjNo][CpyCd][YYYYMMDDHHMMSS] } // 파일 저장용 확장 인터페이스 @@ -108,20 +105,14 @@ interface FileInfoWithBuffer extends InBoxFileInfo { attachment: { id: number; fileName: string; - mimeType?: string; - storagePath?: string; - storageUrl?: string; + mimeType: string | null; + storagePath: string | null; + storageUrl: string | null; + [key: string]: any; }; -} - -// 경로 생성용 인터페이스 (유연한 타입) -interface PathGenerationData { - PROJ_NO: string | number; - VNDR_CD: string | number | null | undefined; - SHI_DOC_NO: string | number; - REVISION_NO: string | number; - STAGE_NAME: string | number; - FILE_NAME: string | number; + // 네트워크 경로 생성을 위한 추가 정보 + _timestamp: string; + _extension: string; } // SaveInBoxList API 응답 인터페이스 @@ -138,6 +129,74 @@ interface SaveInBoxListResponse { } } +// SaveInBoxList API 요청 인터페이스 +interface SaveInBoxListRequest { + externalInboxLists: InBoxFileInfo[] +} + +// 내부 문서 타입 (getDocumentsToSend 반환 타입) +interface DocumentWithStages { + documentId: number + docNumber: string + vendorDocNumber: string | null + title: string + status: string + buyerSystemComment: string | null + projectCode: string + vendorCode: string + vendorName: string + docClass?: string | null + stages: Array<{ + id: number + documentId: number + stageName: string + stageOrder: number | null + planDate: Date | string | null + actualDate: Date | string | null + [key: string]: any + }> +} + +// 제출 정보 타입 (getSubmissionFullInfo 반환 타입) +interface SubmissionFullInfo { + submission: { + id: number + revisionNumber: number + submittedBy: string + [key: string]: any + } + stage: { + id: number + stageName: string + [key: string]: any + } + document: { + id: number + docNumber: string + vendorDocNumber: string | null + [key: string]: any + } + project: { + id: number + code: string + [key: string]: any + } + vendor: { + id: number + vendorCode: string | null + vendorName: string + [key: string]: any + } | null + attachments: Array<{ + id: number + fileName: string + mimeType: string | null + storagePath: string | null + storageUrl: string | null + [key: string]: any + }> +} + export class ShiBuyerSystemAPI { private baseUrl = process.env.SWP_BASE_URL || 'http://60.100.99.217/DDP/Services/VNDRService.svc' private ddcUrl = process.env.DDC_BASE_URL || 'http://60.100.99.217/DDC/Services/WebService.svc' @@ -175,30 +234,51 @@ export class ShiBuyerSystemAPI { } /** - * SMB 마운트 경로에 맞는 파일 경로 생성 - * /mnt/swp-smb-dir/{proj_no}/{cpyCd}/{YYYYMMDDhhmmss}/{[파일명]}{DOC_NO}_{REV}_{STAGE}.{extension} + * SMB 마운트 경로에 맞는 파일 경로 생성 (레거시 경로 규칙) + * /mnt/swp-smb-dir/{PROJ_NO}/{CPY_CD}/{YYYYMMDDHHmmSS}/[OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDD].확장자 */ - private generateMountPath(fileInfo: PathGenerationData): string { - // 모든 값들을 문자열로 변환 - const projNo: string = String(fileInfo.PROJ_NO); - const vndrCd: string = String(fileInfo.VNDR_CD || ''); - const shiDocNo: string = String(fileInfo.SHI_DOC_NO); - const revisionNo: string = String(fileInfo.REVISION_NO); - const stageName: string = String(fileInfo.STAGE_NAME); - const fileName: string = String(fileInfo.FILE_NAME); - - const timestamp = this.getTimestamp(); - const { name: fileNameWithoutExt, extension } = - this.parseFileName(fileName); - - // 새로운 파일명 생성: {[파일명]}{DOC_NO}_{REV}_{STAGE}.{extension} - const newFileName = `[${fileNameWithoutExt}]${shiDocNo}_${revisionNo}_${stageName}`; - const fullFileName = extension - ? `${newFileName}.${extension}` - : newFileName; - + private generateMountPath( + projNo: string, + cpyCode: string, + timestamp: string, + ownDocNo: string, + revNo: string, + stage: string, + extension: string + ): string { + const dateOnly = timestamp.substring(0, 8); // YYYYMMDD만 추출 + + // 파일명 생성: [OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDD].확장자 + const fileName = extension + ? `[${ownDocNo}]_${revNo}_${stage}_${dateOnly}.${extension}` + : `[${ownDocNo}]_${revNo}_${stage}_${dateOnly}`; + // 전체 경로 생성 - return path.join(this.swpMountDir, projNo, vndrCd, timestamp, fullFileName); + return path.join(this.swpMountDir, projNo, cpyCode, timestamp, fileName); + } + + /** + * 네트워크 경로 생성 (SHI 시스템에서 접근 가능한 경로) + * \\60.100.91.61\SBox\{PROJ_NO}\{CPY_CD}\{YYYYMMDDHHmmSS}\[OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDD].확장자 + */ + private generateNetworkPath( + projNo: string, + cpyCode: string, + timestamp: string, + ownDocNo: string, + revNo: string, + stage: string, + extension: string + ): string { + const dateOnly = timestamp.substring(0, 8); // YYYYMMDD만 추출 + + // 파일명 생성: [OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDD].확장자 + const fileName = extension + ? `[${ownDocNo}]_${revNo}_${stage}_${dateOnly}.${extension}` + : `[${ownDocNo}]_${revNo}_${stage}_${dateOnly}`; + + // 네트워크 경로 생성 + return `\\\\60.100.91.61\\SBox\\${projNo}\\${cpyCode}\\${timestamp}\\${fileName}`; } async sendToSHI(contractId: number) { @@ -237,7 +317,7 @@ export class ShiBuyerSystemAPI { } } - private async getDocumentsToSend(contractId: number) { + private async getDocumentsToSend(contractId: number): Promise<DocumentWithStages[]> { // 1. 먼저 문서 목록을 가져옴 const documents = await db .select({ @@ -276,14 +356,14 @@ export class ShiBuyerSystemAPI { return { ...doc, stages: stages || [] - } + } as DocumentWithStages }) ) return documentsWithStages } - private async sendDocumentInfo(documents: any[]) { + private async sendDocumentInfo(documents: DocumentWithStages[]) { const shiDocuments: ShiDocumentInfo[] = documents.map((doc) => ({ PROJ_NO: doc.projectCode, SHI_DOC_NO: doc.docNumber, @@ -326,7 +406,7 @@ export class ShiBuyerSystemAPI { return response.json() } - private async sendScheduleInfo(documents: any[]) { + private async sendScheduleInfo(documents: DocumentWithStages[]) { const schedules: ShiScheduleInfo[] = [] for (const doc of documents) { @@ -413,15 +493,15 @@ export class ShiBuyerSystemAPI { where: eq(contracts.id, contractId), }); - if (!contract) { - throw new Error(`계약을 찾을 수 없습니다: ${contractId}`) + if (!contract || !contract.projectId || !contract.vendorId) { + throw new Error(`계약 정보가 올바르지 않습니다: ${contractId}`) } const project = await db.query.projects.findFirst({ where: eq(projects.id, contract.projectId), }); - if (!project) { + if (!project || !project.code) { throw new Error(`프로젝트를 찾을 수 없습니다: ${contract.projectId}`) } @@ -429,12 +509,12 @@ export class ShiBuyerSystemAPI { where: eq(vendors.id, contract.vendorId), }); - if (!vendor) { + if (!vendor || !vendor.vendorCode) { throw new Error(`벤더를 찾을 수 없습니다: ${contract.vendorId}`) } const shiDocuments = await this.fetchDocumentsFromSHI(project.code, { - VNDR_CD: vendor.vendorCode + VNDR_CD: vendor.vendorCode ?? undefined }) if (!shiDocuments || shiDocuments.length === 0) { @@ -508,7 +588,13 @@ export class ShiBuyerSystemAPI { ) { let updatedCount = 0 let newCount = 0 - const updatedDocuments: any[] = [] + const updatedDocuments: Array<{ + docNumber: string + title: string + status: string | null | undefined + comment: string | null | undefined + action: string + }> = [] const project = await db.query.projects.findFirst({ where: eq(projects.code, projectCode) @@ -599,7 +685,13 @@ export class ShiBuyerSystemAPI { totalCount: submissionIds.length, successCount: 0, failedCount: 0, - details: [] as any[] + details: [] as Array<{ + submissionId: number + success: boolean + message?: string + syncedFiles?: number + error?: string + }> } for (const submissionId of submissionIds) { @@ -696,7 +788,7 @@ export class ShiBuyerSystemAPI { /** * 제출 정보 조회 (관련 정보 포함) */ - private async getSubmissionFullInfo(submissionId: number) { + private async getSubmissionFullInfo(submissionId: number): Promise<SubmissionFullInfo | null> { const result = await db .select({ submission: stageSubmissions, @@ -729,16 +821,18 @@ export class ShiBuyerSystemAPI { return { ...result[0], attachments - } + } as SubmissionFullInfo } /** * 파일 내용과 함께 InBox 파일 정보 준비 */ private async prepareFilesWithContent( - submissionInfo: any + submissionInfo: SubmissionFullInfo ): Promise<FileInfoWithBuffer[]> { const filesWithContent: FileInfoWithBuffer[] = []; + const timestamp = this.getTimestamp(); // 모든 파일에 동일한 타임스탬프 사용 + const cpyCode = 'C00001'; // CPY_CD는 항상 C00001 고정 (레거시 시스템 협의사항) for (const attachment of submissionInfo.attachments) { try { @@ -758,23 +852,46 @@ export class ShiBuyerSystemAPI { // 파일 읽기 const fileBuffer = await fs.readFile(fullPath); - // 파일 정보 생성 + // 파일명 파싱 + const { extension } = this.parseFileName(attachment.fileName); + + // OWN_DOC_NO 결정 (vendorDocNumber가 있으면 사용, 없으면 docNumber 사용) + const ownDocNo = submissionInfo.document.vendorDocNumber || submissionInfo.document.docNumber; + + // 리비전 번호 (2자리로 패딩) + const revNo = String(submissionInfo.submission.revisionNumber).padStart(2, '0'); + + // 파일명 생성: [OWNDOCNO]_[REVNO]_[STAGE].[extension] + const fileName = extension + ? `${ownDocNo}_${revNo}_${submissionInfo.stage.stageName}.${extension}` + : `${ownDocNo}_${revNo}_${submissionInfo.stage.stageName}`; + + // 폴더 경로 생성: [ProjNo][CpyCd][YYYYMMDDHHMMSS] + const fldPath = `${submissionInfo.project.code}${cpyCode}${timestamp}`; + + // 파일 정보 생성 (새로운 API 형식) const fileInfo: FileInfoWithBuffer = { + CPY_CD: cpyCode, + FILE_NM: fileName, + OFDC_NO: null, PROJ_NO: submissionInfo.project.code, - SHI_DOC_NO: submissionInfo.document.docNumber, - STAGE_NAME: submissionInfo.stage.stageName, - REVISION_NO: String(submissionInfo.submission.revisionNumber), - VNDR_CD: submissionInfo.vendor?.vendorCode || '', - VNDR_NM: submissionInfo.vendor?.vendorName || '', - FILE_NAME: attachment.fileName, - FILE_SIZE: fileBuffer.length, // 실제 파일 크기 사용 - CONTENT_TYPE: attachment.mimeType || 'application/octet-stream', - UPLOAD_DATE: new Date().toISOString(), - UPLOADED_BY: submissionInfo.submission.submittedBy, - STATUS: 'PENDING', - COMMENT: `Revision ${submissionInfo.submission.revisionNumber} - ${submissionInfo.stage.stageName}`, + OWN_DOC_NO: ownDocNo, + REV_NO: revNo, + STAGE: submissionInfo.stage.stageName, + STAT: 'SCW03', // Completed 상태 + FILE_SZ: String(fileBuffer.length), + FLD_PATH: fldPath, fileBuffer: fileBuffer, - attachment: attachment, + attachment: { + id: attachment.id, + fileName: attachment.fileName, + mimeType: attachment.mimeType, + storagePath: attachment.storagePath, + storageUrl: attachment.storageUrl, + }, + // 네트워크 경로 생성을 위한 추가 정보 + _timestamp: timestamp, + _extension: extension, }; filesWithContent.push(fileInfo); @@ -794,12 +911,19 @@ export class ShiBuyerSystemAPI { private async sendToInBox( files: FileInfoWithBuffer[] ): Promise<SaveInBoxListResponse> { - // fileBuffer와 attachment를 제외한 메타데이터만 전송 - const fileMetadata = files.map( - ({ fileBuffer, attachment, ...metadata }) => metadata as InBoxFileInfo - ); + // fileBuffer, attachment, _timestamp, _extension을 제외한 메타데이터만 전송 + const fileMetadata = files.map((file) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { fileBuffer, attachment, _timestamp, _extension, ...metadata } = file; + return metadata as InBoxFileInfo; + }); + + // 새로운 API 형식에 맞게 요청 생성 + const request: SaveInBoxListRequest = { + externalInboxLists: fileMetadata + }; - const request = { files: fileMetadata }; + console.log('SaveInBoxList 요청:', JSON.stringify(request, null, 2)); const response = await fetch(`${this.ddcUrl}/SaveInBoxList`, { method: 'POST', @@ -817,6 +941,8 @@ export class ShiBuyerSystemAPI { const data = await response.json(); + console.log('SaveInBoxList 응답:', JSON.stringify(data, null, 2)); + // 응답 구조 확인 및 처리 if (!data.SaveInBoxListResult) { return { @@ -824,11 +950,23 @@ export class ShiBuyerSystemAPI { success: true, message: '전송 완료', processedCount: files.length, - files: files.map((f) => ({ - fileName: f.FILE_NAME, - networkPath: `\\\\network\\share\\${f.PROJ_NO}\\${f.SHI_DOC_NO}\\${f.FILE_NAME}`, - status: 'READY', - })), + files: files.map((f) => { + // 레거시 네트워크 경로 생성 + const networkPath = this.generateNetworkPath( + f.PROJ_NO, + f.CPY_CD, + f._timestamp, + f.OWN_DOC_NO, + f.REV_NO, + f.STAGE, + f._extension + ); + return { + fileName: f.FILE_NM, + networkPath: networkPath, + status: 'READY', + }; + }), }, }; } @@ -837,15 +975,23 @@ export class ShiBuyerSystemAPI { } /** - * SMB 마운트 경로에 파일 저장 (새로운 경로 규칙 적용) + * SMB 마운트 경로에 파일 저장 (레거시 경로 규칙 적용) */ private async saveFilesToNetworkPaths( filesWithContent: FileInfoWithBuffer[] ) { for (const fileInfo of filesWithContent) { try { - // 새로운 경로 규칙에 따라 마운트 경로 생성 - const targetPath = this.generateMountPath(fileInfo); + // 레거시 경로 규칙에 따라 마운트 경로 생성 + const targetPath = this.generateMountPath( + fileInfo.PROJ_NO, + fileInfo.CPY_CD, + fileInfo._timestamp, + fileInfo.OWN_DOC_NO, + fileInfo.REV_NO, + fileInfo.STAGE, + fileInfo._extension + ); // 디렉토리 생성 (없는 경우) const targetDir = path.dirname(targetPath); @@ -854,25 +1000,37 @@ export class ShiBuyerSystemAPI { // 파일 저장 await fs.writeFile(targetPath, fileInfo.fileBuffer); - console.log(`파일 저장 완료: ${fileInfo.FILE_NAME} -> ${targetPath}`); + console.log(`파일 저장 완료: ${fileInfo.FILE_NM} -> ${targetPath}`); console.log( - `생성된 경로 구조: proj_no=${fileInfo.PROJ_NO}, cpyCd=${ - fileInfo.VNDR_CD - }, timestamp=${this.getTimestamp()}` + `생성된 경로 구조: ${fileInfo.PROJ_NO}/${fileInfo.CPY_CD}/${fileInfo._timestamp}/[${fileInfo.OWN_DOC_NO}]_${fileInfo.REV_NO}_${fileInfo.STAGE}_${fileInfo._timestamp.substring(0, 8)}.${fileInfo._extension}` + ); + + // 네트워크 경로 생성 (레거시 형식) + const networkPath = this.generateNetworkPath( + fileInfo.PROJ_NO, + fileInfo.CPY_CD, + fileInfo._timestamp, + fileInfo.OWN_DOC_NO, + fileInfo.REV_NO, + fileInfo.STAGE, + fileInfo._extension ); - // DB에 마운트 경로 업데이트 (네트워크 경로 대신 마운트 경로 저장) + console.log(`네트워크 경로: ${networkPath}`); + console.log(`FLD_PATH (API 전송용): ${fileInfo.FLD_PATH}`); + + // DB에 경로 정보 업데이트 await db .update(stageSubmissionAttachments) .set({ - buyerSystemUrl: targetPath, // 생성된 마운트 경로 저장 + buyerSystemUrl: networkPath, // 네트워크 경로 저장 (SHI 시스템에서 접근 가능한 경로) buyerSystemStatus: 'UPLOADED', lastModifiedBy: 'EVCP' }) .where(eq(stageSubmissionAttachments.id, fileInfo.attachment.id)) } catch (error) { - console.error(`파일 저장 실패: ${fileInfo.FILE_NAME}`, error) + console.error(`파일 저장 실패: ${fileInfo.FILE_NM}`, error) // 개별 파일 실패는 전체 프로세스를 중단하지 않음 } } @@ -885,12 +1043,12 @@ export class ShiBuyerSystemAPI { submissionId: number, status: string, error?: string | null, - additionalData?: any + additionalData?: Record<string, string | number | Date | null> ) { const updateData: any = { syncStatus: status, lastSyncedAt: new Date(), - syncError: error, + syncError: error ?? null, lastModifiedBy: 'EVCP', ...additionalData } |
