diff options
Diffstat (limited to 'lib/vendor-document-list')
| -rw-r--r-- | lib/vendor-document-list/dolce-upload-service.ts | 18 | ||||
| -rw-r--r-- | lib/vendor-document-list/import-service.ts | 816 | ||||
| -rw-r--r-- | lib/vendor-document-list/service.ts | 18 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx | 3 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/import-from-dolce-button.tsx | 82 |
5 files changed, 852 insertions, 85 deletions
diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts index 627e0eba..8e9d386b 100644 --- a/lib/vendor-document-list/dolce-upload-service.ts +++ b/lib/vendor-document-list/dolce-upload-service.ts @@ -98,7 +98,6 @@ class DOLCEUploadService { } // 3. 각 issueStageId별로 첫 번째 revision 정보를 미리 조회 (Mode 결정용) - const firstRevisionMap = await this.getFirstRevisionMap(revisionsToUpload.map(r => r.issueStageId)) let uploadedDocuments = 0 let uploadedFiles = 0 @@ -134,7 +133,6 @@ class DOLCEUploadService { contractInfo, uploadId, contractInfo.vendorCode, - firstRevisionMap ) const docResult = await this.uploadDocument([dolceDoc], userId) @@ -237,6 +235,7 @@ class DOLCEUploadService { .select({ // revision 테이블 정보 id: revisions.id, + registerId:revisions.registerId, revision: revisions.revision, // revisionNo가 아니라 revision revisionStatus: revisions.revisionStatus, uploaderId: revisions.uploaderId, @@ -293,6 +292,8 @@ class DOLCEUploadService { const attachments = await db .select({ id: documentAttachments.id, + uploadId: documentAttachments.uploadId, + fileId: documentAttachments.fileId, fileName: documentAttachments.fileName, filePath: documentAttachments.filePath, fileType: documentAttachments.fileType, @@ -472,16 +473,15 @@ class DOLCEUploadService { contractInfo: any, uploadId?: string, vendorCode?: string, - firstRevisionMap?: Map<number, string> ): DOLCEDocument { // Mode 결정: 해당 issueStageId의 첫 번째 revision인지 확인 - let mode: "ADD" | "MOD" = "MOD" // 기본값은 MOD + let mode: "ADD" | "MOD" = "MOD" // 기본값은 MOD\ + - if (firstRevisionMap && firstRevisionMap.has(revision.issueStageId)) { - const firstRevision = firstRevisionMap.get(revision.issueStageId) - if (revision.revision === firstRevision) { - mode = "ADD" - } + if(revision.registerId){ + mode = "MOD" + } else{ + mode = "ADD" } // RegisterKind 결정: stageName에 따라 설정 diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts index 344597fa..9a4e44db 100644 --- a/lib/vendor-document-list/import-service.ts +++ b/lib/vendor-document-list/import-service.ts @@ -1,14 +1,24 @@ -// lib/vendor-document-list/import-service.ts - DOLCE API 연동 버전 +// lib/vendor-document-list/import-service.ts - DOLCE API 연동 버전 (파일 다운로드 포함) import db from "@/db/db" -import { documents, issueStages, contracts, projects, vendors } from "@/db/schema" +import { documents, issueStages, contracts, projects, vendors, revisions, documentAttachments } from "@/db/schema" import { eq, and, desc, sql } from "drizzle-orm" +import { writeFile, mkdir } from "fs/promises" +import { join } from "path" +import { v4 as uuidv4 } from "uuid" +import { extname } from "path" +import * as crypto from "crypto" export interface ImportResult { success: boolean newCount: number updatedCount: number skippedCount: number + newRevisionsCount: number + updatedRevisionsCount: number + newAttachmentsCount: number + updatedAttachmentsCount: number + downloadedFilesCount: number errors?: string[] message?: string } @@ -18,6 +28,12 @@ export interface ImportStatus { availableDocuments: number newDocuments: number updatedDocuments: number + availableRevisions: number + newRevisions: number + updatedRevisions: number + availableAttachments: number + newAttachments: number + updatedAttachments: number importEnabled: boolean } @@ -41,19 +57,14 @@ interface DOLCEDocument { AppDwg_ResultDate?: string WorDwg_PlanDate?: string WorDwg_ResultDate?: string - - GTTPreDwg_PlanDate?: string GTTPreDwg_ResultDate?: string GTTWorkingDwg_PlanDate?: string GTTWorkingDwg_ResultDate?: string - FMEAFirst_PlanDate?: string FMEAFirst_ResultDate?: string FMEASecond_PlanDate?: string FMEASecond_ResultDate?: string - - JGbn?: string Manager: string ManagerENM: string @@ -65,7 +76,67 @@ interface DOLCEDocument { SHIDrawingNo?: string } +interface DOLCEDetailDocument { + Status: string + Category: string // TS, FS + CategoryNM: string + CategoryENM: string + RegisterId: string + ProjectNo: string + DrawingNo: string + RegisterGroupId: number + RegisterGroup: number + DrawingName: string + RegisterSerialNoMax: number + RegisterSerialNo: number + DrawingUsage: string + DrawingUsageNM: string + DrawingUsageENM: string + RegisterKind: string + RegisterKindNM: string + RegisterKindENM: string + DrawingRevNo: string + RegisterDesc: string + UploadId: string + ManagerNM: string + Manager: string + UseYn: string + RegCompanyCode: string + RegCompanyNM: string + RegCompanyENM: string + CreateUserENM: string + CreateUserNM: string + CreateUserId: string + CreateDt: string + ModifyUserId: string + ModifyDt: string + Discipline: string + DrawingKind: string + DrawingMoveGbn: string + SHIDrawingNo: string + Receiver: string + SHINote: string +} + +interface DOLCEFileInfo { + FileId: string + UploadId: string + FileSeq: number + FileServerId: string + FileTitle: string + FileDescription: string + FileName: string + FileRelativePath: string + FileSize: number + FileCreateDT: string + FileWriteDT: string + OwnerUserId: string + UseYn: string +} + class ImportService { + private readonly DES_KEY = Buffer.from("4fkkdijg", "ascii") + /** * DOLCE 시스템에서 문서 목록 가져오기 */ @@ -107,6 +178,11 @@ class ImportService { newCount: 0, updatedCount: 0, skippedCount: 0, + newRevisionsCount: 0, + updatedRevisionsCount: 0, + newAttachmentsCount: 0, + updatedAttachmentsCount: 0, + downloadedFilesCount: 0, message: '가져올 새로운 데이터가 없습니다.' } } @@ -114,6 +190,11 @@ class ImportService { let newCount = 0 let updatedCount = 0 let skippedCount = 0 + let newRevisionsCount = 0 + let updatedRevisionsCount = 0 + let newAttachmentsCount = 0 + let updatedAttachmentsCount = 0 + let downloadedFilesCount = 0 const errors: string[] = [] // 3. 각 문서 동기화 처리 @@ -127,12 +208,9 @@ class ImportService { if (dolceDoc.DrawingKind === 'B4') { await this.createIssueStagesForB4Document(dolceDoc.DrawingNo, contractId, dolceDoc) } - if (dolceDoc.DrawingKind === 'B3') { await this.createIssueStagesForB3Document(dolceDoc.DrawingNo, contractId, dolceDoc) } - - if (dolceDoc.DrawingKind === 'B5') { await this.createIssueStagesForB5Document(dolceDoc.DrawingNo, contractId, dolceDoc) } @@ -142,6 +220,30 @@ class ImportService { skippedCount++ } + // 4. revisions 동기화 처리 + try { + const revisionResult = await this.syncDocumentRevisions( + contractId, + dolceDoc, + sourceSystem + ) + newRevisionsCount += revisionResult.newCount + updatedRevisionsCount += revisionResult.updatedCount + + // 5. 파일 첨부 동기화 처리 (Category가 FS인 것만) + const attachmentResult = await this.syncDocumentAttachments( + dolceDoc, + sourceSystem + ) + newAttachmentsCount += attachmentResult.newCount + updatedAttachmentsCount += attachmentResult.updatedCount + downloadedFilesCount += attachmentResult.downloadedCount + + } catch (revisionError) { + console.warn(`Failed to sync revisions for ${dolceDoc.DrawingNo}:`, revisionError) + // revisions 동기화 실패는 에러 로그만 남기고 계속 진행 + } + } catch (error) { errors.push(`Document ${dolceDoc.DrawingNo}: ${error instanceof Error ? error.message : 'Unknown error'}`) skippedCount++ @@ -149,14 +251,21 @@ class ImportService { } console.log(`Import completed: ${newCount} new, ${updatedCount} updated, ${skippedCount} skipped`) + console.log(`Revisions: ${newRevisionsCount} new, ${updatedRevisionsCount} updated`) + console.log(`Attachments: ${newAttachmentsCount} new, ${updatedAttachmentsCount} updated, ${downloadedFilesCount} downloaded`) return { success: errors.length === 0, newCount, updatedCount, skippedCount, + newRevisionsCount, + updatedRevisionsCount, + newAttachmentsCount, + updatedAttachmentsCount, + downloadedFilesCount, errors: errors.length > 0 ? errors : undefined, - message: `가져오기 완료: 신규 ${newCount}건, 업데이트 ${updatedCount}건` + message: `가져오기 완료: 신규 ${newCount}건, 업데이트 ${updatedCount}건, 리비전 신규 ${newRevisionsCount}건, 리비전 업데이트 ${updatedRevisionsCount}건, 파일 다운로드 ${downloadedFilesCount}건` } } catch (error) { @@ -189,7 +298,7 @@ class ImportService { } /** - * DOLCE API에서 데이터 조회 + * DOLCE API에서 문서 목록 데이터 조회 */ private async fetchFromDOLCE( projectCode: string, @@ -214,7 +323,6 @@ class ImportService { method: 'POST', headers: { 'Content-Type': 'application/json', - // DOLCE API에 특별한 인증이 필요하다면 여기에 추가 }, body: JSON.stringify(requestBody) }) @@ -248,7 +356,7 @@ class ImportService { documents = [] } - console.log(`Found ${documents.length} documents for ${drawingKind} in ${drawingKind === 'B3' ? 'VendorDwgList' : drawingKind === 'B4' ? 'GTTDwgList' : 'FMEADwgList'}`) + console.log(`Found ${documents.length} documents for ${drawingKind}`) return documents as DOLCEDocument[] } else { @@ -261,6 +369,193 @@ class ImportService { throw error } } + + /** + * DOLCE API에서 문서 상세 정보 조회 (revisions 데이터) + */ + private async fetchDetailFromDOLCE( + projectCode: string, + drawingNo: string, + discipline: string, + drawingKind: string + ): Promise<DOLCEDetailDocument[]> { + const endpoint = process.env.DOLCE_DOC_DETAIL_API_URL || 'http://60.100.99.217:1111/Services/VDCSWebService.svc/DetailDwgReceiptMgmt' + + const requestBody = { + project: projectCode, + drawingNo: drawingNo, + discipline: discipline, + drawingKind: drawingKind + } + + console.log(`Fetching detail from DOLCE: ${projectCode} - ${drawingNo}`) + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DOLCE Detail API failed: HTTP ${response.status} - ${errorText}`) + } + + const data = await response.json() + + // DOLCE Detail API 응답 구조에 맞게 처리 + if (data.DetailDwgReceiptMgmtResult) { + const documents = data.DetailDwgReceiptMgmtResult as DOLCEDetailDocument[] + console.log(`Found ${documents.length} detail records for ${drawingNo}`) + return documents + } else { + console.warn(`Unexpected DOLCE Detail response structure:`, data) + return [] + } + + } catch (error) { + console.error(`DOLCE Detail API call failed for ${drawingNo}:`, error) + throw error + } + } + + /** + * DOLCE API에서 파일 정보 조회 + */ + private async fetchFileInfoFromDOLCE(uploadId: string): Promise<DOLCEFileInfo[]> { + const endpoint = process.env.DOLCE_FILE_INFO_API_URL || 'http://60.100.99.217:1111/Services/VDCSWebService.svc/FileInfoList' + + const requestBody = { + uploadId: uploadId + } + + console.log(`Fetching file info from DOLCE: ${uploadId}`) + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DOLCE FileInfo API failed: HTTP ${response.status} - ${errorText}`) + } + + const data = await response.json() + + // DOLCE FileInfo API 응답 구조에 맞게 처리 + if (data.FileInfoListResult) { + const files = data.FileInfoListResult as DOLCEFileInfo[] + console.log(`Found ${files.length} files for uploadId: ${uploadId}`) + return files + } else { + console.warn(`Unexpected DOLCE FileInfo response structure:`, data) + return [] + } + + } catch (error) { + console.error(`DOLCE FileInfo API call failed for ${uploadId}:`, error) + throw error + } + } + + /** + * DES 암호화 (C# DESCryptoServiceProvider 호환) + */ + private encryptDES(text: string): string { + try { + const cipher = crypto.createCipher('des-ecb', this.DES_KEY) + cipher.setAutoPadding(true) + let encrypted = cipher.update(text, 'utf8', 'base64') + encrypted += cipher.final('base64') + // + 문자를 |||로 치환 + return encrypted.replace(/\+/g, '|||') + } catch (error) { + console.error('DES encryption failed:', error) + throw error + } + } + + /** + * DOLCE에서 파일 다운로드 + */ + private async downloadFileFromDOLCE( + fileId: string, + userId: string, + fileName: string + ): Promise<Buffer> { + try { + // 암호화 문자열 생성: FileId↔UserId↔FileName + const encryptString = `${fileId}↔${userId}↔${fileName}` + const encryptedKey = this.encryptDES(encryptString) + + const downloadUrl = `${process.env.DOLCE_DOWNLOAD_URL}?key=${encryptedKey}` ||`http://60.100.99.217:1111/Download.aspx?key=${encryptedKey}` + + console.log(`Downloading file: ${fileName} with key: ${encryptedKey.substring(0, 20)}...`) + + const response = await fetch(downloadUrl, { + method: 'GET', + headers: { + 'User-Agent': 'DOLCE-Integration-Service' + } + }) + + if (!response.ok) { + throw new Error(`File download failed: HTTP ${response.status}`) + } + + const buffer = Buffer.from(await response.arrayBuffer()) + console.log(`Downloaded ${buffer.length} bytes for ${fileName}`) + + return buffer + + } catch (error) { + console.error(`Failed to download file ${fileName}:`, error) + throw error + } + } + + /** + * 로컬 파일 시스템에 파일 저장 + */ + private async saveFileToLocal( + buffer: Buffer, + originalFileName: string + ): Promise<{ fileName: string; filePath: string; fileSize: number }> { + try { + const baseDir = join(process.cwd(), "public", "documents") + + // 디렉토리가 없으면 생성 + await mkdir(baseDir, { recursive: true }) + + const ext = extname(originalFileName) + const fileName = uuidv4() + ext + const fullPath = join(baseDir, fileName) + const relativePath = "/documents/" + fileName + + await writeFile(fullPath, buffer) + + console.log(`Saved file: ${originalFileName} as ${fileName}`) + + return { + fileName: originalFileName, + filePath: relativePath, + fileSize: buffer.length + } + + } catch (error) { + console.error(`Failed to save file ${originalFileName}:`, error) + throw error + } + } + /** * 단일 문서 동기화 */ @@ -354,35 +649,384 @@ class ImportService { } } - private convertDolceDateToDate(dolceDate: string | undefined | null): Date | null { - if (!dolceDate || dolceDate.trim() === '') { - return null + /** + * 문서의 revisions 동기화 + */ + private async syncDocumentRevisions( + contractId: number, + dolceDoc: DOLCEDocument, + sourceSystem: string + ): Promise<{ newCount: number; updatedCount: number }> { + try { + // 1. 상세 정보 조회 + const detailDocs = await this.fetchDetailFromDOLCE( + dolceDoc.ProjectNo, + dolceDoc.DrawingNo, + dolceDoc.Discipline, + dolceDoc.DrawingKind + ) + + if (detailDocs.length === 0) { + console.log(`No detail data found for ${dolceDoc.DrawingNo}`) + return { newCount: 0, updatedCount: 0 } + } + + // 2. 해당 문서의 issueStages 조회 + const documentRecord = await db + .select({ id: documents.id }) + .from(documents) + .where(and( + eq(documents.contractId, contractId), + eq(documents.docNumber, dolceDoc.DrawingNo) + )) + .limit(1) + + if (documentRecord.length === 0) { + throw new Error(`Document not found: ${dolceDoc.DrawingNo}`) + } + + const documentId = documentRecord[0].id + + const issueStagesList = await db + .select() + .from(issueStages) + .where(eq(issueStages.documentId, documentId)) + + let newCount = 0 + let updatedCount = 0 + + // 3. 각 상세 데이터에 대해 revision 동기화 + for (const detailDoc of detailDocs) { + try { + // RegisterGroupId로 해당하는 issueStage 찾기 + const matchingStage = issueStagesList.find(stage => { + // RegisterGroupId와 매칭하는 로직 (추후 개선 필요) + return stage.id // 임시로 첫 번째 stage 사용 + }) + + if (!matchingStage) { + console.warn(`No matching issue stage found for RegisterGroupId: ${detailDoc.RegisterGroupId}`) + continue + } + + const result = await this.syncSingleRevision(matchingStage.id, detailDoc, sourceSystem) + if (result === 'NEW') { + newCount++ + } else if (result === 'UPDATED') { + updatedCount++ + } + + } catch (error) { + console.error(`Failed to sync revision ${detailDoc.RegisterId}:`, error) + } + } + + return { newCount, updatedCount } + + } catch (error) { + console.error(`Failed to sync revisions for ${dolceDoc.DrawingNo}:`, error) + throw error + } } - - // "20250204" 형태의 문자열을 "2025-02-04" 형태로 변환 - if (dolceDate.length === 8 && /^\d{8}$/.test(dolceDate)) { - const year = dolceDate.substring(0, 4) - const month = dolceDate.substring(4, 6) - const day = dolceDate.substring(6, 8) - + + /** + * 문서의 첨부파일 동기화 (Category가 FS인 것만) + */ + private async syncDocumentAttachments( + dolceDoc: DOLCEDocument, + sourceSystem: string + ): Promise<{ newCount: number; updatedCount: number; downloadedCount: number }> { try { - const date = new Date(`${year}-${month}-${day}`) - // 유효한 날짜인지 확인 - if (isNaN(date.getTime())) { - console.warn(`Invalid date format: ${dolceDate}`) - return null + // 1. 상세 정보 조회 + const detailDocs = await this.fetchDetailFromDOLCE( + dolceDoc.ProjectNo, + dolceDoc.DrawingNo, + dolceDoc.Discipline, + dolceDoc.DrawingKind + ) + + // 2. Category가 'FS'인 것만 필터링 + const fsDetailDocs = detailDocs.filter(doc => doc.Category === 'FS') + + if (fsDetailDocs.length === 0) { + console.log(`No FS category documents found for ${dolceDoc.DrawingNo}`) + return { newCount: 0, updatedCount: 0, downloadedCount: 0 } } - return date + + let newCount = 0 + let updatedCount = 0 + let downloadedCount = 0 + + // 3. 각 FS 문서에 대해 파일 첨부 동기화 + for (const detailDoc of fsDetailDocs) { + try { + if (!detailDoc.UploadId || detailDoc.UploadId.trim() === '') { + console.log(`No UploadId for ${detailDoc.RegisterId}`) + continue + } + + // 4. 해당 revision 조회 + const revisionRecord = await db + .select({ id: revisions.id }) + .from(revisions) + .where(eq(revisions.registerId, detailDoc.RegisterId)) + .limit(1) + + if (revisionRecord.length === 0) { + console.warn(`No revision found for RegisterId: ${detailDoc.RegisterId}`) + continue + } + + const revisionId = revisionRecord[0].id + + // 5. 파일 정보 조회 + const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) + + for (const fileInfo of fileInfos) { + if (fileInfo.UseYn !== 'Y') { + console.log(`Skipping inactive file: ${fileInfo.FileName}`) + continue + } + + const result = await this.syncSingleAttachment( + revisionId, + fileInfo, + detailDoc.CreateUserId, + sourceSystem + ) + + if (result === 'NEW') { + newCount++ + downloadedCount++ + } else if (result === 'UPDATED') { + updatedCount++ + } + } + + } catch (error) { + console.error(`Failed to sync attachments for ${detailDoc.RegisterId}:`, error) + } + } + + return { newCount, updatedCount, downloadedCount } + } catch (error) { - console.warn(`Failed to parse date: ${dolceDate}`, error) - return null + console.error(`Failed to sync attachments for ${dolceDoc.DrawingNo}:`, error) + throw error + } + } + + /** + * 단일 첨부파일 동기화 + */ + private async syncSingleAttachment( + revisionId: number, + fileInfo: DOLCEFileInfo, + userId: string, + sourceSystem: string + ): Promise<'NEW' | 'UPDATED' | 'SKIPPED'> { + try { + // 기존 첨부파일 조회 (FileId로) + const existingAttachment = await db + .select() + .from(documentAttachments) + .where(and( + eq(documentAttachments.revisionId, revisionId), + eq(documentAttachments.fileId, fileInfo.FileId) + )) + .limit(1) + + if (existingAttachment.length > 0) { + // 이미 존재하는 파일인 경우, 필요시 업데이트 로직 추가 + console.log(`File already exists: ${fileInfo.FileName}`) + return 'SKIPPED' + } + + // 파일 다운로드 + console.log(`Downloading file: ${fileInfo.FileName}`) + const fileBuffer = await this.downloadFileFromDOLCE( + fileInfo.FileId, + userId, + fileInfo.FileName + ) + + // 로컬 파일 시스템에 저장 + const savedFile = await this.saveFileToLocal(fileBuffer, fileInfo.FileName) + + // DB에 첨부파일 정보 저장 + const attachmentData = { + revisionId, + fileName: fileInfo.FileName, + filePath: savedFile.filePath, + fileType: extname(fileInfo.FileName).slice(1).toLowerCase() || undefined, + fileSize: fileInfo.FileSize, + uploadId: fileInfo.UploadId, + fileId: fileInfo.FileId, + uploadedBy: userId, + dolceFilePath: fileInfo.FileRelativePath, + uploadedAt: this.convertDolceDateToDate(fileInfo.FileCreateDT), + createdAt: new Date(), + updatedAt: new Date() + } + + await db + .insert(documentAttachments) + .values(attachmentData) + + console.log(`Created new attachment: ${fileInfo.FileName}`) + return 'NEW' + + } catch (error) { + console.error(`Failed to sync attachment ${fileInfo.FileName}:`, error) + throw error + } + } + + /** + * 단일 revision 동기화 + */ + private async syncSingleRevision( + issueStageId: number, + detailDoc: DOLCEDetailDocument, + sourceSystem: string + ): Promise<'NEW' | 'UPDATED' | 'SKIPPED'> { + // 기존 revision 조회 (registerId로) + const existingRevision = await db + .select() + .from(revisions) + .where(and( + eq(revisions.issueStageId, issueStageId), + eq(revisions.registerId, detailDoc.RegisterId) + )) + .limit(1) + + // Category에 따른 uploaderType 매핑 + const uploaderType = this.mapCategoryToUploaderType(detailDoc.Category) + + // RegisterKind에 따른 usage, usageType 매핑 (기본 로직, 추후 개선) + const { usage, usageType } = this.mapRegisterKindToUsage(detailDoc.RegisterKind) + + // DOLCE 상세 데이터를 revisions 스키마에 맞게 변환 + const revisionData = { + issueStageId, + revision: detailDoc.DrawingRevNo, + uploaderType, + uploaderName: detailDoc.CreateUserNM, + usage, + usageType, + revisionStatus: detailDoc.Status, + externalUploadId: detailDoc.UploadId, + registerId: detailDoc.RegisterId, + comment: detailDoc.RegisterDesc, + submittedDate: this.convertDolceDateToDate(detailDoc.CreateDt), + updatedAt: new Date() + } + + if (existingRevision.length > 0) { + // 업데이트 필요 여부 확인 + const existing = existingRevision[0] + const hasChanges = + existing.revision !== revisionData.revision || + existing.revisionStatus !== revisionData.revisionStatus || + existing.uploaderName !== revisionData.uploaderName + + if (hasChanges) { + await db + .update(revisions) + .set(revisionData) + .where(eq(revisions.id, existing.id)) + + console.log(`Updated revision: ${detailDoc.RegisterId}`) + return 'UPDATED' + } else { + return 'SKIPPED' + } + } else { + // 새 revision 생성 + await db + .insert(revisions) + .values({ + ...revisionData, + createdAt: new Date() + }) + + console.log(`Created new revision: ${detailDoc.RegisterId}`) + return 'NEW' + } + } + + /** + * Category를 uploaderType으로 매핑 + */ + private mapCategoryToUploaderType(category: string): string { + switch (category) { + case 'TS': + return 'vendor' + case 'FS': + return 'shi' + default: + return 'vendor' // 기본값 } } - - console.warn(`Unexpected date format: ${dolceDate}`) - return null -} + /** + * RegisterKind를 usage/usageType으로 매핑 + */ + private mapRegisterKindToUsage(registerKind: string): { usage: string; usageType: string } { + // TODO: 추후 비즈니스 로직에 맞게 구현 + // 현재는 기본 매핑만 제공 + return { + usage: registerKind || 'DEFAULT', + usageType: registerKind || 'DEFAULT' + } + } + + /** + * Status를 revisionStatus로 매핑 + */ + private mapStatusToRevisionStatus(status: string): string { + // TODO: DOLCE의 Status 값에 맞게 매핑 로직 구현 + // 현재는 기본 매핑만 제공 + switch (status?.toUpperCase()) { + case 'SUBMITTED': + return 'SUBMITTED' + case 'APPROVED': + return 'APPROVED' + case 'REJECTED': + return 'REJECTED' + default: + return 'SUBMITTED' // 기본값 + } + } + + private convertDolceDateToDate(dolceDate: string | undefined | null): Date | null { + if (!dolceDate || dolceDate.trim() === '') { + return null + } + + // "20250204" 형태의 문자열을 "2025-02-04" 형태로 변환 + if (dolceDate.length === 8 && /^\d{8}$/.test(dolceDate)) { + const year = dolceDate.substring(0, 4) + const month = dolceDate.substring(4, 6) + const day = dolceDate.substring(6, 8) + + try { + const date = new Date(`${year}-${month}-${day}`) + // 유효한 날짜인지 확인 + if (isNaN(date.getTime())) { + console.warn(`Invalid date format: ${dolceDate}`) + return null + } + return date + } catch (error) { + console.warn(`Failed to parse date: ${dolceDate}`, error) + return null + } + } + + console.warn(`Unexpected date format: ${dolceDate}`) + return null + } /** * B4 문서용 이슈 스테이지 자동 생성 @@ -426,8 +1070,8 @@ class ImportService { actualDate: this.convertDolceDateToDate(dolceDoc.GTTPreDwg_ResultDate), stageStatus: 'PLANNED', stageOrder: 1, - priority: 'MEDIUM', // 기본값 - reminderDays: 3, // 기본값 + priority: 'MEDIUM', + reminderDays: 3, description: 'GTT 예비 도면 단계' }) } @@ -449,7 +1093,6 @@ class ImportService { } catch (error) { console.error(`Failed to create issue stages for ${drawingNo}:`, error) - // 이슈 스테이지 생성 실패는 전체 문서 생성을 막지 않음 } } @@ -483,41 +1126,36 @@ class ImportService { const existingStageNames = existingStages.map(stage => stage.stageName) - // For Pre 스테이지 생성 (GTTPreDwg) + // Approval 스테이지 생성 if (!existingStageNames.includes('Approval')) { await db.insert(issueStages).values({ documentId: documentId, stageName: 'Vendor → SHI (For Approval)', - planDate: this.convertDolceDateToDate(dolceDoc.AppDwg_PlanDate), actualDate: this.convertDolceDateToDate(dolceDoc.AppDwg_ResultDate), - stageStatus: 'PLANNED', stageOrder: 1, description: 'Vendor 승인 도면 단계' }) } - // For Working 스테이지 생성 (GTTWorkingDwg) + // Working 스테이지 생성 if (!existingStageNames.includes('Working')) { await db.insert(issueStages).values({ documentId: documentId, stageName: 'Vendor → SHI (For Working)', - planDate: this.convertDolceDateToDate(dolceDoc.WorDwg_PlanDate), actualDate: this.convertDolceDateToDate(dolceDoc.WorDwg_ResultDate), - stageStatus: 'PLANNED', stageOrder: 2, description: 'Vendor 작업 도면 단계' }) } - console.log(`Created issue stages for B4 document: ${drawingNo}`) + console.log(`Created issue stages for B3 document: ${drawingNo}`) } catch (error) { console.error(`Failed to create issue stages for ${drawingNo}:`, error) - // 이슈 스테이지 생성 실패는 전체 문서 생성을 막지 않음 } } @@ -551,43 +1189,39 @@ class ImportService { const existingStageNames = existingStages.map(stage => stage.stageName) - // For Pre 스테이지 생성 (GTTPreDwg) + // Approval 스테이지 생성 if (!existingStageNames.includes('Approval')) { await db.insert(issueStages).values({ documentId: documentId, stageName: 'Vendor → SHI (For Approval)', - planDate: this.convertDolceDateToDate(dolceDoc.FMEAFirst_PlanDate), actualDate: this.convertDolceDateToDate(dolceDoc.FMEAFirst_ResultDate), - stageStatus: 'PLANNED', stageOrder: 1, description: 'FMEA 예비 도면 단계' }) } - // For Working 스테이지 생성 (GTTWorkingDwg) + // Working 스테이지 생성 if (!existingStageNames.includes('Working')) { await db.insert(issueStages).values({ documentId: documentId, stageName: 'Vendor → SHI (For Working)', - planDate: dolceDoc.FMEASecond_PlanDate ? dolceDoc.FMEASecond_PlanDate : null, - actualDate: dolceDoc.FMEASecond_ResultDate ? dolceDoc.FMEASecond_ResultDate : null, + planDate: this.convertDolceDateToDate(dolceDoc.FMEASecond_PlanDate), + actualDate: this.convertDolceDateToDate(dolceDoc.FMEASecond_ResultDate), stageStatus: 'PLANNED', stageOrder: 2, description: 'FMEA 작업 도면 단계' }) } - console.log(`Created issue stages for B4 document: ${drawingNo}`) + console.log(`Created issue stages for B5 document: ${drawingNo}`) } catch (error) { console.error(`Failed to create issue stages for ${drawingNo}:`, error) - // 이슈 스테이지 생성 실패는 전체 문서 생성을 막지 않음 } } - /** * 가져오기 상태 조회 */ @@ -607,14 +1241,9 @@ class ImportService { eq(documents.externalSystemType, sourceSystem) )) - console.log(contractId, "contractId") - - // 프로젝트 코드와 벤더 코드 조회 const contractInfo = await this.getContractInfoById(contractId) - console.log(contractInfo, "contractInfo") - if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { throw new Error(`Project code or vendor code not found for contract ${contractId}`) } @@ -622,6 +1251,12 @@ class ImportService { let availableDocuments = 0 let newDocuments = 0 let updatedDocuments = 0 + let availableRevisions = 0 + let newRevisions = 0 + let updatedRevisions = 0 + let availableAttachments = 0 + let newAttachments = 0 + let updatedAttachments = 0 try { // 각 drawingKind별로 확인 @@ -659,6 +1294,59 @@ class ImportService { } } } + + // revisions 및 attachments 상태도 확인 + try { + const detailDocs = await this.fetchDetailFromDOLCE( + externalDoc.ProjectNo, + externalDoc.DrawingNo, + externalDoc.Discipline, + externalDoc.DrawingKind + ) + availableRevisions += detailDocs.length + + for (const detailDoc of detailDocs) { + const existingRevision = await db + .select({ id: revisions.id }) + .from(revisions) + .where(eq(revisions.registerId, detailDoc.RegisterId)) + .limit(1) + + if (existingRevision.length === 0) { + newRevisions++ + } else { + updatedRevisions++ + } + + // FS Category 문서의 첨부파일 확인 + if (detailDoc.Category === 'FS' && detailDoc.UploadId) { + try { + const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) + availableAttachments += fileInfos.filter(f => f.UseYn === 'Y').length + + for (const fileInfo of fileInfos) { + if (fileInfo.UseYn !== 'Y') continue + + const existingAttachment = await db + .select({ id: documentAttachments.id }) + .from(documentAttachments) + .where(eq(documentAttachments.fileId, fileInfo.FileId)) + .limit(1) + + if (existingAttachment.length === 0) { + newAttachments++ + } else { + updatedAttachments++ + } + } + } catch (error) { + console.warn(`Failed to check files for ${detailDoc.UploadId}:`, error) + } + } + } + } catch (error) { + console.warn(`Failed to check revisions for ${externalDoc.DrawingNo}:`, error) + } } } catch (error) { console.warn(`Failed to check ${drawingKind} for status:`, error) @@ -673,6 +1361,12 @@ class ImportService { availableDocuments, newDocuments, updatedDocuments, + availableRevisions, + newRevisions, + updatedRevisions, + availableAttachments, + newAttachments, + updatedAttachments, importEnabled: this.isImportEnabled(sourceSystem) } diff --git a/lib/vendor-document-list/service.ts b/lib/vendor-document-list/service.ts index f3b2b633..de6f0488 100644 --- a/lib/vendor-document-list/service.ts +++ b/lib/vendor-document-list/service.ts @@ -3,7 +3,7 @@ import { eq, SQL } from "drizzle-orm" import db from "@/db/db" import { documents, documentStagesView, issueStages } from "@/db/schema/vendorDocu" -import { contracts } from "@/db/schema/vendorData" +import { contracts } from "@/db/schema" import { GetVendorDcoumentsSchema } from "./validations" import { unstable_cache } from "@/lib/unstable-cache"; import { filterColumns } from "@/lib/filter-columns"; @@ -293,3 +293,19 @@ export async function modifyDocument(input: ModifyDocumentInputType) { }; } } + + +export async function getContractIdsByVendor(vendorId: number): Promise<number[]> { + try { + const contractsData = await db + .select({ id: contracts.id }) + .from(contracts) + .where(eq(contracts.vendorId, vendorId)) + .orderBy(contracts.id) + + return contractsData.map(contract => contract.id) + } catch (error) { + console.error('Error fetching contract IDs by vendor:', error) + return [] + } +}
\ No newline at end of file diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx index ad184378..9c13573c 100644 --- a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx +++ b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx @@ -108,6 +108,7 @@ export function getSimplifiedDocumentColumns({ ) }, enableResizing: true, + maxSize:300, meta: { excelHeader: "문서명" }, @@ -127,6 +128,8 @@ export function getSimplifiedDocumentColumns({ ) }, enableResizing: true, + maxSize:100, + meta: { excelHeader: "프로젝트" }, diff --git a/lib/vendor-document-list/ship/import-from-dolce-button.tsx b/lib/vendor-document-list/ship/import-from-dolce-button.tsx index 23d80981..d4728d22 100644 --- a/lib/vendor-document-list/ship/import-from-dolce-button.tsx +++ b/lib/vendor-document-list/ship/import-from-dolce-button.tsx @@ -22,6 +22,8 @@ import { Progress } from "@/components/ui/progress" import { Separator } from "@/components/ui/separator" import { SimplifiedDocumentsView } from "@/db/schema" import { ImportStatus } from "../import-service" +import { useSession } from "next-auth/react" +import { getContractIdsByVendor } from "../service" // 서버 액션 import interface ImportFromDOLCEButtonProps { allDocuments: SimplifiedDocumentsView[] // contractId 대신 문서 배열 @@ -37,31 +39,71 @@ export function ImportFromDOLCEButton({ const [isImporting, setIsImporting] = React.useState(false) const [importStatusMap, setImportStatusMap] = React.useState<Map<number, ImportStatus>>(new Map()) const [statusLoading, setStatusLoading] = React.useState(false) + const [vendorContractIds, setVendorContractIds] = React.useState<number[]>([]) // 서버에서 가져온 contractIds + const [loadingVendorContracts, setLoadingVendorContracts] = React.useState(false) + const { data: session } = useSession() - // 문서들에서 contractId들 추출 - const contractIds = React.useMemo(() => { + const vendorId = session?.user.companyId; + + // allDocuments에서 추출한 contractIds + const documentsContractIds = React.useMemo(() => { const uniqueIds = [...new Set(allDocuments.map(doc => doc.contractId).filter(Boolean))] return uniqueIds.sort() }, [allDocuments]) + // 최종 사용할 contractIds (allDocuments가 있으면 문서에서, 없으면 vendor의 모든 contracts) + const contractIds = React.useMemo(() => { + if (documentsContractIds.length > 0) { + return documentsContractIds + } + return vendorContractIds + }, [documentsContractIds, vendorContractIds]) + console.log(contractIds, "contractIds") + // vendorId로 contracts 가져오기 + React.useEffect(() => { + const fetchVendorContracts = async () => { + // allDocuments가 비어있고 vendorId가 있을 때만 실행 + if (allDocuments.length === 0 && vendorId) { + setLoadingVendorContracts(true) + try { + const contractIds = await getContractIdsByVendor(vendorId) + setVendorContractIds(contractIds) + } catch (error) { + console.error('Failed to fetch vendor contracts:', error) + toast.error('계약 정보를 가져오는데 실패했습니다.') + } finally { + setLoadingVendorContracts(false) + } + } + } + + fetchVendorContracts() + }, [allDocuments.length, vendorId]) + // 주요 contractId (가장 많이 나타나는 것) const primaryContractId = React.useMemo(() => { if (contractIds.length === 1) return contractIds[0] - const counts = allDocuments.reduce((acc, doc) => { - const id = doc.contractId || 0 - acc[id] = (acc[id] || 0) + 1 - return acc - }, {} as Record<number, number>) + if (allDocuments.length > 0) { + const counts = allDocuments.reduce((acc, doc) => { + const id = doc.contractId || 0 + acc[id] = (acc[id] || 0) + 1 + return acc + }, {} as Record<number, number>) + + return Number(Object.entries(counts) + .sort(([,a], [,b]) => b - a)[0]?.[0] || contractIds[0] || 0) + } - return Number(Object.entries(counts) - .sort(([,a], [,b]) => b - a)[0]?.[0] || contractIds[0] || 0) + return contractIds[0] || 0 }, [contractIds, allDocuments]) // 모든 contractId에 대한 상태 조회 const fetchAllImportStatus = async () => { + if (contractIds.length === 0) return + setStatusLoading(true) const statusMap = new Map<number, ImportStatus>() @@ -217,6 +259,10 @@ export function ImportFromDOLCEButton({ } const getStatusBadge = () => { + if (loadingVendorContracts) { + return <Badge variant="secondary">계약 정보 로딩 중...</Badge> + } + if (statusLoading) { return <Badge variant="secondary">DOLCE 연결 확인 중...</Badge> } @@ -231,7 +277,7 @@ export function ImportFromDOLCEButton({ if (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) { return ( - <Badge variant="default" className="gap-1 bg-blue-500 hover:bg-blue-600"> + <Badge variant="samsung" className="gap-1"> <AlertTriangle className="w-3 h-3" /> 업데이트 가능 ({contractIds.length}개 계약) </Badge> @@ -249,8 +295,9 @@ export function ImportFromDOLCEButton({ const canImport = totalStats.importEnabled && (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) - if (contractIds.length === 0) { - return null // 계약이 없으면 버튼을 표시하지 않음 + // 로딩 중이거나 contractIds가 없으면 버튼을 표시하지 않음 + if (loadingVendorContracts || contractIds.length === 0) { + return null } return ( @@ -272,8 +319,8 @@ export function ImportFromDOLCEButton({ <span className="hidden sm:inline">DOLCE에서 가져오기</span> {totalStats.newDocuments + totalStats.updatedDocuments > 0 && ( <Badge - variant="default" - className="h-5 w-5 p-0 text-xs flex items-center justify-center bg-blue-500" + variant="samsung" + className="h-5 w-5 p-0 text-xs flex items-center justify-center" > {totalStats.newDocuments + totalStats.updatedDocuments} </Badge> @@ -292,6 +339,13 @@ export function ImportFromDOLCEButton({ </div> </div> + {/* 계약 소스 표시 */} + {allDocuments.length === 0 && vendorContractIds.length > 0 && ( + <div className="text-xs text-blue-600 bg-blue-50 p-2 rounded"> + 문서가 없어서 전체 계약에서 가져오기를 진행합니다. + </div> + )} + {/* 다중 계약 정보 표시 */} {contractIds.length > 1 && ( <div className="text-sm"> |
