diff options
Diffstat (limited to 'lib/pos/sync-rfq-pos-files.ts')
| -rw-r--r-- | lib/pos/sync-rfq-pos-files.ts | 407 |
1 files changed, 407 insertions, 0 deletions
diff --git a/lib/pos/sync-rfq-pos-files.ts b/lib/pos/sync-rfq-pos-files.ts new file mode 100644 index 00000000..acb34f20 --- /dev/null +++ b/lib/pos/sync-rfq-pos-files.ts @@ -0,0 +1,407 @@ +'use server'; + +import db from '@/db/db'; +import { rfqPrItems, rfqLastAttachments, rfqLastAttachmentRevisions } from '@/db/schema/rfqLast'; +import { eq, and, ne } from 'drizzle-orm'; +import { getDcmtmIdByMaterialCode } from './get-dcmtm-id'; +import { getEncryptDocumentumFile } from './get-pos'; +import { downloadPosFile } from './download-pos-file'; +import type { PosFileSyncResult } from './types'; +import path from 'path'; +import fs from 'fs/promises'; +import { revalidatePath } from 'next/cache'; +import { debugLog, debugError, debugSuccess, debugProcess, debugWarn } from '@/lib/debug-utils'; + + +/** + * RFQ의 모든 PR Items의 MATNR로 POS 파일을 조회하고 서버에 다운로드하여 저장 + */ +export async function syncRfqPosFiles( + rfqId: number, + userId: number +): Promise<PosFileSyncResult> { + debugLog(`🚀 POS 파일 동기화 시작`, { rfqId, userId }); + + const result: PosFileSyncResult = { + success: false, + processedCount: 0, + successCount: 0, + failedCount: 0, + errors: [], + details: [] + }; + + try { + // 1. RFQ의 모든 PR Items 조회 (materialCode 중복 제거) + debugProcess(`📋 RFQ PR Items 조회 시작 (RFQ ID: ${rfqId})`); + + const prItems = await db + .selectDistinct({ + materialCode: rfqPrItems.materialCode, + materialDescription: rfqPrItems.materialDescription, + }) + .from(rfqPrItems) + .where(and( + eq(rfqPrItems.rfqsLastId, rfqId), + // materialCode가 null이 아닌 것만 + )) + .then(items => items.filter(item => item.materialCode && item.materialCode.trim() !== '')); + + debugLog(`📦 조회된 PR Items`, { + totalCount: prItems.length, + materialCodes: prItems.map(item => item.materialCode) + }); + + if (prItems.length === 0) { + debugWarn(`⚠️ 처리할 자재코드가 없습니다 (RFQ ID: ${rfqId})`); + result.errors.push('처리할 자재코드가 없습니다.'); + return result; + } + + result.processedCount = prItems.length; + debugSuccess(`✅ 처리할 자재코드 ${prItems.length}개 발견`); + + // 2. 각 자재코드별로 POS 파일 처리 + debugProcess(`🔄 자재코드별 POS 파일 처리 시작`); + + for (const prItem of prItems) { + const materialCode = prItem.materialCode!; + debugLog(`📋 자재코드 처리 시작: ${materialCode}`); + + try { + // 2-1. 자재코드로 DCMTM_ID 조회 + debugProcess(`🔍 DCMTM_ID 조회 시작 (자재코드: ${materialCode})`); + const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode }); + + debugLog(`🎯 DCMTM_ID 조회 결과`, { + materialCode, + success: dcmtmResult.success, + fileCount: dcmtmResult.files?.length || 0, + files: dcmtmResult.files + }); + + if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) { + debugWarn(`⚠️ DCMTM_ID 조회 실패 또는 파일 없음 (자재코드: ${materialCode})`, dcmtmResult.error); + result.details.push({ + materialCode, + status: 'no_files', + error: dcmtmResult.error || 'POS 파일을 찾을 수 없음' + }); + continue; + } + + // 여러 파일이 있을 경우 첫 번째 파일만 처리 + const posFile = dcmtmResult.files[0]; + debugLog(`📁 처리할 POS 파일 선택`, { + materialCode, + selectedFile: posFile, + totalFiles: dcmtmResult.files.length + }); + + // 2-2. POS API로 파일 경로 가져오기 + debugProcess(`🌐 POS API 호출 시작 (DCMTM_ID: ${posFile.dcmtmId})`); + const posResult = await getEncryptDocumentumFile({ + objectID: posFile.dcmtmId + }); + + debugLog(`🌐 POS API 호출 결과`, { + materialCode, + dcmtmId: posFile.dcmtmId, + success: posResult.success, + resultPath: posResult.result, + error: posResult.error + }); + + if (!posResult.success || !posResult.result) { + debugError(`❌ POS API 호출 실패 (자재코드: ${materialCode})`, posResult.error); + result.details.push({ + materialCode, + fileName: posFile.fileName, + status: 'failed', + error: posResult.error || 'POS 파일 경로 조회 실패' + }); + result.failedCount++; + continue; + } + + // 2-3. 내부망에서 파일 다운로드 + debugProcess(`⬇️ 파일 다운로드 시작 (경로: ${posResult.result})`); + const downloadResult = await downloadPosFile({ + relativePath: posResult.result + }); + + debugLog(`⬇️ 파일 다운로드 결과`, { + materialCode, + success: downloadResult.success, + fileName: downloadResult.fileName, + fileSize: downloadResult.fileBuffer?.length, + mimeType: downloadResult.mimeType, + error: downloadResult.error + }); + + if (!downloadResult.success || !downloadResult.fileBuffer) { + debugError(`❌ 파일 다운로드 실패 (자재코드: ${materialCode})`, downloadResult.error); + result.details.push({ + materialCode, + fileName: posFile.fileName, + status: 'failed', + error: downloadResult.error || '파일 다운로드 실패' + }); + result.failedCount++; + continue; + } + + // 2-4. 서버 파일 시스템에 저장 + debugProcess(`💾 서버 파일 저장 시작 (파일명: ${downloadResult.fileName || `${materialCode}.pdf`})`); + const saveResult = await saveFileToServer( + downloadResult.fileBuffer, + downloadResult.fileName || `${materialCode}.pdf` + ); + + debugLog(`💾 서버 파일 저장 결과`, { + materialCode, + success: saveResult.success, + filePath: saveResult.filePath, + fileName: saveResult.fileName, + error: saveResult.error + }); + + if (!saveResult.success) { + debugError(`❌ 서버 파일 저장 실패 (자재코드: ${materialCode})`, saveResult.error); + result.details.push({ + materialCode, + fileName: posFile.fileName, + status: 'failed', + error: saveResult.error || '서버 파일 저장 실패' + }); + result.failedCount++; + continue; + } + + // 2-5. 데이터베이스에 첨부파일 정보 저장 + debugProcess(`🗄️ DB 첨부파일 정보 저장 시작 (자재코드: ${materialCode})`); + const dbResult = await saveAttachmentToDatabase( + rfqId, + materialCode, + { dcmtmId: posFile.dcmtmId, fileName: posFile.fileName }, + saveResult.filePath!, + saveResult.fileName!, + downloadResult.fileBuffer.length, + downloadResult.mimeType || 'application/pdf', + userId + ); + + debugLog(`🗄️ DB 저장 결과`, { + materialCode, + success: dbResult.success, + error: dbResult.error + }); + + if (!dbResult.success) { + debugError(`❌ DB 저장 실패 (자재코드: ${materialCode})`, dbResult.error); + result.details.push({ + materialCode, + fileName: posFile.fileName, + status: 'failed', + error: dbResult.error || 'DB 저장 실패' + }); + result.failedCount++; + continue; + } + + debugSuccess(`✅ 자재코드 ${materialCode} 처리 완료`); + result.details.push({ + materialCode, + fileName: posFile.fileName, + status: 'success' + }); + result.successCount++; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류'; + debugError(`❌ 자재코드 ${materialCode} 처리 중 예외 발생`, { error: errorMessage, stack: error instanceof Error ? error.stack : undefined }); + result.details.push({ + materialCode, + status: 'failed', + error: errorMessage + }); + result.failedCount++; + result.errors.push(`${materialCode}: ${errorMessage}`); + } + } + + result.success = result.successCount > 0; + + debugLog(`📊 POS 파일 동기화 최종 결과`, { + rfqId, + processedCount: result.processedCount, + successCount: result.successCount, + failedCount: result.failedCount, + success: result.success, + errors: result.errors + }); + + // 캐시 무효화 + debugProcess(`🔄 캐시 무효화 (경로: /evcp/rfq-last/${rfqId})`); + revalidatePath(`/evcp/rfq-last/${rfqId}`); + + debugSuccess(`🎉 POS 파일 동기화 완료 (성공: ${result.successCount}건, 실패: ${result.failedCount}건)`); + return result; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류'; + debugError(`❌ POS 파일 동기화 전체 처리 오류`, { error: errorMessage, stack: error instanceof Error ? error.stack : undefined }); + result.errors.push(`전체 처리 오류: ${errorMessage}`); + return result; + } +} + +/** + * 파일을 서버 파일 시스템에 저장 + */ +async function saveFileToServer( + fileBuffer: Buffer, + originalFileName: string +): Promise<{ + success: boolean; + filePath?: string; + fileName?: string; + error?: string; +}> { + try { + // uploads/pos 디렉토리 생성 + const uploadDir = path.join(process.cwd(), 'uploads', 'pos'); + + try { + await fs.access(uploadDir); + } catch { + await fs.mkdir(uploadDir, { recursive: true }); + } + + // 고유한 파일명 생성 (타임스탬프 + 원본명) + const timestamp = Date.now(); + const sanitizedFileName = originalFileName.replace(/[^a-zA-Z0-9.-]/g, '_'); + const fileName = `${timestamp}_${sanitizedFileName}`; + const filePath = path.join(uploadDir, fileName); + + // 파일 저장 + await fs.writeFile(filePath, fileBuffer); + + return { + success: true, + filePath: `uploads/pos/${fileName}`, // 상대 경로로 저장 + fileName: originalFileName + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '파일 저장 실패' + }; + } +} + +/** + * 첨부파일 정보를 데이터베이스에 저장 + */ +async function saveAttachmentToDatabase( + rfqId: number, + materialCode: string, + posFileInfo: { dcmtmId: string; fileName: string }, + filePath: string, + originalFileName: string, + fileSize: number, + fileType: string, + userId: number +): Promise<{ + success: boolean; + error?: string; +}> { + try { + await db.transaction(async (tx) => { + // 1. 기존 동일한 자재코드의 설계 첨부파일이 있는지 확인 + const existingAttachment = await tx + .select() + .from(rfqLastAttachments) + .where(and( + eq(rfqLastAttachments.rfqId, rfqId), + eq(rfqLastAttachments.attachmentType, '설계'), + eq(rfqLastAttachments.serialNo, materialCode) + )) + .limit(1); + + let attachmentId: number; + + if (existingAttachment.length > 0) { + // 기존 첨부파일이 있으면 업데이트 + attachmentId = existingAttachment[0].id; + + await tx + .update(rfqLastAttachments) + .set({ + currentRevision: 'Rev.1', // 새 리비전으로 업데이트 + description: `${posFileInfo.fileName} (자재코드: ${materialCode})`, + updatedAt: new Date() + }) + .where(eq(rfqLastAttachments.id, attachmentId)); + } else { + // 새 첨부파일 생성 + const [newAttachment] = await tx + .insert(rfqLastAttachments) + .values({ + attachmentType: '설계', + serialNo: materialCode, + rfqId, + currentRevision: 'Rev.0', + description: `${posFileInfo.fileName} (자재코드: ${materialCode})`, + createdBy: userId, + createdAt: new Date(), + updatedAt: new Date() + }) + .returning({ id: rfqLastAttachments.id }); + + attachmentId = newAttachment.id; + } + + // 2. 새 리비전 생성 + const [newRevision] = await tx + .insert(rfqLastAttachmentRevisions) + .values({ + attachmentId, + revisionNo: 'Rev.0', + fileName: path.basename(filePath), + originalFileName, + filePath, + fileSize, + fileType, + isLatest: true, + revisionComment: `POS 시스템에서 자동 동기화됨 (DCMTM_ID: ${posFileInfo.dcmtmId})`, + createdBy: userId + }) + .returning({ id: rfqLastAttachmentRevisions.id }); + + // 3. 첨부파일의 latestRevisionId 업데이트 + await tx + .update(rfqLastAttachments) + .set({ + latestRevisionId: newRevision.id + }) + .where(eq(rfqLastAttachments.id, attachmentId)); + + // 4. 기존 리비전들의 isLatest를 false로 업데이트 + await tx + .update(rfqLastAttachmentRevisions) + .set({ isLatest: false }) + .where(and( + eq(rfqLastAttachmentRevisions.attachmentId, attachmentId), + ne(rfqLastAttachmentRevisions.id, newRevision.id) + )); + }); + + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'DB 저장 실패' + }; + } +} |
