summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/import-service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/import-service.ts')
-rw-r--r--lib/vendor-document-list/import-service.ts816
1 files changed, 755 insertions, 61 deletions
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)
}