diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-06 04:25:00 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-06 04:25:00 +0000 |
| commit | 0e68be98b4ad4691af77232cb0b02b17af825ba3 (patch) | |
| tree | 09dd9152c87a9c21343e6a1562c959a828e2b462 | |
| parent | de2ac5a2860bc25180971e7a11f852d9d44675b7 (diff) | |
(김준회) 문서/도서 리스트 및 제출(조선) 메뉴 디버깅(dolce)
- attachment revisionId 누락건 해결
- crypto 모듈에서 deprecated des-ecb 알고리즘 사용을 위한 추가 처리
- enhancedDocumentsView에 projectId 추가
- route에서 contractId -> projectId 사용하도록 변경
| -rw-r--r-- | app/api/revision-upload-ship/route.ts | 18 | ||||
| -rw-r--r-- | app/api/revision-upload/route.ts | 11 | ||||
| -rw-r--r-- | db/schema/vendorDocu.ts | 2 | ||||
| -rw-r--r-- | lib/vendor-document-list/dolce-upload-service.ts | 87 | ||||
| -rw-r--r-- | lib/vendor-document-list/import-service.ts | 71 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/send-to-shi-button.tsx | 6 | ||||
| -rw-r--r-- | lib/vendor-document-list/sync-service.ts | 14 | ||||
| -rw-r--r-- | package.json | 6 |
8 files changed, 190 insertions, 25 deletions
diff --git a/app/api/revision-upload-ship/route.ts b/app/api/revision-upload-ship/route.ts index 3d1ebba9..671b8bac 100644 --- a/app/api/revision-upload-ship/route.ts +++ b/app/api/revision-upload-ship/route.ts @@ -54,7 +54,10 @@ export async function POST(request: NextRequest) { /* ------- 계약 ID 확보 ------- */ const [docInfo] = await db - .select({ contractId: documents.contractId }) + .select({ + contractId: documents.contractId, + projectId: documents.projectId + }) .from(documents) .where(eq(documents.id, docId)) .limit(1) @@ -63,6 +66,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Document not found" }, { status: 404 }) } + // projectId가 null인 경우 처리 + if (!docInfo.projectId) { + return NextResponse.json({ + error: "Document must have a valid project ID for synchronization" + }, { status: 400 }) + } + /* ------- Stage 찾기 로직 ------- */ // 1. usage 값과 일치하는 stage 찾기 let targetStage = await db @@ -148,7 +158,7 @@ export async function POST(request: NextRequest) { revisionData = updated await logRevisionChange( - docInfo.contractId, + docInfo.projectId!, // null 체크 후이므로 non-null assertion 사용 revisionId, "UPDATE", updated, @@ -178,7 +188,7 @@ export async function POST(request: NextRequest) { revisionData = newRev await logRevisionChange( - docInfo.contractId, + docInfo.projectId!, // null 체크 후이므로 non-null assertion 사용 revisionId, "CREATE", newRev, @@ -233,7 +243,7 @@ export async function POST(request: NextRequest) { // change_logs: attachment CREATE await logAttachmentChange( - docInfo.contractId, + docInfo.projectId!, att.id, "CREATE", att, diff --git a/app/api/revision-upload/route.ts b/app/api/revision-upload/route.ts index bd75e0b5..0f67def6 100644 --- a/app/api/revision-upload/route.ts +++ b/app/api/revision-upload/route.ts @@ -65,6 +65,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Document not found" }, { status: 404 }) } + // projectId가 null인 경우 처리 + if (!docInfo.projectId) { + return NextResponse.json({ + error: "Document must have a valid project ID for synchronization" + }, { status: 400 }) + } + /* ------- 트랜잭션 ------- */ const result = await db.transaction(async (tx) => { /* 1) Stage 생성/조회 */ @@ -124,7 +131,7 @@ export async function POST(request: NextRequest) { // change_logs: CREATE await logRevisionChange( - docInfo.projectId, + docInfo.projectId!, // null 체크 후이므로 non-null assertion 사용 revisionId, "CREATE", newRev, @@ -157,7 +164,7 @@ export async function POST(request: NextRequest) { revisionId = revRow.id await logRevisionChange( - docInfo.projectId, + docInfo.projectId!, // null 체크 후이므로 non-null assertion 사용 revisionId, "UPDATE", updated, diff --git a/db/schema/vendorDocu.ts b/db/schema/vendorDocu.ts index 642657e2..d451c408 100644 --- a/db/schema/vendorDocu.ts +++ b/db/schema/vendorDocu.ts @@ -246,6 +246,7 @@ export const enhancedDocumentsView = pgView("enhanced_documents_view", { contractId: integer("contract_id").notNull(), // ✅ 프로젝트 및 벤더 정보 추가 + projectId: integer("project_id"), projectCode: varchar("project_code", { length: 50 }), vendorName: varchar("vendor_name", { length: 255 }), vendorCode: varchar("vendor_code", { length: 50 }), @@ -503,6 +504,7 @@ export const enhancedDocumentsView = pgView("enhanced_documents_view", { -- ✅ 프로젝트 및 벤더 정보 추가 + c.project_id as project_id, p.code as project_code, v.vendor_name as vendor_name, v.vendor_code as vendor_code, diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts index d0db9f2f..2d6a83c6 100644 --- a/lib/vendor-document-list/dolce-upload-service.ts +++ b/lib/vendor-document-list/dolce-upload-service.ts @@ -4,6 +4,7 @@ import { documents, revisions, documentAttachments, contracts, projects, vendors 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" export interface DOLCEUploadResult { success: boolean @@ -94,7 +95,7 @@ function getFileReaderConfig(): FileReaderConfig { }; } else { return { - baseDir: path.join(process.cwd(), "public"), // 개발환경 public 폴더 + baseDir: process.cwd(), // 개발환경 현재 디렉토리 isProduction: false, }; } @@ -215,7 +216,7 @@ class DOLCEUploadService { /** * 계약 정보 조회 */ - private async getContractInfo(revisionIds: number) { + private async getContractInfo(projectId: number) { const [result] = await db .select({ projectCode: projects.code, @@ -225,7 +226,7 @@ class DOLCEUploadService { .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) .innerJoin(vendors, eq(contracts.vendorId, vendors.id)) - .where(eq(contracts.projectId, revisionIds)) + .where(eq(contracts.projectId, projectId)) .limit(1) return result @@ -421,6 +422,18 @@ private async uploadFiles( 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 @@ -443,7 +456,7 @@ private async uploadFiles( private async finalizeUploadResult(resultDataArray: ResultData[]): Promise<void> { - const url = `${this.UPLOAD_SERVICE_URL}/PWPUploadResultService.ashx` + const url = `${this.BASE_URL}/PWPUploadResultService.ashx?` try { const jsonData = JSON.stringify(resultDataArray) @@ -783,7 +796,7 @@ private transformToDoLCEDocument( // ✅ DB에 저장된 경로 형태: "/documents/[uuid].ext" // 개발: public/documents/[uuid].ext // 프로덕션: /evcp_nas/documents/[uuid].ext - actualFilePath = path.join(config.baseDir, filePath.substring(1)); // 앞의 '/' 제거 + actualFilePath = path.join(config.baseDir, 'public', filePath.substring(1)); // 앞의 '/' 제거 console.log(`📁 documents 경로 처리: ${filePath} → ${actualFilePath}`); } @@ -856,6 +869,70 @@ private transformToDoLCEDocument( 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() diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts index 9e1016ea..f2c62d0b 100644 --- a/lib/vendor-document-list/import-service.ts +++ b/lib/vendor-document-list/import-service.ts @@ -471,7 +471,7 @@ class ImportService { */ private encryptDES(text: string): string { try { - const cipher = crypto.createCipher('des-ecb', this.DES_KEY) + const cipher = crypto.createCipheriv('des-ecb', this.DES_KEY, '') cipher.setAutoPadding(true) let encrypted = cipher.update(text, 'utf8', 'base64') encrypted += cipher.final('base64') @@ -498,7 +498,13 @@ class ImportService { 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)}...`) + console.log(`🔗 DOLCE 다운로드 링크 생성:`) + console.log(` 파일명: ${fileName}`) + console.log(` FileId: ${fileId}`) + console.log(` UserId: ${userId}`) + console.log(` 암호화 키: ${encryptedKey}`) + console.log(` 다운로드 URL: ${downloadUrl}`) + console.log(`📥 DOLCE에서 파일 다운로드 시작: ${fileName}`) const response = await fetch(downloadUrl, { method: 'GET', @@ -508,16 +514,18 @@ class ImportService { }) if (!response.ok) { + console.error(`❌ DOLCE 다운로드 실패: HTTP ${response.status}`) + console.error(` URL: ${downloadUrl}`) throw new Error(`File download failed: HTTP ${response.status}`) } const buffer = Buffer.from(await response.arrayBuffer()) - console.log(`Downloaded ${buffer.length} bytes for ${fileName}`) + console.log(`✅ DOLCE에서 파일 다운로드 완료: ${fileName} (${buffer.length} bytes)`) return buffer } catch (error) { - console.error(`Failed to download file ${fileName}:`, error) + console.error(`❌ DOLCE 파일 다운로드 실패: ${fileName}`, error) throw error } } @@ -1524,6 +1532,61 @@ async getImportStatus( const enabled = process.env[`IMPORT_${upperSystem}_ENABLED`] return enabled === 'true' || enabled === '1' } + + /** + * DOLCE 업로드 확인 테스트 (업로드 후 파일이 DOLCE에 존재하는지 확인) + */ + async testDOLCEFileDownload( + fileId: string, + userId: string, + fileName: string + ): Promise<{ success: boolean; downloadUrl?: string; error?: string }> { + 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(`🧪 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 importService = new ImportService()
\ No newline at end of file diff --git a/lib/vendor-document-list/ship/send-to-shi-button.tsx b/lib/vendor-document-list/ship/send-to-shi-button.tsx index c67c7b2c..7236dfde 100644 --- a/lib/vendor-document-list/ship/send-to-shi-button.tsx +++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx @@ -44,10 +44,10 @@ export function SendToSHIButton({ const targetSystem = projectType === 'ship' ? "DOLCE" : "SWP" - // 문서에서 유효한 계약 ID 목록 추출 + // 문서에서 유효한 계약 ID 목록 추출 (projectId 사용) const documentsContractIds = React.useMemo(() => { const validIds = documents - .map(doc => doc.projectId) + .map(doc => (doc as any).projectId) .filter((id): id is number => typeof id === 'number' && id > 0) const uniqueIds = [...new Set(validIds)] @@ -185,7 +185,7 @@ export function SendToSHIButton({ setSyncProgress(0) setCurrentSyncingContract(null) - const errorMessage = syncUtils.formatError(error as any) + const errorMessage = syncUtils.formatError(error as Error) toast.error('동기화 실패', { description: errorMessage }) diff --git a/lib/vendor-document-list/sync-service.ts b/lib/vendor-document-list/sync-service.ts index e058803b..0544ce06 100644 --- a/lib/vendor-document-list/sync-service.ts +++ b/lib/vendor-document-list/sync-service.ts @@ -309,10 +309,16 @@ class SyncService { throw new Error('DOLCE upload is not enabled') } - // 변경사항에서 리비전 ID들 추출 - const revisionIds = changes - .filter(change => change.entityType === 'revision') - .map(change => change.entityId) + // 변경사항에서 리비전 ID들 추출 (revision 엔티티 + attachment 엔티티의 revisionId) + const revisionIds = [...new Set([ + ...changes + .filter(change => change.entityType === 'revision') + .map(change => change.entityId), + ...changes + .filter(change => change.entityType === 'attachment') + .map(change => change.newValues?.revisionId) + .filter((id): id is number => typeof id === 'number' && id > 0) + ])] if (revisionIds.length === 0) { return { diff --git a/package.json b/package.json index 14cd0127..52949bfa 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", + "dev": "export NODE_OPTIONS=--openssl-legacy-provider && next dev --turbopack", + "build": "export NODE_OPTIONS=--openssl-legacy-provider && next build", + "start": "export NODE_OPTIONS=--openssl-legacy-provider && next start", "lint": "next lint", "db:seed_2": "tsx db/seeds_2/seed.ts" }, |
