summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/plant/shi-buyer-system-api.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-22 21:10:24 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-22 21:10:24 +0900
commit6b2a561265fb649398e1770f720365ee10f542e9 (patch)
tree50ae6453939d8ce4be850d603450d03d31e05442 /lib/vendor-document-list/plant/shi-buyer-system-api.ts
parent2ecf88af270c5d044a853793f72f3a4536e05b89 (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.ts356
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
}