// lib/vendor-document-list/dolce-upload-service.ts import db from "@/db/db" import { documents, revisions, documentAttachments, contracts, projects, vendors, issueStages } from "@/db/schema" import { eq, and, desc, sql, inArray, min } from "drizzle-orm" import { v4 as uuidv4 } from "uuid" import path from "path" import * as crypto from "crypto" import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" export interface DOLCEUploadResult { success: boolean uploadedDocuments: number uploadedFiles: number errors?: string[] results?: { documentResults?: any[] fileResults?: any[] mappingResults?: any[] } } interface ResultData { FileId: string; UploadId: string; FileSeq: number; FileName: string; FileRelativePath: string; FileSize: number; FileCreateDT: string; // ISO string format FileWriteDT: string; // ISO string format OwnerUserId: string; } interface FileReaderConfig { baseDir: string; isProduction: boolean; } interface DOLCEDocument { Mode: "ADD" | "MOD" Status: string RegisterId: number ProjectNo: string Discipline: string DrawingKind: string DrawingNo: string DrawingName: string RegisterGroupId: number RegisterSerialNo: number RegisterKind: string DrawingRevNo: string Category: string Receiver: string | null Manager: string RegisterDesc: string UploadId?: string RegCompanyCode: string } interface DOLCEFileMapping { CGbn?: string Category?: string CheckBox: string DGbn?: string DegreeGbn?: string DeptGbn?: string Discipline: string DrawingKind: string DrawingMoveGbn: string DrawingName: string DrawingNo: string DrawingUsage: string FileNm: string JGbn?: string Manager: string MappingYN: string NewOrNot: string ProjectNo: string RegisterGroup: number RegisterGroupId: number RegisterKindCode: string RegisterSerialNo: number RevNo?: string SGbn?: string UploadId: string } function getFileReaderConfig(): FileReaderConfig { const isProduction = process.env.NODE_ENV === "production"; if (isProduction) { return { baseDir: process.env.NAS_PATH || "/evcp_nas", // NAS 기본 경로 isProduction: true, }; } else { return { baseDir: process.cwd(), // 개발환경 현재 디렉토리 isProduction: false, }; } } class DOLCEUploadService { private readonly BASE_URL = process.env.DOLCE_API_URL || 'http://60.100.99.217:1111' private readonly UPLOAD_SERVICE_URL = process.env.DOLCE_UPLOAD_URL || 'http://60.100.99.217:1111/PWPUploadService.ashx' /** * 메인 업로드 함수: 변경된 문서와 파일을 DOLCE로 업로드 */ async uploadToDoLCE( projectId: number, revisionIds: number[], userId: string, userName?: string ): Promise { try { console.log(`Starting DOLCE upload for contract ${projectId}, revisions: ${revisionIds.join(', ')}`) // 1. 계약 정보 조회 (프로젝트 코드, 벤더 코드 등) const contractInfo = await this.getContractInfo(projectId) if (!contractInfo) { throw new Error(`Contract info not found for ID: ${projectId}`) } // 2. 업로드할 리비전 정보 조회 const revisionsToUpload = await this.getRevisionsForUpload(revisionIds) if (revisionsToUpload.length === 0) { return { success: true, uploadedDocuments: 0, uploadedFiles: 0 } } let uploadedDocuments = 0 let uploadedFiles = 0 const errors: string[] = [] const results: any = { documentResults: [], fileResults: [], mappingResults: [] } // 3. 각 리비전별로 처리 for (const revision of revisionsToUpload) { try { console.log(`Processing revision ${revision.revision} for document ${revision.documentNo}`) // 3-1. UploadId 미리 생성 (파일이 있는 경우에만) let uploadId: string | undefined if (revision.attachments && revision.attachments.length > 0) { uploadId = uuidv4() // 문서 업로드 시 사용할 UploadId 미리 생성 console.log(`Generated UploadId for document upload: ${uploadId}`) } // 3-2. 문서 정보 업로드 (UploadId 포함) const dolceDoc = this.transformToDoLCEDocument( revision, contractInfo, uploadId, // 미리 생성된 UploadId 사용 contractInfo.vendorCode, ) const docResult = await this.uploadDocument([dolceDoc], userId) if (!docResult.success) { errors.push(`Document upload failed for ${revision.documentNo}: ${docResult.error}`) continue // 문서 업로드 실패 시 다음 리비전으로 넘어감 } uploadedDocuments++ results.documentResults.push(docResult) console.log(`✅ Document uploaded successfully: ${revision.documentNo}`) // 3-3. 파일 업로드 (이미 생성된 UploadId 사용) if (uploadId && revision.attachments && revision.attachments.length > 0) { try { // 파일 업로드 시 이미 생성된 UploadId 사용 const fileUploadResults = await this.uploadFiles( revision.attachments, userId, uploadId // 이미 생성된 UploadId 전달 ) } catch (fileError) { errors.push(`File upload failed for ${revision.documentNo}: ${fileError instanceof Error ? fileError.message : 'Unknown error'}`) console.error(`❌ File upload failed for ${revision.documentNo}:`, fileError) } } // 3-5. 성공한 리비전의 상태 업데이트 await this.updateRevisionStatus(revision.id, 'SUBMITTED', uploadId) } catch (error) { const errorMessage = `Failed to process revision ${revision.revision}: ${error instanceof Error ? error.message : 'Unknown error'}` errors.push(errorMessage) console.error(errorMessage, error) } } return { success: errors.length === 0, uploadedDocuments, uploadedFiles, errors: errors.length > 0 ? errors : undefined, results } } catch (error) { console.error('DOLCE upload failed:', error) throw error } } /** * 계약 정보 조회 */ private async getContractInfo(projectId: number): Promise<{ projectCode: string; vendorCode: string; } | null> { const session = await getServerSession(authOptions) if (!session?.user?.companyId) { throw new Error("인증이 필요합니다.") } const [result] = await db .select({ 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)))) .limit(1) return result?.projectCode && result?.vendorCode ? { projectCode: result.projectCode, vendorCode: result.vendorCode } : null } /** * 각 issueStageId별로 첫 번째 revision 정보를 조회 */ private async getFirstRevisionMap(issueStageIds: number[]): Promise> { const firstRevisions = await db .select({ issueStageId: revisions.issueStageId, firstRevision: min(revisions.revision) }) .from(revisions) .where(inArray(revisions.issueStageId, issueStageIds)) .groupBy(revisions.issueStageId) const map = new Map() firstRevisions.forEach(item => { if (item.firstRevision) { map.set(item.issueStageId, item.firstRevision) } }) return map } /** * 업로드할 리비전 정보 조회 (문서 정보 및 첨부파일 포함) */ private async getRevisionsForUpload(revisionIds: number[]) { // revisions → issueStages → documents 순서로 join하여 정보 조회 const revisionResults = await db .select({ // revision 테이블 정보 id: revisions.id, registerId: revisions.registerId, revision: revisions.revision, // revisionNo가 아니라 revision revisionStatus: revisions.revisionStatus, uploaderId: revisions.uploaderId, uploaderName: revisions.uploaderName, submittedDate: revisions.submittedDate, comment: revisions.comment, usage: revisions.usage, usageType: revisions.usageType, // ✅ DOLCE 연동 필드들 (새로 추가) externalUploadId: revisions.externalUploadId, externalRegisterId: revisions.id, externalSentAt: revisions.submittedDate, // issueStages 테이블 정보 issueStageId: issueStages.id, stageName: issueStages.stageName, documentId: issueStages.documentId, // documents 테이블 정보 (DOLCE 업로드에 필요한 모든 필드) documentNo: documents.docNumber, documentName: documents.title, drawingKind: documents.drawingKind, drawingMoveGbn: documents.drawingMoveGbn, discipline: documents.discipline, registerGroupId: documents.registerGroupId, // DOLCE B4 전용 필드들 cGbn: documents.cGbn, dGbn: documents.dGbn, degreeGbn: documents.degreeGbn, deptGbn: documents.deptGbn, jGbn: documents.jGbn, sGbn: documents.sGbn, // DOLCE 추가 정보 manager: documents.manager, managerENM: documents.managerENM, managerNo: documents.managerNo, shiDrawingNo: documents.shiDrawingNo, // 외부 시스템 연동 정보 externalDocumentId: documents.externalDocumentId, externalSystemType: documents.externalSystemType, externalSyncedAt: documents.externalSyncedAt }) .from(revisions) .innerJoin(issueStages, eq(revisions.issueStageId, issueStages.id)) .innerJoin(documents, eq(issueStages.documentId, documents.id)) .where(inArray(revisions.id, revisionIds)) // 각 리비전의 첨부파일 정보도 조회 const revisionsWithAttachments = [] for (const revision of revisionResults) { const attachments = await db .select({ id: documentAttachments.id, uploadId: documentAttachments.uploadId, fileId: documentAttachments.fileId, fileName: documentAttachments.fileName, filePath: documentAttachments.filePath, fileType: documentAttachments.fileType, fileSize: documentAttachments.fileSize, createdAt: documentAttachments.createdAt }) .from(documentAttachments) .where(eq(documentAttachments.revisionId, revision.id)) revisionsWithAttachments.push({ ...revision, attachments }) } return revisionsWithAttachments } /** * 파일 업로드 (PWPUploadService.ashx) - 수정된 버전 * @param attachments 업로드할 첨부파일 목록 * @param userId 사용자 ID * @param uploadId 이미 생성된 UploadId (문서 업로드 시 생성됨) */ private async uploadFiles( attachments: any[], userId: string, uploadId: string // 이미 생성된 UploadId를 매개변수로 받음 ): Promise> { const uploadResults = [] const resultDataArray: ResultData[] = [] for (let i = 0; i < attachments.length; i++) { const attachment = attachments[i] try { // FileId만 새로 생성 (UploadId는 이미 생성된 것 사용) const fileId = uuidv4() console.log(`Uploading file with predefined UploadId: ${uploadId}, FileId: ${fileId}`) // 파일 데이터 읽기 const fileBuffer = await this.getFileBuffer(attachment.filePath) const uploadUrl = `${this.UPLOAD_SERVICE_URL}?UploadId=${uploadId}&FileId=${fileId}` const response = await fetch(uploadUrl, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', }, body: fileBuffer }) if (!response.ok) { const errorText = await response.text() throw new Error(`File upload failed: HTTP ${response.status} - ${errorText}`) } const dolceFilePath = await response.text() // DOLCE에서 반환하는 파일 경로 // 업로드 성공 후 documentAttachments 테이블 업데이트 await db .update(documentAttachments) .set({ uploadId: uploadId, // 이미 생성된 UploadId 사용 fileId: fileId, uploadedBy: userId, dolceFilePath: dolceFilePath, uploadedAt: new Date(), updatedAt: new Date() }) .where(eq(documentAttachments.id, attachment.id)) uploadResults.push({ uploadId, fileId, filePath: dolceFilePath }) // ResultData 객체 생성 (PWPUploadResultService 호출용) const fileStats = await this.getFileStats(attachment.filePath) // 파일 통계 정보 조회 const resultData: ResultData = { FileId: fileId, UploadId: uploadId, FileSeq: i + 1, // 1부터 시작하는 시퀀스 FileName: attachment.fileName, FileRelativePath: dolceFilePath, FileSize: fileStats.size, FileCreateDT: fileStats.birthtime.toISOString(), FileWriteDT: fileStats.mtime.toISOString(), OwnerUserId: userId } resultDataArray.push(resultData) console.log(`✅ File uploaded successfully: ${attachment.fileName} -> ${dolceFilePath}`) console.log(`✅ DB updated for attachment ID: ${attachment.id}`) // 🧪 DOLCE 업로드 확인 테스트 try { const testResult = await this.testDOLCEFileDownload(fileId, userId, attachment.fileName) if (testResult.success) { console.log(`✅ DOLCE 업로드 확인 성공: ${attachment.fileName}`) } else { console.warn(`⚠️ DOLCE 업로드 확인 실패: ${attachment.fileName} - ${testResult.error}`) } } catch (testError) { console.warn(`⚠️ DOLCE 업로드 확인 중 오류: ${attachment.fileName}`, testError) } } catch (error) { console.error(`❌ File upload failed for ${attachment.fileName}:`, error) throw error } } // 모든 파일 업로드가 완료된 후 PWPUploadResultService 호출 if (resultDataArray.length > 0) { try { await this.finalizeUploadResult(resultDataArray) console.log(`✅ Upload result finalized for UploadId: ${uploadId}`) } catch (error) { console.error(`❌ Failed to finalize upload result for UploadId: ${uploadId}`, error) // 파일 업로드는 성공했지만 결과 저장 실패 - 로그만 남기고 계속 진행 } } return uploadResults } private async finalizeUploadResult(resultDataArray: ResultData[]): Promise { const url = `${this.BASE_URL}/PWPUploadResultService.ashx?` try { const jsonData = JSON.stringify(resultDataArray) const dataBuffer = Buffer.from(jsonData, 'utf-8') console.log(`Calling PWPUploadResultService with ${resultDataArray.length} files`) console.log('ResultData:', JSON.stringify(resultDataArray, null, 2)) const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: dataBuffer }) if (!response.ok) { const errorText = await response.text() throw new Error(`PWPUploadResultService failed: HTTP ${response.status} - ${errorText}`) } const result = await response.text() if (result !== 'Success') { console.log(result, "돌체 업로드 실패") throw new Error(`PWPUploadResultService returned unexpected result: ${result}`) } console.log('✅ PWPUploadResultService call successful') } catch (error) { console.error('❌ PWPUploadResultService call failed:', error) throw error } } // 파일 통계 정보 조회 헬퍼 메서드 (파일시스템에서 파일 정보를 가져옴) private async getFileStats(filePath: string): Promise<{ size: number, birthtime: Date, mtime: Date }> { try { // Node.js 환경이라면 fs.stat 사용 const fs = require('fs').promises const stats = await fs.stat(filePath) return { size: stats.size, birthtime: stats.birthtime, mtime: stats.mtime } } catch (error) { console.warn(`Could not get file stats for ${filePath}, using defaults`) // 파일 정보를 가져올 수 없는 경우 기본값 사용 const now = new Date() return { size: 0, birthtime: now, mtime: now } } } /** * 문서 정보 업로드 (DetailDwgReceiptMgmtEdit) */ private async uploadDocument(dwgList: DOLCEDocument[], userId: string): Promise<{ success: boolean, error?: string, data?: any }> { try { const endpoint = `${this.BASE_URL}/Services/VDCSWebService.svc/DetailDwgReceiptMgmtEdit` const requestBody = { DwgList: dwgList, UserID: userId } console.log('Uploading documents to DOLCE:', JSON.stringify(requestBody, null, 2)) 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(`HTTP ${response.status} - ${errorText}`) } const result = await response.json() return { success: true, data: result } } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' } } } /** * 파일 매핑 정보 업로드 (MatchBatchFileDwgEdit) */ private async uploadFileMapping(mappingList: DOLCEFileMapping[], userId: string): Promise<{ success: boolean, error?: string, data?: any }> { try { const endpoint = `${this.BASE_URL}/Services/VDCSWebService.svc/MatchBatchFileDwgEdit` const requestBody = { mappingSaveLists: mappingList, UserID: userId } console.log('Uploading file mapping to DOLCE:', JSON.stringify(requestBody, null, 2)) 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(`HTTP ${response.status} - ${errorText}`) } const result = await response.json() return { success: true, data: result } } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' } } } /** * 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용) */ /** * 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용) */ private transformToDoLCEDocument( revision: any, contractInfo: any, uploadId?: string, vendorCode?: string, ): DOLCEDocument { // Mode 결정: registerId가 있으면 MOD, 없으면 ADD let mode: "ADD" | "MOD" = "ADD" // 기본값은 ADD if (revision.registerId) { mode = "MOD" } else { mode = "ADD" } // RegisterKind 결정: usage와 usageType에 따라 설정 let registerKind = "APPR" // 기본값 if (revision.usage && revision.usage !== 'DEFAULT') { switch (revision.usage) { case "APPROVAL": if (revision.usageType === "Full") { registerKind = "APPR" } else if (revision.usageType === "Partial") { registerKind = "APPR-P" } else { registerKind = "APPR" // 기본값 } break case "WORKING": if (revision.usageType === "Full") { registerKind = "WORK" } else if (revision.usageType === "Partial") { registerKind = "WORK-P" } else { registerKind = "WORK" // 기본값 } break case "The 1st": registerKind = "FMEA-1" break case "The 2nd": registerKind = "FMEA-2" break case "Pre": registerKind = "RECP" break case "Working": registerKind = "RECW" break case "Mark-Up": registerKind = "CMTM" break case "Comment": // 김혜빈 프로 요청사항 20250826 // DrawingKind에 따라 분기 if (revision.drawingKind === "B3") { registerKind = "CMTV" // B3(Vendor) Comment } else if (revision.drawingKind === "B4" && revision.drawingMoveGbn === "GTT Deliverable") { registerKind = "CMTQ" // B4(GTT) + GTT Deliverable } else { registerKind = "CMTV" // 기타 Comment (기본) } break default: console.warn(`Unknown usage type: ${revision.usage}, using default APPR`) registerKind = "APPR" // 기본값 break } } else { console.warn(`No usage specified for revision ${revision.revision}, using default APPR`) } // Serial Number 계산 함수 const getSerialNumber = (revisionValue: string): number => { if (!revisionValue) { return 1 } // 먼저 숫자인지 확인 const numericValue = parseInt(revisionValue) if (!isNaN(numericValue)) { return numericValue } // 문자인 경우 (a=1, b=2, c=3, ...) if (typeof revisionValue === 'string' && revisionValue.length === 1) { const charCode = revisionValue.toLowerCase().charCodeAt(0) if (charCode >= 97 && charCode <= 122) { // a-z return charCode - 96 // a=1, b=2, c=3, ... } } // 기본값 return 1 } console.log(`Transform to DOLCE: Mode=${mode}, RegisterKind=${registerKind}, Usage=${revision.usage}, UsageType=${revision.usageType}`) return { Mode: mode, // Status: revision.revisionStatus || "Standby", Status: "Standby", RegisterId: revision.registerId || 0, // registerId가 없으면 0 (ADD 모드) ProjectNo: contractInfo.projectCode, Discipline: revision.discipline || "DL", DrawingKind: revision.drawingKind || "B3", DrawingNo: revision.documentNo, DrawingName: revision.documentName, RegisterGroupId: revision.registerGroupId || 0, RegisterSerialNo: getSerialNumber(revision.revision || "1"), RegisterKind: registerKind, // usage/usageType에 따라 동적 설정 DrawingRevNo: revision.revision || "-", Category: revision.category || "TS", Receiver: null, Manager: revision.managerNo || "202206", // 담당자 번호 사용 RegisterDesc: revision.comment || "System upload", UploadId: uploadId, RegCompanyCode: vendorCode || "A0005531" // 벤더 코드 } } /** * 파일 매핑 데이터 변환 */ private transformToFileMapping( revision: any, contractInfo: any, uploadId: string, fileName: string ): DOLCEFileMapping { return { CGbn: revision.cGbn, Category: revision.category, CheckBox: "0", DGbn: revision.dGbn, DegreeGbn: revision.degreeGbn, DeptGbn: revision.deptGbn, Discipline: revision.discipline || "DL", DrawingKind: revision.drawingKind || "B4", DrawingMoveGbn: revision.drawingMoveGbn || "도면입수", DrawingName: revision.documentName, DrawingNo: revision.documentNo, DrawingUsage: "입수용", FileNm: fileName, JGbn: revision.jGbn, Manager: revision.managerNo || "970043", MappingYN: "Y", NewOrNot: "N", ProjectNo: contractInfo.projectCode, RegisterGroup: 0, RegisterGroupId: revision.registerGroupId || 0, RegisterKindCode: "RECW", RegisterSerialNo: parseInt(revision.revision) || 1, RevNo: revision.revision, SGbn: revision.sGbn, UploadId: uploadId } } /** * 파일 버퍼 읽기 (실제 파일 시스템 기반) - 타입 에러 수정 */ private async getFileBuffer(filePath: string): Promise { try { console.log(`📂 파일 읽기 요청: ${filePath}`); if (filePath.startsWith('http')) { // ✅ URL인 경우 직접 다운로드 (기존과 동일) console.log(`🌐 HTTP URL에서 파일 다운로드: ${filePath}`); const response = await fetch(filePath); if (!response.ok) { throw new Error(`파일 다운로드 실패: ${response.status}`); } const arrayBuffer = await response.arrayBuffer(); console.log(`✅ HTTP 다운로드 완료: ${arrayBuffer.byteLength} bytes`); return arrayBuffer; } else { // ✅ 로컬/NAS 파일 경로 처리 (환경별 분기) const fs = await import('fs'); const path = await import('path'); const config = getFileReaderConfig(); let actualFilePath: string; // 경로 형태별 처리 if (filePath.startsWith('/documents/')) { // ✅ DB에 저장된 경로 형태: "/documents/[uuid].ext" // 개발: public/documents/[uuid].ext // 프로덕션: /evcp_nas/documents/[uuid].ext actualFilePath = path.join(config.baseDir, 'public', filePath.substring(1)); // 앞의 '/' 제거 console.log(`📁 documents 경로 처리: ${filePath} → ${actualFilePath}`); } else if (filePath.startsWith('/api/files')) { actualFilePath = `${process.env.NEXT_PUBLIC_URL}${filePath}` const response = await fetch(actualFilePath); if (!response.ok) { throw new Error(`파일 다운로드 실패: ${response.status}`); } const arrayBuffer = await response.arrayBuffer(); console.log(`✅ HTTP 다운로드 완료: ${arrayBuffer.byteLength} bytes`); return arrayBuffer; } else { // ✅ 상대 경로는 현재 디렉토리 기준 actualFilePath = filePath; console.log(`📂 상대 경로 사용: ${actualFilePath}`); } console.log(`🔍 실제 파일 경로: ${actualFilePath}`); console.log(`🏠 환경: ${config.isProduction ? 'PRODUCTION (NAS)' : 'DEVELOPMENT (public)'}`); // 파일 존재 여부 확인 if (!fs.existsSync(actualFilePath)) { console.error(`❌ 파일 없음: ${actualFilePath}`); throw new Error(`파일을 찾을 수 없습니다: ${actualFilePath}`); } // 파일 읽기 const fileBuffer = fs.readFileSync(actualFilePath); console.log(`✅ 파일 읽기 성공: ${actualFilePath} (${fileBuffer.length} bytes)`); // ✅ Buffer를 ArrayBuffer로 정확히 변환 const arrayBuffer = new ArrayBuffer(fileBuffer.length); const uint8Array = new Uint8Array(arrayBuffer); uint8Array.set(fileBuffer); return arrayBuffer; } } catch (error) { console.error(`❌ 파일 읽기 실패: ${filePath}`, error); throw error; } } /** * 리비전 상태 업데이트 (업데이트된 스키마 사용) */ private async updateRevisionStatus(revisionId: number, status: string, uploadId?: string) { const updateData: any = { revisionStatus: status, updatedAt: new Date() } // 업로드 성공 시 관련 날짜 설정 if (status === 'SUBMITTED') { updateData.submittedDate = new Date().toISOString().slice(0, 10) // updateData.externalSentAt = new Date().toISOString().slice(0, 10) } else if (status === 'APPROVED') { updateData.approvedDate = new Date().toISOString().slice(0, 10) } // DOLCE 업로드 ID 저장 if (uploadId) { updateData.externalUploadId = uploadId } await db .update(revisions) .set(updateData) .where(eq(revisions.id, revisionId)) console.log(`✅ Updated revision ${revisionId} status to ${status}${uploadId ? ` with upload ID: ${uploadId}` : ''}`) } /** * 업로드 가능 여부 확인 */ isUploadEnabled(): boolean { const enabled = process.env.DOLCE_UPLOAD_ENABLED return enabled === 'true' || enabled === '1' } /** * DOLCE 업로드 확인 테스트 (업로드 후 파일이 DOLCE에 존재하는지 확인) */ private async testDOLCEFileDownload( fileId: string, userId: string, fileName: string ): Promise<{ success: boolean; downloadUrl?: string; error?: string }> { try { // DES 암호화 (C# DESCryptoServiceProvider 호환) const DES_KEY = Buffer.from("4fkkdijg", "ascii") // 암호화 문자열 생성: FileId↔UserId↔FileName const encryptString = `${fileId}↔${userId}↔${fileName}` // DES 암호화 (createCipheriv 사용) const cipher = crypto.createCipheriv('des-ecb', DES_KEY, '') cipher.setAutoPadding(true) let encrypted = cipher.update(encryptString, 'utf8', 'base64') encrypted += cipher.final('base64') const encryptedKey = encrypted.replace(/\+/g, '|||') const downloadUrl = `${process.env.DOLCE_DOWNLOAD_URL}?key=${encryptedKey}` || `http://60.100.99.217:1111/Download.aspx?key=${encryptedKey}` console.log(`🧪 DOLCE 파일 다운로드 테스트:`) console.log(` 파일명: ${fileName}`) console.log(` FileId: ${fileId}`) console.log(` UserId: ${userId}`) console.log(` 암호화 키: ${encryptedKey}`) console.log(` 다운로드 URL: ${downloadUrl}`) const response = await fetch(downloadUrl, { method: 'GET', headers: { 'User-Agent': 'DOLCE-Integration-Service' } }) if (!response.ok) { console.error(`❌ DOLCE 파일 다운로드 테스트 실패: HTTP ${response.status}`) return { success: false, downloadUrl, error: `HTTP ${response.status}` } } const buffer = Buffer.from(await response.arrayBuffer()) console.log(`✅ DOLCE 파일 다운로드 테스트 성공: ${fileName} (${buffer.length} bytes)`) return { success: true, downloadUrl } } catch (error) { console.error(`❌ DOLCE 파일 다운로드 테스트 실패: ${fileName}`, error) return { success: false, error: error instanceof Error ? error.message : 'Unknown error' } } } } export const dolceUploadService = new DOLCEUploadService() // 편의 함수 export async function uploadRevisionsToDOLCE( projectId: number, revisionIds: number[], userId: string, userName?: string ): Promise { return dolceUploadService.uploadToDoLCE(projectId, revisionIds, userId, userName) }