diff options
Diffstat (limited to 'lib/vendor-document-list')
| -rw-r--r-- | lib/vendor-document-list/import-service.ts | 809 |
1 files changed, 689 insertions, 120 deletions
diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts index e2cf31f0..5af49215 100644 --- a/lib/vendor-document-list/import-service.ts +++ b/lib/vendor-document-list/import-service.ts @@ -2,7 +2,7 @@ import db from "@/db/db" import { documents, issueStages, contracts, projects, vendors, revisions, documentAttachments } from "@/db/schema" -import { eq, and, sql } from "drizzle-orm" +import { eq, and, sql, asc } from "drizzle-orm" import { writeFile, mkdir } from "fs/promises" import { join } from "path" import { v4 as uuidv4 } from "uuid" @@ -145,24 +145,40 @@ class ImportService { * DOLCE 시스템에서 문서 목록 가져오기 */ async importFromExternalSystem( - projectId: number, + projectId: number, // ✅ projectId sourceSystem: string = 'DOLCE' ): Promise<ImportResult> { try { + console.log('\n') + console.log('🚀'.repeat(40)) + console.log('🚀 importFromExternalSystem 호출됨!') + console.log('🚀'.repeat(40)) debugProcess(`DOLCE 가져오기 시작`, { projectId, sourceSystem }) + // 🔥 세션을 한 번만 가져와서 재사용 + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + debugError(`세션 없음 - 인증 필요`, { projectId }) + throw new Error("인증이 필요합니다.") + } + const vendorId = Number(session.user.companyId) + debugProcess(`세션 조회 완료`, { vendorId, userId: session.user.id }) + // 1. 계약 정보를 통해 프로젝트 코드와 벤더 코드 조회 - const contractInfo = await this.getContractInfoById(projectId) - if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { - debugError(`프로젝트 코드 또는 벤더 코드 없음`, { projectId }) - throw new Error(`Project code or vendor code not found for contract ${projectId}`) + const contractInfo = await this.getContractInfoByProjectId(projectId, vendorId) + if (!contractInfo?.projectCode || !contractInfo?.vendorCode || !contractInfo?.contractId) { + debugError(`계약 정보 없음`, { projectId, vendorId }) + throw new Error(`Contract info not found for project ${projectId}`) } - // debugLog(`계약 정보 조회 완료`, { - // projectId, - // projectCode: contractInfo.projectCode, - // vendorCode: contractInfo.vendorCode - // }) + const contractId = contractInfo.contractId // contract.id를 가져옴 + + debugProcess(`계약 정보 조회 완료`, { + contractId, + projectId, + projectCode: contractInfo.projectCode, + vendorCode: contractInfo.vendorCode + }) // 2. 각 drawingKind별로 데이터 조회 const allDocuments: DOLCEDocument[] = [] @@ -187,7 +203,7 @@ class ImportService { } if (allDocuments.length === 0) { - debugProcess(`가져올 문서 없음`, { projectId }) + debugProcess(`가져올 문서 없음`, { contractId, projectId }) return { success: true, newCount: 0, @@ -203,6 +219,7 @@ class ImportService { } debugProcess(`전체 문서 수`, { + contractId, projectId, totalDocuments: allDocuments.length, byDrawingKind: { @@ -230,7 +247,7 @@ class ImportService { drawingKind: dolceDoc.DrawingKind }) - const result = await this.syncSingleDocument(projectId, dolceDoc, sourceSystem) + const result = await this.syncSingleDocument(contractId, projectId, vendorId, dolceDoc, sourceSystem) if (result === 'NEW') { newCount++ @@ -254,17 +271,23 @@ class ImportService { try { const revisionResult = await this.syncDocumentRevisions( projectId, - dolceDoc, - sourceSystem + dolceDoc ) newRevisionsCount += revisionResult.newCount updatedRevisionsCount += revisionResult.updatedCount // 5. 파일 첨부 동기화 처리 (Category가 FS인 것만) + console.log(`📎 첨부파일 동기화 시도: ${dolceDoc.DrawingNo} [${dolceDoc.Discipline}]`) const attachmentResult = await this.syncDocumentAttachments( - dolceDoc, - sourceSystem + dolceDoc ) + console.log(`📎 첨부파일 동기화 결과:`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + new: attachmentResult.newCount, + updated: attachmentResult.updatedCount, + downloaded: attachmentResult.downloadedCount + }) newAttachmentsCount += attachmentResult.newCount updatedAttachmentsCount += attachmentResult.updatedCount downloadedFilesCount += attachmentResult.downloadedCount @@ -278,16 +301,32 @@ class ImportService { } } catch (error) { - debugError(`문서 동기화 실패`, { - drawingNo: dolceDoc.DrawingNo, - error + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + const errorStack = error instanceof Error ? error.stack : undefined + + debugError(`❌ 문서 동기화 실패`, { + drawingNo: dolceDoc.DrawingNo, + drawingKind: dolceDoc.DrawingKind, + discipline: dolceDoc.Discipline, + errorMessage, + errorStack }) - errors.push(`Document ${dolceDoc.DrawingNo}: ${error instanceof Error ? error.message : 'Unknown error'}`) + + console.error(`❌ 문서 동기화 실패 상세:`, { + 문서번호: dolceDoc.DrawingNo, + 문서종류: dolceDoc.DrawingKind, + discipline: dolceDoc.Discipline, + 에러메시지: errorMessage, + 스택: errorStack + }) + + errors.push(`Document ${dolceDoc.DrawingNo}: ${errorMessage}`) skippedCount++ } } debugSuccess(`DOLCE 가져오기 완료`, { + contractId, projectId, newCount, updatedCount, @@ -321,32 +360,35 @@ class ImportService { } /** - * 계약 ID로 프로젝트 코드와 벤더 코드 조회 + * 프로젝트 ID로 계약 정보 조회 */ - private async getContractInfoById(projectId: number): Promise<{ + private async getContractInfoByProjectId(projectId: number, vendorId: number): Promise<{ + contractId: number; // 🔥 contract.id 반환 projectCode: string; vendorCode: string; } | null> { - const session = await getServerSession(authOptions) - if (!session?.user?.companyId) { - throw new Error("인증이 필요합니다.") - } - - const [result] = await db .select({ + contractId: contracts.id, // 🔥 contract.id 가져오기 projectCode: projects.code, vendorCode: vendors.vendorCode }) .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) .innerJoin(vendors, eq(contracts.vendorId, vendors.id)) - .where(and(eq(contracts.projectId, projectId),eq(contracts.vendorId, Number(session.user.companyId)))) + .where(and( + eq(contracts.projectId, projectId), // ✅ projects.id로 조회 + eq(contracts.vendorId, vendorId) + )) .limit(1) return result?.projectCode && result?.vendorCode - ? { projectCode: result.projectCode, vendorCode: result.vendorCode } + ? { + contractId: result.contractId, // 🔥 contract.id 반환 + projectCode: result.projectCode, + vendorCode: result.vendorCode + } : null } @@ -639,32 +681,47 @@ class ImportService { * 단일 문서 동기화 */ private async syncSingleDocument( + contractId: number, // 🔥 contractId 추가 projectId: number, + vendorId: number, dolceDoc: DOLCEDocument, sourceSystem: string ): Promise<'NEW' | 'UPDATED' | 'SKIPPED'> { - const session = await getServerSession(authOptions) - if (!session?.user?.companyId) { - throw new Error("인증이 필요합니다.") - } - - const vendorId = Number(session.user.companyId) - + debugProcess(`📄 문서 동기화 처리 중`, { + contractId, + projectId, + vendorId, + drawingNo: dolceDoc.DrawingNo, + drawingKind: dolceDoc.DrawingKind, + discipline: dolceDoc.Discipline + }) // 기존 문서 조회 (문서 번호로) + // ✅ projectId + externalDocumentId + discipline로 조회 (유니크 인덱스와 일치) const existingDoc = await db .select() .from(documents) .where(and( eq(documents.projectId, projectId), - eq(documents.docNumber, dolceDoc.DrawingNo), - eq(documents.discipline, dolceDoc.Discipline) + eq(documents.externalDocumentId, dolceDoc.DrawingNo), // externalDocumentId 사용 + eq(documents.discipline, dolceDoc.Discipline), + eq(documents.externalSystemType, sourceSystem) )) .limit(1) + debugProcess(`🔍 기존 문서 조회 결과`, { + projectId, + contractId, + drawingNo: dolceDoc.DrawingNo, + externalDocumentId: dolceDoc.DrawingNo, + found: existingDoc.length > 0, + existingId: existingDoc.length > 0 ? existingDoc[0].id : null + }) + // DOLCE 문서를 DB 스키마에 맞게 변환 const documentData = { + contractId, // 🔥 contractId 추가 - 유니크 인덱스에 필수! projectId, vendorId, docNumber: dolceDoc.DrawingNo, @@ -714,18 +771,41 @@ class ImportService { existing.manager !== documentData.manager if (hasChanges) { + debugProcess(`🔄 문서 업데이트 시작`, { + drawingNo: dolceDoc.DrawingNo, + existingId: existing.id, + changes: { + title: existing.title !== documentData.title, + drawingMoveGbn: existing.drawingMoveGbn !== documentData.drawingMoveGbn, + manager: existing.manager !== documentData.manager + } + }) + await db .update(documents) .set(documentData) .where(eq(documents.id, existing.id)) - console.log(`Updated document: ${dolceDoc.DrawingNo}`) + debugSuccess(`✅ 문서 업데이트 완료`, { + drawingNo: dolceDoc.DrawingNo, + documentId: existing.id + }) return 'UPDATED' } else { + debugProcess(`⏭️ 문서 변경사항 없음 - 스킵`, { + drawingNo: dolceDoc.DrawingNo, + documentId: existing.id + }) return 'SKIPPED' } } else { // 새 문서 생성 + debugProcess(`➕ 새 문서 생성 시작`, { + drawingNo: dolceDoc.DrawingNo, + drawingKind: dolceDoc.DrawingKind, + title: dolceDoc.DrawingName + }) + const [newDoc] = await db .insert(documents) .values({ @@ -734,7 +814,11 @@ class ImportService { }) .returning({ id: documents.id }) - console.log(`Created new document: ${dolceDoc.DrawingNo}`) + debugSuccess(`✅ 새 문서 생성 완료`, { + drawingNo: dolceDoc.DrawingNo, + documentId: newDoc.id, + drawingKind: dolceDoc.DrawingKind + }) return 'NEW' } } @@ -744,8 +828,7 @@ class ImportService { */ private async syncDocumentRevisions( projectId: number, - dolceDoc: DOLCEDocument, - sourceSystem: string + dolceDoc: DOLCEDocument ): Promise<{ newCount: number; updatedCount: number }> { try { // 1. 상세 정보 조회 @@ -782,6 +865,19 @@ class ImportService { .select() .from(issueStages) .where(eq(issueStages.documentId, documentId)) + .orderBy(asc(issueStages.stageOrder)) // 순서대로 정렬 + + console.log(`📋 Issue Stages 목록:`, { + drawingNo: dolceDoc.DrawingNo, + documentId, + totalStages: issueStagesList.length, + stages: issueStagesList.map(s => ({ + id: s.id, + name: s.stageName, + order: s.stageOrder, + status: s.stageStatus + })) + }) let newCount = 0 let updatedCount = 0 @@ -789,18 +885,115 @@ class ImportService { // 3. 각 상세 데이터에 대해 revision 동기화 for (const detailDoc of detailDocs) { try { - // RegisterGroupId로 해당하는 issueStage 찾기 - const matchingStage = issueStagesList.find(stage => { - // RegisterGroupId와 매칭하는 로직 (추후 개선 필요) - return stage.id // 임시로 첫 번째 stage 사용 + console.log(`🔄 Revision 동기화 시도:`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + registerId: detailDoc.RegisterId, + drawingRevNo: detailDoc.DrawingRevNo, + registerSerialNo: detailDoc.RegisterSerialNo, + registerGroupId: detailDoc.RegisterGroupId, + registerGroup: detailDoc.RegisterGroup, + category: detailDoc.Category, + drawingUsage: detailDoc.DrawingUsage, + registerKind: detailDoc.RegisterKind }) + + // issueStage 매칭 로직 (여러 fallback) + let matchingStage = null + + // 1. RegisterGroupId가 유효한 경우 (> 0) ID로 매칭 + if (detailDoc.RegisterGroupId > 0) { + matchingStage = issueStagesList.find(stage => stage.id === detailDoc.RegisterGroupId) + if (matchingStage) { + console.log(`✅ Stage 매칭 (RegisterGroupId):`, { + registerId: detailDoc.RegisterId, + stageId: matchingStage.id, + stageName: matchingStage.stageName, + method: 'RegisterGroupId' + }) + } + } + + // 2. stageName으로 매칭 시도 (DrawingUsage 기반) + if (!matchingStage && detailDoc.DrawingUsage) { + const usageKeywords: Record<string, string[]> = { + 'SUB': ['제출', 'submission', 'submit', 'SUB'], + 'WOR': ['작업', 'work', 'working', 'WOR'], + 'REV': ['검토', 'review', 'REV'], + 'APP': ['승인', 'approval', 'approve', 'APP'] + } + + const keywords = usageKeywords[detailDoc.DrawingUsage] || [] + matchingStage = issueStagesList.find(stage => + keywords.some(keyword => + stage.stageName?.toLowerCase().includes(keyword.toLowerCase()) + ) + ) + + if (matchingStage) { + console.log(`✅ Stage 매칭 (DrawingUsage):`, { + registerId: detailDoc.RegisterId, + stageId: matchingStage.id, + stageName: matchingStage.stageName, + drawingUsage: detailDoc.DrawingUsage, + method: 'DrawingUsage keyword' + }) + } + } + + // 3. Category로 매칭 시도 + if (!matchingStage && detailDoc.Category) { + const categoryKeywords: Record<string, string[]> = { + 'FS': ['발신', 'from shi', 'outgoing'], + 'TS': ['수신', 'to shi', 'incoming'] + } + + const keywords = categoryKeywords[detailDoc.Category] || [] + matchingStage = issueStagesList.find(stage => + keywords.some(keyword => + stage.stageName?.toLowerCase().includes(keyword.toLowerCase()) + ) + ) + + if (matchingStage) { + console.log(`✅ Stage 매칭 (Category):`, { + registerId: detailDoc.RegisterId, + stageId: matchingStage.id, + stageName: matchingStage.stageName, + category: detailDoc.Category, + method: 'Category keyword' + }) + } + } + + // 4. Fallback: stageOrder가 가장 낮은 것 (첫 번째 단계) + if (!matchingStage && issueStagesList.length > 0) { + matchingStage = issueStagesList[0] + console.warn(`⚠️ Stage 매칭 실패 - Fallback 사용:`, { + registerId: detailDoc.RegisterId, + stageId: matchingStage.id, + stageName: matchingStage.stageName, + method: 'fallback (first stage)' + }) + } if (!matchingStage) { - console.warn(`No matching issue stage found for RegisterGroupId: ${detailDoc.RegisterGroupId}`) + console.warn(`⚠️ Issue Stage 없음 - Revision 생성 불가:`, { + drawingNo: dolceDoc.DrawingNo, + registerId: detailDoc.RegisterId, + registerGroupId: detailDoc.RegisterGroupId, + availableStages: issueStagesList.length + }) continue } - const result = await this.syncSingleRevision(matchingStage.id, detailDoc, sourceSystem) + const result = await this.syncSingleRevision(matchingStage.id, detailDoc) + + console.log(`✅ Revision 동기화 완료:`, { + registerId: detailDoc.RegisterId, + result + }) + if (result === 'NEW') { newCount++ } else if (result === 'UPDATED') { @@ -808,7 +1001,12 @@ class ImportService { } } catch (error) { - console.error(`Failed to sync revision ${detailDoc.RegisterId}:`, error) + console.error(`❌ Revision 동기화 실패:`, { + drawingNo: dolceDoc.DrawingNo, + registerId: detailDoc.RegisterId, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }) } } @@ -824,8 +1022,7 @@ class ImportService { * 문서의 첨부파일 동기화 (Category가 FS인 것만) */ private async syncDocumentAttachments( - dolceDoc: DOLCEDocument, - sourceSystem: string + dolceDoc: DOLCEDocument ): Promise<{ newCount: number; updatedCount: number; downloadedCount: number }> { try { debugProcess(`문서 첨부파일 동기화 시작`, { @@ -846,12 +1043,16 @@ class ImportService { const fsDetailDocs = detailDocs.filter(doc => doc.Category === 'FS') if (fsDetailDocs.length === 0) { - debugProcess(`FS 카테고리 문서 없음`, { drawingNo: dolceDoc.DrawingNo }) + debugProcess(`FS 카테고리 문서 없음`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline + }) return { newCount: 0, updatedCount: 0, downloadedCount: 0 } } debugProcess(`FS 문서 발견`, { - drawingNo: dolceDoc.DrawingNo, + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, totalDetails: detailDocs.length, fsDetails: fsDetailDocs.length }) @@ -876,7 +1077,38 @@ class ImportService { .limit(1) if (revisionRecord.length === 0) { - debugWarn(`Revision 없음`, { registerId: detailDoc.RegisterId }) + debugWarn(`⚠️ Revision 없음 - 첨부파일 처리 건너뜀`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + registerId: detailDoc.RegisterId, + uploadId: detailDoc.UploadId, + drawingRevNo: detailDoc.DrawingRevNo, + registerSerialNo: detailDoc.RegisterSerialNo, + message: 'Revision이 DB에 없습니다. syncDocumentRevisions에서 생성 실패했을 가능성이 있습니다.' + }) + + // 🔍 디버깅: 파일 정보가 있는지 확인 + try { + const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) + if (fileInfos.length > 0) { + console.warn(`⚠️ Orphan 파일 발견 (Revision 없음):`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + registerId: detailDoc.RegisterId, + uploadId: detailDoc.UploadId, + fileCount: fileInfos.length, + files: fileInfos.map(f => ({ + fileId: f.FileId, + fileName: f.FileName, + fileSize: f.FileSize + })), + message: 'API에는 파일이 있지만 Revision이 없어서 처리할 수 없습니다.' + }) + } + } catch (error) { + console.error(`파일 정보 조회 실패 (Revision 없음):`, error) + } + continue } @@ -884,35 +1116,77 @@ class ImportService { // 5. 파일 정보 조회 const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) + console.log(`📂 파일 정보 조회 완료:`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + uploadId: detailDoc.UploadId, + totalFiles: fileInfos.length, + activeFiles: fileInfos.filter(f => f.UseYn === 'True').length, + files: fileInfos.map(f => ({ + fileName: f.FileName, + fileSize: f.FileSize, + fileId: f.FileId, + useYn: f.UseYn + })) + }) for (const fileInfo of fileInfos) { + console.log(`🔍 파일 처리 시작:`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + fileName: fileInfo.FileName, + fileId: fileInfo.FileId, + useYn: fileInfo.UseYn, + revisionId + }) + if (fileInfo.UseYn !== 'True') { debugProcess(`비활성 파일 스킵`, { fileName: fileInfo.FileName }) continue } - const result = await this.syncSingleAttachment( - revisionId, - fileInfo, - detailDoc.CreateUserId, - sourceSystem - ) + try { + const result = await this.syncSingleAttachment( + revisionId, + fileInfo, + detailDoc.CreateUserId + ) - if (result === 'NEW') { - newCount++ - downloadedCount++ - } else if (result === 'UPDATED') { - updatedCount++ + if (result === 'NEW') { + newCount++ + downloadedCount++ + } else if (result === 'UPDATED') { + updatedCount++ + } + } catch (attachmentError) { + debugError(`⚠️ 개별 첨부파일 동기화 실패 (계속 진행)`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + fileName: fileInfo.FileName, + fileId: fileInfo.FileId, + revisionId, + registerId: detailDoc.RegisterId, + error: attachmentError, + errorMessage: attachmentError instanceof Error ? attachmentError.message : String(attachmentError) + }) + // 개별 첨부파일 실패는 전체 프로세스를 중단하지 않음 + continue } } } catch (error) { - debugError(`첨부파일 동기화 실패`, { registerId: detailDoc.RegisterId, error }) + debugError(`첨부파일 동기화 실패`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + registerId: detailDoc.RegisterId, + error + }) } } debugSuccess(`문서 첨부파일 동기화 완료`, { drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, newCount, updatedCount, downloadedCount @@ -921,7 +1195,11 @@ class ImportService { return { newCount, updatedCount, downloadedCount } } catch (error) { - debugError(`문서 첨부파일 동기화 실패`, { drawingNo: dolceDoc.DrawingNo, error }) + debugError(`문서 첨부파일 동기화 실패`, { + drawingNo: dolceDoc.DrawingNo, + discipline: dolceDoc.Discipline, + error + }) throw error } } @@ -932,8 +1210,7 @@ class ImportService { private async syncSingleAttachment( revisionId: number, fileInfo: DOLCEFileInfo, - userId: string, - sourceSystem: string + userId: string ): Promise<'NEW' | 'UPDATED' | 'SKIPPED'> { try { debugProcess(`단일 첨부파일 동기화 시작`, { @@ -954,23 +1231,122 @@ class ImportService { .limit(1) if (existingAttachment.length > 0) { - // 이미 존재하는 파일인 경우, 필요시 업데이트 로직 추가 - debugProcess(`파일 이미 존재`, { fileName: fileInfo.FileName, fileId: fileInfo.FileId }) + // ✅ 변경사항 체크 (fileName, fileSize) + const existing = existingAttachment[0] + + // 타입 안전 비교 (fileName은 문자열, fileSize는 숫자로 변환) + const fileNameMatch = existing.fileName === fileInfo.FileName + const fileSizeMatch = Number(existing.fileSize) === Number(fileInfo.FileSize) + + debugProcess(`첨부파일 비교`, { + fileId: fileInfo.FileId, + revisionId, + fileNameMatch, + fileSizeMatch, + existing: { + fileName: existing.fileName, + fileSize: existing.fileSize, + fileNameType: typeof existing.fileName, + fileSizeType: typeof existing.fileSize + }, + dolce: { + fileName: fileInfo.FileName, + fileSize: fileInfo.FileSize, + fileNameType: typeof fileInfo.FileName, + fileSizeType: typeof fileInfo.FileSize + } + }) + + const hasChanges = !fileNameMatch || !fileSizeMatch + + if (hasChanges) { + // 변경사항이 있으면 업데이트 + debugProcess(`파일 정보 변경 감지 - 업데이트`, { + fileName: fileInfo.FileName, + fileId: fileInfo.FileId, + changes: { + fileName: !fileNameMatch, + fileSize: !fileSizeMatch + } + }) + + await db + .update(documentAttachments) + .set({ + fileName: fileInfo.FileName, + fileSize: fileInfo.FileSize, + updatedAt: new Date() + }) + .where(eq(documentAttachments.id, existing.id)) + + debugSuccess(`첨부파일 정보 업데이트 완료`, { + fileName: fileInfo.FileName, + fileId: fileInfo.FileId + }) + return 'UPDATED' + } + + // 변경사항 없으면 SKIPPED + debugProcess(`파일 이미 존재 - 변경사항 없음`, { + fileName: fileInfo.FileName, + fileId: fileInfo.FileId + }) return 'SKIPPED' } // 파일 다운로드 - debugProcess(`파일 다운로드 시작`, { fileName: fileInfo.FileName, fileId: fileInfo.FileId }) - const fileBuffer = await this.downloadFileFromDOLCE( - fileInfo.FileId, - userId, - fileInfo.FileName - ) + debugProcess(`📥 [1/3] 파일 다운로드 시작`, { + fileName: fileInfo.FileName, + fileId: fileInfo.FileId, + revisionId + }) + + let fileBuffer: Buffer + try { + fileBuffer = await this.downloadFileFromDOLCE( + fileInfo.FileId, + userId, + fileInfo.FileName + ) + debugSuccess(`✅ [1/3] 파일 다운로드 완료`, { + fileName: fileInfo.FileName, + bufferSize: fileBuffer.length + }) + } catch (downloadError) { + debugError(`❌ [1/3] 파일 다운로드 실패`, { + fileName: fileInfo.FileName, + fileId: fileInfo.FileId, + error: downloadError + }) + throw downloadError + } // 로컬 파일 시스템에 저장 - const savedFile = await this.saveFileToLocal(fileBuffer, fileInfo.FileName) + debugProcess(`💾 [2/3] 로컬 저장 시작`, { fileName: fileInfo.FileName }) + + let savedFile: { filePath: string; fileSize: number } + try { + savedFile = await this.saveFileToLocal(fileBuffer, fileInfo.FileName) + debugSuccess(`✅ [2/3] 로컬 저장 완료`, { + fileName: fileInfo.FileName, + filePath: savedFile.filePath, + fileSize: savedFile.fileSize + }) + } catch (saveError) { + debugError(`❌ [2/3] 로컬 저장 실패`, { + fileName: fileInfo.FileName, + error: saveError + }) + throw saveError + } // DB에 첨부파일 정보 저장 + debugProcess(`💿 [3/3] DB Insert 시작`, { + fileName: fileInfo.FileName, + revisionId, + fileId: fileInfo.FileId + }) + const attachmentData = { revisionId, fileName: fileInfo.FileName, @@ -986,11 +1362,34 @@ class ImportService { updatedAt: new Date() } - await db - .insert(documentAttachments) - .values(attachmentData) + debugProcess(`💿 [3/3] DB Insert 데이터`, attachmentData) + + try { + const insertResult = await db + .insert(documentAttachments) + .values(attachmentData) + .returning({ id: documentAttachments.id }) + + debugSuccess(`✅ [3/3] DB Insert 완료`, { + fileName: fileInfo.FileName, + fileId: fileInfo.FileId, + insertedId: insertResult[0]?.id, + filePath: savedFile.filePath, + fileSize: savedFile.fileSize + }) + } catch (insertError) { + debugError(`❌ [3/3] DB Insert 실패`, { + fileName: fileInfo.FileName, + fileId: fileInfo.FileId, + revisionId, + error: insertError, + errorMessage: insertError instanceof Error ? insertError.message : String(insertError), + errorStack: insertError instanceof Error ? insertError.stack : undefined + }) + throw insertError + } - debugSuccess(`새 첨부파일 생성 완료`, { + debugSuccess(`🎉 새 첨부파일 생성 완료 (전체 프로세스)`, { fileName: fileInfo.FileName, fileId: fileInfo.FileId, filePath: savedFile.filePath, @@ -1013,8 +1412,7 @@ class ImportService { */ private async syncSingleRevision( issueStageId: number, - detailDoc: DOLCEDetailDocument, - sourceSystem: string + detailDoc: DOLCEDetailDocument ): Promise<'NEW' | 'UPDATED' | 'SKIPPED'> { console.log(detailDoc,"detailDoc") @@ -1110,12 +1508,14 @@ class ImportService { const { usage, usageType } = this.mapRegisterKindToUsage(detailDoc.RegisterKind) // DOLCE 상세 데이터를 revisions 스키마에 맞게 변환 + const submittedDate = this.convertDolceDateToDate(detailDoc.CreateDt) + const revisionData = { - serialNo:detailDoc.RegisterSerialNo , + serialNo: String(detailDoc.RegisterSerialNo), issueStageId, revision: detailDoc.DrawingRevNo, uploaderType, - registerSerialNoMax:detailDoc.RegisterSerialNoMax, + registerSerialNoMax: String(detailDoc.RegisterSerialNoMax), // uploaderName: detailDoc.CreateUserNM, usage, usageType, @@ -1123,7 +1523,7 @@ class ImportService { externalUploadId: detailDoc.UploadId, registerId: detailDoc.RegisterId, // 🆕 항상 최신 registerId로 업데이트 comment: detailDoc.SHINote, - submittedDate: this.convertDolceDateToDate(detailDoc.CreateDt), + submittedDate: submittedDate ? submittedDate.toISOString().split('T')[0] : null, // Date를 YYYY-MM-DD string으로 변환 updatedAt: new Date() } @@ -1504,29 +1904,39 @@ class ImportService { * 가져오기 상태 조회 - 에러 시 안전한 기본값 반환 */ async getImportStatus( - projectId: number, + projectId: number, // ✅ projectId sourceSystem: string = 'DOLCE' ): Promise<ImportStatus> { try { - // 마지막 가져오기 시간 조회 - const [lastImport] = await db - .select({ - lastSynced: sql<string>`MAX(${documents.externalSyncedAt})` - }) - .from(documents) - .where(and( - eq(documents.projectId, projectId), - eq(documents.externalSystemType, sourceSystem) - )) + // 세션 조회 + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + console.warn(`Session not found for import status check`) + return { + lastImportAt: undefined, + availableDocuments: 0, + newDocuments: 0, + updatedDocuments: 0, + availableRevisions: 0, + newRevisions: 0, + updatedRevisions: 0, + availableAttachments: 0, + newAttachments: 0, + updatedAttachments: 0, + importEnabled: false, + error: '세션이 없습니다. 다시 로그인해주세요.' + } + } + const vendorId = Number(session.user.companyId) // 프로젝트 코드와 벤더 코드 조회 - const contractInfo = await this.getContractInfoById(projectId) + const contractInfo = await this.getContractInfoByProjectId(projectId, vendorId) // 🔥 계약 정보가 없으면 기본 상태 반환 (에러 throw 하지 않음) if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { - console.warn(`Project code or vendor code not found for contract ${projectId}`) + console.warn(`Contract not found for project ${projectId}`) return { - lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, + lastImportAt: undefined, availableDocuments: 0, newDocuments: 0, updatedDocuments: 0, @@ -1536,10 +1946,23 @@ async getImportStatus( availableAttachments: 0, newAttachments: 0, updatedAttachments: 0, - importEnabled: false, // 🔥 계약 정보가 없으면 import 비활성화 - error: `Contract ${projectId}에 대한 프로젝트 코드 또는 벤더 코드를 찾을 수 없습니다.` // 🔥 에러 메시지 추가 + importEnabled: false, + error: `Project ${projectId}에 대한 계약 정보를 찾을 수 없습니다.` } } + + const contractId = contractInfo.contractId // 🔥 contract.id 추출 + + // 마지막 가져오기 시간 조회 + const [lastImport] = await db + .select({ + lastSynced: sql<string>`MAX(${documents.externalSyncedAt})` + }) + .from(documents) + .where(and( + eq(documents.contractId, contractId), // ✅ contractId로 조회 + eq(documents.externalSystemType, sourceSystem) + )) let availableDocuments = 0 let newDocuments = 0 @@ -1550,6 +1973,28 @@ async getImportStatus( let availableAttachments = 0 let newAttachments = 0 let updatedAttachments = 0 + + // 🔍 디버깅용: 새로운 attachment 상세 정보 수집 + const newAttachmentDetails: Array<{ + fileId: string + fileName: string + fileSize: number + revisionId: number + documentNo: string + discipline: string + revision: string + }> = [] + + const updatedAttachmentDetails: Array<{ + fileId: string + fileName: string + fileSize: number + revisionId: number + documentNo: string + discipline: string + revision: string + changes: { fileName: boolean; fileSize: boolean } + }> = [] try { // 각 drawingKind별로 확인 @@ -1566,13 +2011,14 @@ async getImportStatus( // 신규/업데이트 문서 수 계산 for (const externalDoc of externalDocs) { - const existing = await db + const existing = await db .select({ id: documents.id, updatedAt: documents.updatedAt }) .from(documents) .where(and( - eq(documents.projectId, projectId), - eq(documents.docNumber, externalDoc.DrawingNo), - eq(documents.discipline, externalDoc.Discipline) + eq(documents.projectId, projectId), // ✅ projectId로 조회 + eq(documents.externalDocumentId, externalDoc.DrawingNo), // externalDocumentId 사용 + eq(documents.discipline, externalDoc.Discipline), + eq(documents.externalSystemType, sourceSystem) )) .limit(1) @@ -1691,43 +2137,122 @@ async getImportStatus( // FS Category 문서의 첨부파일 확인 if (detailDoc.Category === 'FS' && detailDoc.UploadId) { try { + console.log(`🔍 [getImportStatus] FileInfoList 조회 시작:`, { + drawingNo: externalDoc.DrawingNo, + discipline: externalDoc.Discipline, + registerId: detailDoc.RegisterId, + uploadId: detailDoc.UploadId + }) + const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) + + console.log(`🔍 [getImportStatus] FileInfoList 조회 결과:`, { + drawingNo: externalDoc.DrawingNo, + discipline: externalDoc.Discipline, + uploadId: detailDoc.UploadId, + totalFiles: fileInfos.length, + files: fileInfos.map(f => ({ + fileId: f.FileId, + fileName: f.FileName, + fileSize: f.FileSize, + useYn: f.UseYn + })) + }) + availableAttachments += fileInfos.filter(f => f.UseYn === 'True').length for (const fileInfo of fileInfos) { if (fileInfo.UseYn !== 'True') continue - // 1. 먼저 attachment가 존재하는지 확인 + // 1. 먼저 해당 revision의 attachment가 존재하는지 확인 + // ✅ revisionId를 찾기 위해 먼저 revision 조회 + if (!existingRevision) { + // ⚠️ revision이 없으면 orphan attachment (처리 불가) + console.warn(`⚠️ [getImportStatus] Orphan 파일 - Revision 없음:`, { + drawingNo: externalDoc.DrawingNo, + discipline: externalDoc.Discipline, + registerId: detailDoc.RegisterId, + uploadId: detailDoc.UploadId, + fileId: fileInfo.FileId, + fileName: fileInfo.FileName, + fileSize: fileInfo.FileSize, + reason: 'Revision not found in DB - cannot process this file' + }) + // ❌ 신규로 카운트하지 않음 (처리할 수 없으므로) + continue + } + const existingAttachment = await db .select({ id: documentAttachments.id, fileName: documentAttachments.fileName, - fileSize: documentAttachments.fileSize, - uploadedAt: documentAttachments.uploadedAt + fileSize: documentAttachments.fileSize }) .from(documentAttachments) - .where(eq(documentAttachments.fileId, fileInfo.FileId)) + .where(and( + eq(documentAttachments.revisionId, existingRevision.id), + eq(documentAttachments.fileId, fileInfo.FileId) + )) .limit(1) if (existingAttachment.length === 0) { // attachment가 존재하지 않음 -> 신규 newAttachments++ + console.log(`✨ [getImportStatus] 신규 Attachment 감지:`, { + drawingNo: externalDoc.DrawingNo, + discipline: externalDoc.Discipline, + registerId: detailDoc.RegisterId, + uploadId: detailDoc.UploadId, + fileId: fileInfo.FileId, + fileName: fileInfo.FileName, + fileSize: fileInfo.FileSize, + revisionId: existingRevision.id + }) + newAttachmentDetails.push({ + fileId: fileInfo.FileId, + fileName: fileInfo.FileName, + fileSize: fileInfo.FileSize, + revisionId: existingRevision.id, + documentNo: externalDoc.DrawingNo, + discipline: externalDoc.Discipline, + revision: detailDoc.DrawingRevNo + }) } else { // 2. attachment가 존재하면 변경사항이 있는지 체크 const existing = existingAttachment[0] - const dolceUploadDate = this.convertDolceDateToDate(fileInfo.FileCreateDT) - const hasChanges = - existing.fileName !== fileInfo.FileName || - existing.fileSize !== fileInfo.FileSize || - (dolceUploadDate && existing.uploadedAt && - dolceUploadDate.getTime() !== existing.uploadedAt.getTime()) + // 타입 안전 비교 (fileName은 문자열, fileSize는 숫자로 변환) + const fileNameMatch = existing.fileName === fileInfo.FileName + const fileSizeMatch = Number(existing.fileSize) === Number(fileInfo.FileSize) + + if (!fileNameMatch || !fileSizeMatch) { + console.log(`🔍 Attachment difference detected:`, { + fileId: fileInfo.FileId, + revisionId: existingRevision.id, + fileNameMatch, + fileSizeMatch, + existing: { fileName: existing.fileName, fileSize: existing.fileSize, fileSizeType: typeof existing.fileSize }, + dolce: { fileName: fileInfo.FileName, fileSize: fileInfo.FileSize, fileSizeType: typeof fileInfo.FileSize } + }) + } + + const hasChanges = !fileNameMatch || !fileSizeMatch if (hasChanges) { // 변경사항이 있음 -> 업데이트 대상 updatedAttachments++ + updatedAttachmentDetails.push({ + fileId: fileInfo.FileId, + fileName: fileInfo.FileName, + fileSize: fileInfo.FileSize, + revisionId: existingRevision.id, + documentNo: externalDoc.DrawingNo, + discipline: externalDoc.Discipline, + revision: detailDoc.DrawingRevNo, + changes: { fileName: !fileNameMatch, fileSize: !fileSizeMatch } + }) } - // 변경사항이 없으면 카운트하지 않음 + // ✅ fileId가 같고 fileName, fileSize도 같으면 변경사항 없음 } } } catch (error) { @@ -1748,6 +2273,50 @@ async getImportStatus( // 🔥 외부 API 호출 실패 시에도 기본값 반환 } + // 🔍 최종 diff 요약 출력 + console.log('\n========================================') + console.log('📊 DOLCE 동기화 상태 검사 완료') + console.log('========================================') + console.log(`프로젝트 ID: ${projectId}`) + console.log(`마지막 동기화: ${lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : '없음'}`) + console.log('\n📄 Documents:') + console.log(` - 총 개수: ${availableDocuments}`) + console.log(` - 신규: ${newDocuments}`) + console.log(` - 업데이트: ${updatedDocuments}`) + console.log('\n📝 Revisions:') + console.log(` - 총 개수: ${availableRevisions}`) + console.log(` - 신규: ${newRevisions}`) + console.log(` - 업데이트: ${updatedRevisions}`) + console.log('\n📎 Attachments:') + console.log(` - 총 개수: ${availableAttachments}`) + console.log(` - 신규: ${newAttachments}`) + console.log(` - 업데이트: ${updatedAttachments}`) + + if (newAttachmentDetails.length > 0) { + console.log('\n🆕 신규 Attachments 상세:') + newAttachmentDetails.forEach((att, idx) => { + console.log(` ${idx + 1}. FileID: ${att.fileId}`) + console.log(` - Document: ${att.documentNo} [${att.discipline}] (Rev: ${att.revision})`) + console.log(` - FileName: ${att.fileName}`) + console.log(` - FileSize: ${att.fileSize}`) + console.log(` - RevisionID: ${att.revisionId}`) + }) + } + + if (updatedAttachmentDetails.length > 0) { + console.log('\n🔄 업데이트 Attachments 상세:') + updatedAttachmentDetails.forEach((att, idx) => { + console.log(` ${idx + 1}. FileID: ${att.fileId}`) + console.log(` - Document: ${att.documentNo} [${att.discipline}] (Rev: ${att.revision})`) + console.log(` - FileName: ${att.fileName}`) + console.log(` - FileSize: ${att.fileSize}`) + console.log(` - RevisionID: ${att.revisionId}`) + console.log(` - Changes: fileName=${att.changes.fileName}, fileSize=${att.changes.fileSize}`) + }) + } + + console.log('========================================\n') + return { lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, availableDocuments, |
