diff options
Diffstat (limited to 'lib/vendor-document-list/sync-service.ts')
| -rw-r--r-- | lib/vendor-document-list/sync-service.ts | 847 |
1 files changed, 641 insertions, 206 deletions
diff --git a/lib/vendor-document-list/sync-service.ts b/lib/vendor-document-list/sync-service.ts index 6978c1cc..1f2872c4 100644 --- a/lib/vendor-document-list/sync-service.ts +++ b/lib/vendor-document-list/sync-service.ts @@ -1,17 +1,13 @@ -// lib/sync-service.ts +// lib/sync-service.ts (시스템별 분리 버전) import db from "@/db/db" -import { - syncConfigs, - changeLogs, - syncBatches, - syncStatusView, - type SyncConfig, +import { + changeLogs, + syncBatches, type ChangeLog, - type SyncBatch + type SyncBatch } from "@/db/schema/vendorDocu" import { documents, revisions, documentAttachments } from "@/db/schema/vendorDocu" import { eq, and, lt, desc, sql, inArray } from "drizzle-orm" -import { toast } from "sonner" export interface SyncableEntity { entityType: 'document' | 'revision' | 'attachment' @@ -27,10 +23,23 @@ export interface SyncResult { successCount: number failureCount: number errors?: string[] + endpointResults?: Record<string, any> } class SyncService { - + private readonly CHUNK_SIZE = 50 + + + + /** + * 동기화 활성화 여부 확인 + */ + private isSyncEnabled(targetSystem: string): boolean { + const upperSystem = targetSystem.toUpperCase() + const enabled = process.env[`SYNC_${upperSystem}_ENABLED`] + return enabled === 'true' || enabled === '1' + } + /** * 변경사항을 change_logs에 기록 */ @@ -42,11 +51,12 @@ class SyncService { newValues?: any, oldValues?: any, userId?: number, - userName?: string + userName?: string, + targetSystems: string[] = ["DOLCE", "SWP"] ) { try { const changedFields = this.detectChangedFields(oldValues, newValues) - + await db.insert(changeLogs).values({ contractId, entityType, @@ -57,9 +67,9 @@ class SyncService { newValues, userId, userName, - targetSystems: ['SHI'], // 기본적으로 SHI로 동기화 + targetSystems, }) - + console.log(`Change logged: ${entityType}/${entityId} - ${action}`) } catch (error) { console.error('Failed to log change:', error) @@ -72,9 +82,9 @@ class SyncService { */ private detectChangedFields(oldValues: any, newValues: any): Record<string, any> | null { if (!oldValues || !newValues) return null - + const changes: Record<string, any> = {} - + for (const [key, newValue] of Object.entries(newValues)) { if (JSON.stringify(oldValues[key]) !== JSON.stringify(newValue)) { changes[key] = { @@ -83,65 +93,45 @@ class SyncService { } } } - - return Object.keys(changes).length > 0 ? changes : null - } - /** - * 계약별 동기화 설정 조회 - */ - async getSyncConfig(contractId: number, targetSystem: string = 'SHI'): Promise<SyncConfig | null> { - const [config] = await db - .select() - .from(syncConfigs) - .where(and( - eq(syncConfigs.contractId, contractId), - eq(syncConfigs.targetSystem, targetSystem) - )) - .limit(1) - - return config || null - } - - /** - * 동기화 설정 생성/업데이트 - */ - async upsertSyncConfig(config: Partial<SyncConfig> & { - contractId: number - targetSystem: string - endpointUrl: string - }) { - const existing = await this.getSyncConfig(config.contractId, config.targetSystem) - - if (existing) { - await db - .update(syncConfigs) - .set({ ...config, updatedAt: new Date() }) - .where(eq(syncConfigs.id, existing.id)) - } else { - await db.insert(syncConfigs).values(config) - } + return Object.keys(changes).length > 0 ? changes : null } /** * 동기화할 변경사항 조회 (증분) */ async getPendingChanges( - contractId: number, - targetSystem: string = 'SHI', - limit: number = 100 + contractId: number, + targetSystem: string = 'DOLCE', + limit?: number ): Promise<ChangeLog[]> { - return await db + const query = db .select() .from(changeLogs) .where(and( eq(changeLogs.contractId, contractId), eq(changeLogs.isSynced, false), - lt(changeLogs.syncAttempts, 3), // 최대 3회 재시도 + lt(changeLogs.syncAttempts, 3), sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` )) .orderBy(changeLogs.createdAt) - .limit(limit) + + if (limit) { + query.limit(limit) + } + + return await query + } + + /** + * 배열을 청크 단위로 분할 + */ + private chunkArray<T>(array: T[], chunkSize: number): T[][] { + const chunks: T[][] = [] + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)) + } + return chunks } /** @@ -162,31 +152,26 @@ class SyncService { status: 'PENDING' }) .returning({ id: syncBatches.id }) - + return batch.id } /** - * 메인 동기화 실행 함수 + * 메인 동기화 실행 함수 (청크 처리 포함) */ async syncToExternalSystem( contractId: number, - targetSystem: string = 'SHI', + targetSystem: string = 'DOLCE', manualTrigger: boolean = false ): Promise<SyncResult> { try { - // 1. 동기화 설정 확인 - const config = await this.getSyncConfig(contractId, targetSystem) - if (!config || !config.syncEnabled) { - throw new Error(`Sync not enabled for contract ${contractId} to ${targetSystem}`) + // 1. 동기화 활성화 확인 + if (!this.isSyncEnabled(targetSystem)) { + throw new Error(`Sync not enabled for ${targetSystem}`) } - // 2. 대기 중인 변경사항 조회 - const pendingChanges = await this.getPendingChanges( - contractId, - targetSystem, - config.maxBatchSize || 100 - ) + // 2. 대기 중인 변경사항 조회 (전체) + const pendingChanges = await this.getPendingChanges(contractId, targetSystem) if (pendingChanges.length === 0) { return { @@ -207,75 +192,99 @@ class SyncService { // 4. 배치 상태를 PROCESSING으로 업데이트 await db .update(syncBatches) - .set({ - status: 'PROCESSING', + .set({ + status: 'PROCESSING', startedAt: new Date(), updatedAt: new Date() }) .where(eq(syncBatches.id, batchId)) - // 5. 실제 데이터 동기화 수행 - const syncResult = await this.performSync(config, pendingChanges) - - // 6. 배치 상태 업데이트 - await db - .update(syncBatches) - .set({ - status: syncResult.success ? 'SUCCESS' : (syncResult.successCount > 0 ? 'PARTIAL' : 'FAILED'), - completedAt: new Date(), - successCount: syncResult.successCount, - failureCount: syncResult.failureCount, - errorMessage: syncResult.errors?.join('; '), - updatedAt: new Date() - }) - .where(eq(syncBatches.id, batchId)) + // 5. 청크 단위로 동기화 수행 - 시스템별 분기 + const chunks = this.chunkArray(pendingChanges, this.CHUNK_SIZE) + let totalSuccessCount = 0 + let totalFailureCount = 0 + const allErrors: string[] = [] + const endpointResults: Record<string, any> = {} + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i] + console.log(`Processing chunk ${i + 1}/${chunks.length} (${chunk.length} items) for ${targetSystem}`) + + try { + let chunkResult; + + // 시스템별로 다른 동기화 메서드 호출 + switch (targetSystem.toUpperCase()) { + case 'DOLCE': + chunkResult = await this.performSyncDOLCE(chunk, contractId) + break + case 'SWP': + chunkResult = await this.performSyncSWP(chunk, contractId) + break + default: + throw new Error(`Unsupported target system: ${targetSystem}`) + } + + totalSuccessCount += chunkResult.successCount + totalFailureCount += chunkResult.failureCount + + if (chunkResult.errors) { + allErrors.push(...chunkResult.errors) + } - // 7. 성공한 변경사항들을 동기화 완료로 표시 - if (syncResult.successCount > 0) { - const successfulChangeIds = pendingChanges - .slice(0, syncResult.successCount) - .map(c => c.id) - - await db - .update(changeLogs) - .set({ - isSynced: true, - syncedAt: new Date() - }) - .where(inArray(changeLogs.id, successfulChangeIds)) - } + // 엔드포인트별 결과 병합 + Object.assign(endpointResults, chunkResult.endpointResults || {}) - // 8. 실패한 변경사항들의 재시도 횟수 증가 - if (syncResult.failureCount > 0) { - const failedChangeIds = pendingChanges - .slice(syncResult.successCount) - .map(c => c.id) - - await db - .update(changeLogs) - .set({ - syncAttempts: sql`${changeLogs.syncAttempts} + 1`, - lastSyncError: syncResult.errors?.[0] || 'Unknown error' - }) - .where(inArray(changeLogs.id, failedChangeIds)) + // 성공한 변경사항들을 동기화 완료로 표시 + if (chunkResult.successCount > 0) { + const successfulChangeIds = chunk + .slice(0, chunkResult.successCount) + .map(c => c.id) + + await this.markChangesAsSynced(successfulChangeIds) + } + + // 실패한 변경사항들의 재시도 횟수 증가 + if (chunkResult.failureCount > 0) { + const failedChangeIds = chunk + .slice(chunkResult.successCount) + .map(c => c.id) + + await this.incrementSyncAttempts(failedChangeIds, chunkResult.errors?.[0]) + } + + } catch (error) { + console.error(`Chunk ${i + 1} failed for ${targetSystem}:`, error) + totalFailureCount += chunk.length + allErrors.push(`Chunk ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`) + + // 전체 청크 실패 시 재시도 횟수 증가 + await this.incrementSyncAttempts(chunk.map(c => c.id), error instanceof Error ? error.message : 'Unknown error') + } } - // 9. 동기화 설정의 마지막 동기화 시간 업데이트 + const overallSuccess = totalFailureCount === 0 + + // 6. 배치 상태 업데이트 await db - .update(syncConfigs) + .update(syncBatches) .set({ - lastSyncAttempt: new Date(), - ...(syncResult.success && { lastSuccessfulSync: new Date() }), + status: overallSuccess ? 'SUCCESS' : (totalSuccessCount > 0 ? 'PARTIAL' : 'FAILED'), + completedAt: new Date(), + successCount: totalSuccessCount, + failureCount: totalFailureCount, + errorMessage: allErrors.length > 0 ? allErrors.join('; ') : null, updatedAt: new Date() }) - .where(eq(syncConfigs.id, config.id)) + .where(eq(syncBatches.id, batchId)) return { batchId, - success: syncResult.success, - successCount: syncResult.successCount, - failureCount: syncResult.failureCount, - errors: syncResult.errors + success: overallSuccess, + successCount: totalSuccessCount, + failureCount: totalFailureCount, + errors: allErrors.length > 0 ? allErrors : undefined, + endpointResults } } catch (error) { @@ -285,77 +294,275 @@ class SyncService { } /** - * 실제 외부 시스템으로 데이터 전송 + * DOLCE 시스템 전용 동기화 수행 */ - private async performSync( - config: SyncConfig, - changes: ChangeLog[] - ): Promise<{ success: boolean; successCount: number; failureCount: number; errors?: string[] }> { + private async performSyncDOLCE( + changes: ChangeLog[], + contractId: number + ): Promise<{ success: boolean; successCount: number; failureCount: number; errors?: string[]; endpointResults?: Record<string, any> }> { const errors: string[] = [] - let successCount = 0 - let failureCount = 0 + const endpointResults: Record<string, any> = {} + let overallSuccess = true + + // 변경사항을 DOLCE 시스템 형태로 변환 + const syncData = await this.transformChangesForDOLCE(changes) + + // DOLCE 엔드포인트 호출들을 직접 정의 + const endpointPromises = [] + + // 1. DOLCE 메인 엔드포인트 + const mainUrl = process.env.SYNC_DOLCE_URL + if (mainUrl) { + endpointPromises.push( + (async () => { + try { + console.log(`Sending to DOLCE main: ${mainUrl}`) + + const transformedData = { + contractId, + systemType: 'DOLCE', + changes: syncData, + batchSize: changes.length, + timestamp: new Date().toISOString(), + source: 'EVCP', + version: '1.0' + } - try { - // 변경사항을 외부 시스템 형태로 변환 - const syncData = await this.transformChangesForExternalSystem(changes) - - // 외부 API 호출 - const response = await fetch(config.endpointUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.authToken}`, - 'X-API-Version': config.apiVersion || 'v1' - }, - body: JSON.stringify({ - contractId: changes[0]?.contractId, + // 헤더 구성 (토큰이 있을 때만 Authorization 포함) + const headers: Record<string, string> = { + 'Content-Type': 'application/json', + 'X-API-Version': process.env.SYNC_DOLCE_VERSION || 'v1', + 'X-System': 'DOLCE' + } + + if (process.env.SYNC_DOLCE_TOKEN) { + headers['Authorization'] = `Bearer ${process.env.SYNC_DOLCE_TOKEN}` + } + + const response = await fetch(mainUrl, { + method: 'POST', + headers, + body: JSON.stringify(transformedData) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DOLCE main: HTTP ${response.status} - ${errorText}`) + } + + const result = await response.json() + endpointResults['dolce_main'] = result + + console.log(`✅ DOLCE main sync successful`) + return { success: true, endpoint: 'dolce_main', result } + + } catch (error) { + const errorMessage = `DOLCE main: ${error instanceof Error ? error.message : 'Unknown error'}` + errors.push(errorMessage) + overallSuccess = false + + console.error(`❌ DOLCE main sync failed:`, error) + return { success: false, endpoint: 'dolce_main', error: errorMessage } + } + })() + ) + } + + // 2. DOLCE 문서 전용 엔드포인트 (선택사항) + const docUrl = process.env.SYNC_DOLCE_DOCUMENT_URL + if (docUrl) { + endpointPromises.push( + (async () => { + try { + console.log(`Sending to DOLCE documents: ${docUrl}`) + + const documentData = { + documents: syncData.filter(item => item.entityType === 'document'), + source: 'EVCP_DOLCE', + timestamp: new Date().toISOString() + } + + // 헤더 구성 (토큰이 있을 때만 Authorization 포함) + const headers: Record<string, string> = { + 'Content-Type': 'application/json' + } + + if (process.env.SYNC_DOLCE_TOKEN) { + headers['Authorization'] = `Bearer ${process.env.SYNC_DOLCE_TOKEN}` + } + + const response = await fetch(docUrl, { + method: 'PUT', + headers, + body: JSON.stringify(documentData) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DOLCE documents: HTTP ${response.status} - ${errorText}`) + } + + const result = await response.json() + endpointResults['dolce_documents'] = result + + console.log(`✅ DOLCE documents sync successful`) + return { success: true, endpoint: 'dolce_documents', result } + + } catch (error) { + const errorMessage = `DOLCE documents: ${error instanceof Error ? error.message : 'Unknown error'}` + errors.push(errorMessage) + overallSuccess = false + + console.error(`❌ DOLCE documents sync failed:`, error) + return { success: false, endpoint: 'dolce_documents', error: errorMessage } + } + })() + ) + } + + if (endpointPromises.length === 0) { + throw new Error('No DOLCE sync endpoints configured') + } + + // 모든 엔드포인트 요청 완료 대기 + const results = await Promise.allSettled(endpointPromises) + + // 결과 집계 + const successfulEndpoints = results.filter(r => r.status === 'fulfilled' && r.value.success).length + const totalEndpoints = endpointPromises.length + + console.log(`DOLCE endpoint results: ${successfulEndpoints}/${totalEndpoints} successful`) + + return { + success: overallSuccess && errors.length === 0, + successCount: overallSuccess ? changes.length : 0, + failureCount: overallSuccess ? 0 : changes.length, + errors: errors.length > 0 ? errors : undefined, + endpointResults + } + } + + /** + * SWP 시스템 전용 동기화 수행 + */ + private async performSyncSWP( + changes: ChangeLog[], + contractId: number + ): Promise<{ success: boolean; successCount: number; failureCount: number; errors?: string[]; endpointResults?: Record<string, any> }> { + const errors: string[] = [] + const endpointResults: Record<string, any> = {} + let overallSuccess = true + + // 변경사항을 SWP 시스템 형태로 변환 + const syncData = await this.transformChangesForSWP(changes) + + // 1. SWP 메인 엔드포인트 (XML 전송) + const mainUrl = process.env.SYNC_SWP_URL + if (mainUrl) { + try { + console.log(`Sending to SWP main: ${mainUrl}`) + + const transformedData = this.convertToXML({ + contractId, + systemType: 'SWP', changes: syncData, batchSize: changes.length, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + source: 'EVCP' }) - }) - if (!response.ok) { - const errorText = await response.text() - throw new Error(`HTTP ${response.status}: ${errorText}`) + const response = await fetch(mainUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + 'Authorization': `Basic ${Buffer.from(`${process.env.SYNC_SWP_USER}:${process.env.SYNC_SWP_PASSWORD}`).toString('base64')}`, + 'X-System': 'SWP' + }, + body: transformedData + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`SWP main: HTTP ${response.status} - ${errorText}`) + } + + let result + const contentType = response.headers.get('content-type') + if (contentType?.includes('application/json')) { + result = await response.json() + } else { + result = await response.text() + } + + endpointResults['swp_main'] = result + console.log(`✅ SWP main sync successful`) + + } catch (error) { + const errorMessage = `SWP main: ${error instanceof Error ? error.message : 'Unknown error'}` + errors.push(errorMessage) + overallSuccess = false + + console.error(`❌ SWP main sync failed:`, error) } + } - const result = await response.json() - - // 응답에 따라 성공/실패 카운트 처리 - if (result.success) { - successCount = changes.length - } else if (result.partialSuccess) { - successCount = result.successCount || 0 - failureCount = changes.length - successCount - if (result.errors) { - errors.push(...result.errors) + // 2. SWP 알림 엔드포인트 (선택사항) + const notificationUrl = process.env.SYNC_SWP_NOTIFICATION_URL + if (notificationUrl) { + try { + console.log(`Sending to SWP notification: ${notificationUrl}`) + + const notificationData = { + event: 'swp_sync_notification', + itemCount: syncData.length, + syncTime: new Date().toISOString(), + system: 'SWP' } - } else { - failureCount = changes.length - if (result.error) { - errors.push(result.error) + + const response = await fetch(notificationUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(notificationData) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`SWP notification: HTTP ${response.status} - ${errorText}`) } + + const result = await response.json() + endpointResults['swp_notification'] = result + console.log(`✅ SWP notification sync successful`) + + } catch (error) { + const errorMessage = `SWP notification: ${error instanceof Error ? error.message : 'Unknown error'}` + errors.push(errorMessage) + // 알림은 실패해도 전체 동기화는 성공으로 처리 + console.error(`❌ SWP notification sync failed:`, error) } + } - } catch (error) { - console.error('External sync failed:', error) - failureCount = changes.length - errors.push(error instanceof Error ? error.message : 'Unknown error') + if (!mainUrl) { + throw new Error('No SWP main endpoint configured') } + console.log(`SWP sync completed with ${errors.length} errors`) + return { - success: failureCount === 0, - successCount, - failureCount, - errors: errors.length > 0 ? errors : undefined + success: overallSuccess && errors.length === 0, + successCount: overallSuccess ? changes.length : 0, + failureCount: overallSuccess ? 0 : changes.length, + errors: errors.length > 0 ? errors : undefined, + endpointResults } } /** - * 변경사항을 외부 시스템 형태로 변환 + * DOLCE 시스템용 데이터 변환 */ - private async transformChangesForExternalSystem(changes: ChangeLog[]): Promise<SyncableEntity[]> { + private async transformChangesForDOLCE(changes: ChangeLog[]): Promise<SyncableEntity[]> { const syncData: SyncableEntity[] = [] for (const change of changes) { @@ -374,7 +581,7 @@ class SyncService { entityData = document } break - + case 'revision': if (change.action !== 'DELETE') { const [revision] = await db @@ -385,7 +592,7 @@ class SyncService { entityData = revision } break - + case 'attachment': if (change.action !== 'DELETE') { const [attachment] = await db @@ -398,21 +605,26 @@ class SyncService { break } + // DOLCE 특화 데이터 구조 syncData.push({ entityType: change.entityType as any, entityId: change.entityId, action: change.action as any, - data: entityData || change.oldValues, // DELETE의 경우 oldValues 사용 + data: entityData || change.oldValues, metadata: { changeId: change.id, changedAt: change.createdAt, changedBy: change.userName, - changedFields: change.changedFields + changedFields: change.changedFields, + // DOLCE 전용 메타데이터 + dolceVersion: '2.0', + processingPriority: change.entityType === 'revision' ? 'HIGH' : 'NORMAL', + requiresApproval: change.action === 'DELETE' } }) } catch (error) { - console.error(`Failed to transform change ${change.id}:`, error) + console.error(`Failed to transform change ${change.id} for DOLCE:`, error) } } @@ -420,40 +632,260 @@ class SyncService { } /** - * 동기화 상태 조회 + * SWP 시스템용 데이터 변환 */ - async getSyncStatus(contractId: number, targetSystem: string = 'SHI') { - const [status] = await db - .select() - .from(syncStatusView) + private async transformChangesForSWP(changes: ChangeLog[]): Promise<SyncableEntity[]> { + const syncData: SyncableEntity[] = [] + + for (const change of changes) { + try { + let entityData = null + + // 엔티티 타입별로 현재 데이터 조회 + switch (change.entityType) { + case 'document': + if (change.action !== 'DELETE') { + const [document] = await db + .select() + .from(documents) + .where(eq(documents.id, change.entityId)) + .limit(1) + entityData = document + } + break + + case 'revision': + if (change.action !== 'DELETE') { + const [revision] = await db + .select() + .from(revisions) + .where(eq(revisions.id, change.entityId)) + .limit(1) + entityData = revision + } + break + + case 'attachment': + if (change.action !== 'DELETE') { + const [attachment] = await db + .select() + .from(documentAttachments) + .where(eq(documentAttachments.id, change.entityId)) + .limit(1) + entityData = attachment + } + break + } + + // SWP 특화 데이터 구조 + syncData.push({ + entityType: change.entityType as any, + entityId: change.entityId, + action: change.action as any, + data: entityData || change.oldValues, + metadata: { + changeId: change.id, + changedAt: change.createdAt, + changedBy: change.userName, + changedFields: change.changedFields, + // SWP 전용 메타데이터 + swpFormat: 'legacy', + batchSequence: syncData.length + 1, + needsValidation: change.entityType === 'document', + legacyId: `SWP_${change.entityId}_${Date.now()}` + } + }) + + } catch (error) { + console.error(`Failed to transform change ${change.id} for SWP:`, error) + } + } + + return syncData + } + + /** + * 간단한 XML 변환 헬퍼 (SWP용) + */ + private convertToXML(data: any): string { + const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>' + const xmlBody = ` + <SyncRequest> + <ContractId>${data.contractId}</ContractId> + <SystemType>${data.systemType}</SystemType> + <BatchSize>${data.batchSize}</BatchSize> + <Timestamp>${data.timestamp}</Timestamp> + <Source>${data.source}</Source> + <Changes> + ${data.changes.map((change: SyncableEntity) => ` + <Change> + <EntityType>${change.entityType}</EntityType> + <EntityId>${change.entityId}</EntityId> + <Action>${change.action}</Action> + <Data>${JSON.stringify(change.data)}</Data> + </Change> + `).join('')} + </Changes> + </SyncRequest>` + + return xmlHeader + xmlBody + } + + /** + * 성공한 변경사항들을 동기화 완료로 표시 + */ + private async markChangesAsSynced(changeIds: number[]) { + if (changeIds.length === 0) return + + await db + .update(changeLogs) + .set({ + isSynced: true, + syncedAt: new Date() + }) + .where(inArray(changeLogs.id, changeIds)) + + // 리비전 상태 업데이트 + const revisionChanges = await db + .select({ entityId: changeLogs.entityId }) + .from(changeLogs) .where(and( - eq(syncStatusView.contractId, contractId), - eq(syncStatusView.targetSystem, targetSystem) + inArray(changeLogs.id, changeIds), + eq(changeLogs.entityType, 'revision') )) - .limit(1) - return status + if (revisionChanges.length > 0) { + const revisionIds = revisionChanges.map(c => c.entityId) + await db.update(revisions) + .set({ + revisionStatus: "SUBMITTED", + externalSentAt: new Date().toISOString().slice(0, 10) + }) + .where(inArray(revisions.id, revisionIds)) + } + } + + /** + * 실패한 변경사항들의 재시도 횟수 증가 + */ + private async incrementSyncAttempts(changeIds: number[], errorMessage?: string) { + if (changeIds.length === 0) return + + await db + .update(changeLogs) + .set({ + syncAttempts: sql`${changeLogs.syncAttempts} + 1`, + lastSyncError: errorMessage || 'Unknown error' + }) + .where(inArray(changeLogs.id, changeIds)) + } + + /** + * 동기화 상태 조회 + */ + async getSyncStatus(contractId: number, targetSystem: string = 'DOLCE') { + try { + // 대기 중인 변경사항 수 조회 + const pendingCount = await db.$count( + changeLogs, + and( + eq(changeLogs.contractId, contractId), + eq(changeLogs.isSynced, false), + lt(changeLogs.syncAttempts, 3), + sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` + ) + ) + + // 동기화된 변경사항 수 조회 + const syncedCount = await db.$count( + changeLogs, + and( + eq(changeLogs.contractId, contractId), + eq(changeLogs.isSynced, true), + sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` + ) + ) + + // 실패한 변경사항 수 조회 + const failedCount = await db.$count( + changeLogs, + and( + eq(changeLogs.contractId, contractId), + eq(changeLogs.isSynced, false), + sql`${changeLogs.syncAttempts} >= 3`, + sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` + ) + ) + + // 마지막 성공한 배치 조회 + const [lastSuccessfulBatch] = await db + .select() + .from(syncBatches) + .where(and( + eq(syncBatches.contractId, contractId), + eq(syncBatches.targetSystem, targetSystem), + eq(syncBatches.status, 'SUCCESS') + )) + .orderBy(desc(syncBatches.completedAt)) + .limit(1) + + return { + contractId, + targetSystem, + totalChanges: pendingCount + syncedCount + failedCount, + pendingChanges: pendingCount, + syncedChanges: syncedCount, + failedChanges: failedCount, + lastSyncAt: lastSuccessfulBatch?.completedAt?.toISOString() || null, + syncEnabled: this.isSyncEnabled(targetSystem) + } + } catch (error) { + console.error('Failed to get sync status:', error) + throw error + } } /** * 최근 동기화 배치 목록 조회 */ - async getRecentSyncBatches(contractId: number, targetSystem: string = 'SHI', limit: number = 10) { - return await db - .select() - .from(syncBatches) - .where(and( - eq(syncBatches.contractId, contractId), - eq(syncBatches.targetSystem, targetSystem) - )) - .orderBy(desc(syncBatches.createdAt)) - .limit(limit) + async getRecentSyncBatches(contractId: number, targetSystem: string = 'DOLCE', limit: number = 10) { + try { + const batches = await db + .select() + .from(syncBatches) + .where(and( + eq(syncBatches.contractId, contractId), + eq(syncBatches.targetSystem, targetSystem) + )) + .orderBy(desc(syncBatches.createdAt)) + .limit(limit) + + // Date 객체를 문자열로 변환 + return batches.map(batch => ({ + id: Number(batch.id), + contractId: batch.contractId, + targetSystem: batch.targetSystem, + batchSize: batch.batchSize, + status: batch.status, + startedAt: batch.startedAt?.toISOString() || null, + completedAt: batch.completedAt?.toISOString() || null, + errorMessage: batch.errorMessage, + retryCount: batch.retryCount, + successCount: batch.successCount, + failureCount: batch.failureCount, + createdAt: batch.createdAt.toISOString(), + updatedAt: batch.updatedAt.toISOString() + })) + } catch (error) { + console.error('Failed to get sync batches:', error) + throw error + } } } export const syncService = new SyncService() -// 편의 함수들 +// 편의 함수들 (기본 타겟 시스템을 DOLCE로 변경) export async function logDocumentChange( contractId: number, documentId: number, @@ -461,9 +893,10 @@ export async function logDocumentChange( newValues?: any, oldValues?: any, userId?: number, - userName?: string + userName?: string, + targetSystems: string[] = ["DOLCE", "SWP"] ) { - return syncService.logChange(contractId, 'document', documentId, action, newValues, oldValues, userId, userName) + return syncService.logChange(contractId, 'document', documentId, action, newValues, oldValues, userId, userName, targetSystems) } export async function logRevisionChange( @@ -473,9 +906,10 @@ export async function logRevisionChange( newValues?: any, oldValues?: any, userId?: number, - userName?: string + userName?: string, + targetSystems: string[] = ["DOLCE", "SWP"] ) { - return syncService.logChange(contractId, 'revision', revisionId, action, newValues, oldValues, userId, userName) + return syncService.logChange(contractId, 'revision', revisionId, action, newValues, oldValues, userId, userName, targetSystems) } export async function logAttachmentChange( @@ -485,7 +919,8 @@ export async function logAttachmentChange( newValues?: any, oldValues?: any, userId?: number, - userName?: string + userName?: string, + targetSystems: string[] = ["DOLCE", "SWP"] ) { - return syncService.logChange(contractId, 'attachment', attachmentId, action, newValues, oldValues, userId, userName) + return syncService.logChange(contractId, 'attachment', attachmentId, action, newValues, oldValues, userId, userName, targetSystems) }
\ No newline at end of file |
