summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list')
-rw-r--r--lib/vendor-document-list/import-service.ts809
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,