summaryrefslogtreecommitdiff
path: root/lib/pos/sync-rfq-pos-files.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pos/sync-rfq-pos-files.ts')
-rw-r--r--lib/pos/sync-rfq-pos-files.ts407
1 files changed, 407 insertions, 0 deletions
diff --git a/lib/pos/sync-rfq-pos-files.ts b/lib/pos/sync-rfq-pos-files.ts
new file mode 100644
index 00000000..acb34f20
--- /dev/null
+++ b/lib/pos/sync-rfq-pos-files.ts
@@ -0,0 +1,407 @@
+'use server';
+
+import db from '@/db/db';
+import { rfqPrItems, rfqLastAttachments, rfqLastAttachmentRevisions } from '@/db/schema/rfqLast';
+import { eq, and, ne } from 'drizzle-orm';
+import { getDcmtmIdByMaterialCode } from './get-dcmtm-id';
+import { getEncryptDocumentumFile } from './get-pos';
+import { downloadPosFile } from './download-pos-file';
+import type { PosFileSyncResult } from './types';
+import path from 'path';
+import fs from 'fs/promises';
+import { revalidatePath } from 'next/cache';
+import { debugLog, debugError, debugSuccess, debugProcess, debugWarn } from '@/lib/debug-utils';
+
+
+/**
+ * RFQ의 모든 PR Items의 MATNR로 POS 파일을 조회하고 서버에 다운로드하여 저장
+ */
+export async function syncRfqPosFiles(
+ rfqId: number,
+ userId: number
+): Promise<PosFileSyncResult> {
+ debugLog(`🚀 POS 파일 동기화 시작`, { rfqId, userId });
+
+ const result: PosFileSyncResult = {
+ success: false,
+ processedCount: 0,
+ successCount: 0,
+ failedCount: 0,
+ errors: [],
+ details: []
+ };
+
+ try {
+ // 1. RFQ의 모든 PR Items 조회 (materialCode 중복 제거)
+ debugProcess(`📋 RFQ PR Items 조회 시작 (RFQ ID: ${rfqId})`);
+
+ const prItems = await db
+ .selectDistinct({
+ materialCode: rfqPrItems.materialCode,
+ materialDescription: rfqPrItems.materialDescription,
+ })
+ .from(rfqPrItems)
+ .where(and(
+ eq(rfqPrItems.rfqsLastId, rfqId),
+ // materialCode가 null이 아닌 것만
+ ))
+ .then(items => items.filter(item => item.materialCode && item.materialCode.trim() !== ''));
+
+ debugLog(`📦 조회된 PR Items`, {
+ totalCount: prItems.length,
+ materialCodes: prItems.map(item => item.materialCode)
+ });
+
+ if (prItems.length === 0) {
+ debugWarn(`⚠️ 처리할 자재코드가 없습니다 (RFQ ID: ${rfqId})`);
+ result.errors.push('처리할 자재코드가 없습니다.');
+ return result;
+ }
+
+ result.processedCount = prItems.length;
+ debugSuccess(`✅ 처리할 자재코드 ${prItems.length}개 발견`);
+
+ // 2. 각 자재코드별로 POS 파일 처리
+ debugProcess(`🔄 자재코드별 POS 파일 처리 시작`);
+
+ for (const prItem of prItems) {
+ const materialCode = prItem.materialCode!;
+ debugLog(`📋 자재코드 처리 시작: ${materialCode}`);
+
+ try {
+ // 2-1. 자재코드로 DCMTM_ID 조회
+ debugProcess(`🔍 DCMTM_ID 조회 시작 (자재코드: ${materialCode})`);
+ const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode });
+
+ debugLog(`🎯 DCMTM_ID 조회 결과`, {
+ materialCode,
+ success: dcmtmResult.success,
+ fileCount: dcmtmResult.files?.length || 0,
+ files: dcmtmResult.files
+ });
+
+ if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) {
+ debugWarn(`⚠️ DCMTM_ID 조회 실패 또는 파일 없음 (자재코드: ${materialCode})`, dcmtmResult.error);
+ result.details.push({
+ materialCode,
+ status: 'no_files',
+ error: dcmtmResult.error || 'POS 파일을 찾을 수 없음'
+ });
+ continue;
+ }
+
+ // 여러 파일이 있을 경우 첫 번째 파일만 처리
+ const posFile = dcmtmResult.files[0];
+ debugLog(`📁 처리할 POS 파일 선택`, {
+ materialCode,
+ selectedFile: posFile,
+ totalFiles: dcmtmResult.files.length
+ });
+
+ // 2-2. POS API로 파일 경로 가져오기
+ debugProcess(`🌐 POS API 호출 시작 (DCMTM_ID: ${posFile.dcmtmId})`);
+ const posResult = await getEncryptDocumentumFile({
+ objectID: posFile.dcmtmId
+ });
+
+ debugLog(`🌐 POS API 호출 결과`, {
+ materialCode,
+ dcmtmId: posFile.dcmtmId,
+ success: posResult.success,
+ resultPath: posResult.result,
+ error: posResult.error
+ });
+
+ if (!posResult.success || !posResult.result) {
+ debugError(`❌ POS API 호출 실패 (자재코드: ${materialCode})`, posResult.error);
+ result.details.push({
+ materialCode,
+ fileName: posFile.fileName,
+ status: 'failed',
+ error: posResult.error || 'POS 파일 경로 조회 실패'
+ });
+ result.failedCount++;
+ continue;
+ }
+
+ // 2-3. 내부망에서 파일 다운로드
+ debugProcess(`⬇️ 파일 다운로드 시작 (경로: ${posResult.result})`);
+ const downloadResult = await downloadPosFile({
+ relativePath: posResult.result
+ });
+
+ debugLog(`⬇️ 파일 다운로드 결과`, {
+ materialCode,
+ success: downloadResult.success,
+ fileName: downloadResult.fileName,
+ fileSize: downloadResult.fileBuffer?.length,
+ mimeType: downloadResult.mimeType,
+ error: downloadResult.error
+ });
+
+ if (!downloadResult.success || !downloadResult.fileBuffer) {
+ debugError(`❌ 파일 다운로드 실패 (자재코드: ${materialCode})`, downloadResult.error);
+ result.details.push({
+ materialCode,
+ fileName: posFile.fileName,
+ status: 'failed',
+ error: downloadResult.error || '파일 다운로드 실패'
+ });
+ result.failedCount++;
+ continue;
+ }
+
+ // 2-4. 서버 파일 시스템에 저장
+ debugProcess(`💾 서버 파일 저장 시작 (파일명: ${downloadResult.fileName || `${materialCode}.pdf`})`);
+ const saveResult = await saveFileToServer(
+ downloadResult.fileBuffer,
+ downloadResult.fileName || `${materialCode}.pdf`
+ );
+
+ debugLog(`💾 서버 파일 저장 결과`, {
+ materialCode,
+ success: saveResult.success,
+ filePath: saveResult.filePath,
+ fileName: saveResult.fileName,
+ error: saveResult.error
+ });
+
+ if (!saveResult.success) {
+ debugError(`❌ 서버 파일 저장 실패 (자재코드: ${materialCode})`, saveResult.error);
+ result.details.push({
+ materialCode,
+ fileName: posFile.fileName,
+ status: 'failed',
+ error: saveResult.error || '서버 파일 저장 실패'
+ });
+ result.failedCount++;
+ continue;
+ }
+
+ // 2-5. 데이터베이스에 첨부파일 정보 저장
+ debugProcess(`🗄️ DB 첨부파일 정보 저장 시작 (자재코드: ${materialCode})`);
+ const dbResult = await saveAttachmentToDatabase(
+ rfqId,
+ materialCode,
+ { dcmtmId: posFile.dcmtmId, fileName: posFile.fileName },
+ saveResult.filePath!,
+ saveResult.fileName!,
+ downloadResult.fileBuffer.length,
+ downloadResult.mimeType || 'application/pdf',
+ userId
+ );
+
+ debugLog(`🗄️ DB 저장 결과`, {
+ materialCode,
+ success: dbResult.success,
+ error: dbResult.error
+ });
+
+ if (!dbResult.success) {
+ debugError(`❌ DB 저장 실패 (자재코드: ${materialCode})`, dbResult.error);
+ result.details.push({
+ materialCode,
+ fileName: posFile.fileName,
+ status: 'failed',
+ error: dbResult.error || 'DB 저장 실패'
+ });
+ result.failedCount++;
+ continue;
+ }
+
+ debugSuccess(`✅ 자재코드 ${materialCode} 처리 완료`);
+ result.details.push({
+ materialCode,
+ fileName: posFile.fileName,
+ status: 'success'
+ });
+ result.successCount++;
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
+ debugError(`❌ 자재코드 ${materialCode} 처리 중 예외 발생`, { error: errorMessage, stack: error instanceof Error ? error.stack : undefined });
+ result.details.push({
+ materialCode,
+ status: 'failed',
+ error: errorMessage
+ });
+ result.failedCount++;
+ result.errors.push(`${materialCode}: ${errorMessage}`);
+ }
+ }
+
+ result.success = result.successCount > 0;
+
+ debugLog(`📊 POS 파일 동기화 최종 결과`, {
+ rfqId,
+ processedCount: result.processedCount,
+ successCount: result.successCount,
+ failedCount: result.failedCount,
+ success: result.success,
+ errors: result.errors
+ });
+
+ // 캐시 무효화
+ debugProcess(`🔄 캐시 무효화 (경로: /evcp/rfq-last/${rfqId})`);
+ revalidatePath(`/evcp/rfq-last/${rfqId}`);
+
+ debugSuccess(`🎉 POS 파일 동기화 완료 (성공: ${result.successCount}건, 실패: ${result.failedCount}건)`);
+ return result;
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
+ debugError(`❌ POS 파일 동기화 전체 처리 오류`, { error: errorMessage, stack: error instanceof Error ? error.stack : undefined });
+ result.errors.push(`전체 처리 오류: ${errorMessage}`);
+ return result;
+ }
+}
+
+/**
+ * 파일을 서버 파일 시스템에 저장
+ */
+async function saveFileToServer(
+ fileBuffer: Buffer,
+ originalFileName: string
+): Promise<{
+ success: boolean;
+ filePath?: string;
+ fileName?: string;
+ error?: string;
+}> {
+ try {
+ // uploads/pos 디렉토리 생성
+ const uploadDir = path.join(process.cwd(), 'uploads', 'pos');
+
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ // 고유한 파일명 생성 (타임스탬프 + 원본명)
+ const timestamp = Date.now();
+ const sanitizedFileName = originalFileName.replace(/[^a-zA-Z0-9.-]/g, '_');
+ const fileName = `${timestamp}_${sanitizedFileName}`;
+ const filePath = path.join(uploadDir, fileName);
+
+ // 파일 저장
+ await fs.writeFile(filePath, fileBuffer);
+
+ return {
+ success: true,
+ filePath: `uploads/pos/${fileName}`, // 상대 경로로 저장
+ fileName: originalFileName
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '파일 저장 실패'
+ };
+ }
+}
+
+/**
+ * 첨부파일 정보를 데이터베이스에 저장
+ */
+async function saveAttachmentToDatabase(
+ rfqId: number,
+ materialCode: string,
+ posFileInfo: { dcmtmId: string; fileName: string },
+ filePath: string,
+ originalFileName: string,
+ fileSize: number,
+ fileType: string,
+ userId: number
+): Promise<{
+ success: boolean;
+ error?: string;
+}> {
+ try {
+ await db.transaction(async (tx) => {
+ // 1. 기존 동일한 자재코드의 설계 첨부파일이 있는지 확인
+ const existingAttachment = await tx
+ .select()
+ .from(rfqLastAttachments)
+ .where(and(
+ eq(rfqLastAttachments.rfqId, rfqId),
+ eq(rfqLastAttachments.attachmentType, '설계'),
+ eq(rfqLastAttachments.serialNo, materialCode)
+ ))
+ .limit(1);
+
+ let attachmentId: number;
+
+ if (existingAttachment.length > 0) {
+ // 기존 첨부파일이 있으면 업데이트
+ attachmentId = existingAttachment[0].id;
+
+ await tx
+ .update(rfqLastAttachments)
+ .set({
+ currentRevision: 'Rev.1', // 새 리비전으로 업데이트
+ description: `${posFileInfo.fileName} (자재코드: ${materialCode})`,
+ updatedAt: new Date()
+ })
+ .where(eq(rfqLastAttachments.id, attachmentId));
+ } else {
+ // 새 첨부파일 생성
+ const [newAttachment] = await tx
+ .insert(rfqLastAttachments)
+ .values({
+ attachmentType: '설계',
+ serialNo: materialCode,
+ rfqId,
+ currentRevision: 'Rev.0',
+ description: `${posFileInfo.fileName} (자재코드: ${materialCode})`,
+ createdBy: userId,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ })
+ .returning({ id: rfqLastAttachments.id });
+
+ attachmentId = newAttachment.id;
+ }
+
+ // 2. 새 리비전 생성
+ const [newRevision] = await tx
+ .insert(rfqLastAttachmentRevisions)
+ .values({
+ attachmentId,
+ revisionNo: 'Rev.0',
+ fileName: path.basename(filePath),
+ originalFileName,
+ filePath,
+ fileSize,
+ fileType,
+ isLatest: true,
+ revisionComment: `POS 시스템에서 자동 동기화됨 (DCMTM_ID: ${posFileInfo.dcmtmId})`,
+ createdBy: userId
+ })
+ .returning({ id: rfqLastAttachmentRevisions.id });
+
+ // 3. 첨부파일의 latestRevisionId 업데이트
+ await tx
+ .update(rfqLastAttachments)
+ .set({
+ latestRevisionId: newRevision.id
+ })
+ .where(eq(rfqLastAttachments.id, attachmentId));
+
+ // 4. 기존 리비전들의 isLatest를 false로 업데이트
+ await tx
+ .update(rfqLastAttachmentRevisions)
+ .set({ isLatest: false })
+ .where(and(
+ eq(rfqLastAttachmentRevisions.attachmentId, attachmentId),
+ ne(rfqLastAttachmentRevisions.id, newRevision.id)
+ ));
+ });
+
+ return { success: true };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'DB 저장 실패'
+ };
+ }
+}