diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/rfq-last/table/rfq-attachments-dialog.tsx | 88 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/bidding-and-pr-mapper.ts | 178 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/rfq-and-pr-mapper.ts | 35 |
3 files changed, 254 insertions, 47 deletions
diff --git a/lib/rfq-last/table/rfq-attachments-dialog.tsx b/lib/rfq-last/table/rfq-attachments-dialog.tsx index 4513b0b0..161e446a 100644 --- a/lib/rfq-last/table/rfq-attachments-dialog.tsx +++ b/lib/rfq-last/table/rfq-attachments-dialog.tsx @@ -26,8 +26,8 @@ import { toast } from "sonner" import { RfqsLastView } from "@/db/schema" import { getRfqAttachmentsAction } from "../service" import { downloadFile, quickPreview, smartFileAction, formatFileSize, getFileInfo } from "@/lib/file-download" -import { syncRfqPosFiles } from "@/lib/pos" -import { useSession } from "next-auth/react" +// import { syncRfqPosFiles } from "@/lib/pos" // 주석 처리: ECC 매핑 시 자동 처리로 변경 +// import { useSession } from "next-auth/react" // 주석 처리: 동기화 UI 제거로 불필요 // 첨부파일 타입 interface RfqAttachment { @@ -57,9 +57,9 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment const [attachments, setAttachments] = React.useState<RfqAttachment[]>([]) const [isLoading, setIsLoading] = React.useState(false) const [downloadingFiles, setDownloadingFiles] = React.useState<Set<number>>(new Set()) - const [isSyncing, setIsSyncing] = React.useState(false) + // const [isSyncing, setIsSyncing] = React.useState(false) // 주석 처리: 동기화 UI 제거 - const { data: session } = useSession() + // const { data: session } = useSession() // 주석 처리: 동기화 UI 제거로 불필요 // 첨부파일 목록 로드 React.useEffect(() => { @@ -158,48 +158,48 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment } } - // POS 파일 동기화 핸들러 - const handlePosSync = async () => { - if (!session?.user?.id || !rfqData.id) { - toast.error("로그인이 필요하거나 RFQ 정보가 없습니다") - return - } + // POS 파일 동기화 핸들러 - 주석 처리: ECC 매핑 시 자동 처리로 변경 + // const handlePosSync = async () => { + // if (!session?.user?.id || !rfqData.id) { + // toast.error("로그인이 필요하거나 RFQ 정보가 없습니다") + // return + // } - setIsSyncing(true) + // setIsSyncing(true) - try { - const result = await syncRfqPosFiles(rfqData.id, parseInt(session.user.id)) + // try { + // const result = await syncRfqPosFiles(rfqData.id, parseInt(session.user.id)) - if (result.success) { - toast.success( - `POS 파일 동기화 완료: 성공 ${result.successCount}건, 실패 ${result.failedCount}건` - ) + // if (result.success) { + // toast.success( + // `POS 파일 동기화 완료: 성공 ${result.successCount}건, 실패 ${result.failedCount}건` + // ) - // 성공한 경우 첨부파일 목록 새로고침 - if (result.successCount > 0) { - const refreshResult = await getRfqAttachmentsAction(rfqData.id) - if (refreshResult.success) { - setAttachments(refreshResult.data) - } - } + // // 성공한 경우 첨부파일 목록 새로고침 + // if (result.successCount > 0) { + // const refreshResult = await getRfqAttachmentsAction(rfqData.id) + // if (refreshResult.success) { + // setAttachments(refreshResult.data) + // } + // } - // 상세 결과 표시 - if (result.details.length > 0) { - const failedItems = result.details.filter(d => d.status === 'failed') - if (failedItems.length > 0) { - console.warn("POS 동기화 실패 항목:", failedItems) - } - } - } else { - toast.error(`POS 파일 동기화 실패: ${result.errors.join(', ')}`) - } - } catch (error) { - console.error("POS 동기화 오류:", error) - toast.error("POS 파일 동기화 중 오류가 발생했습니다") - } finally { - setIsSyncing(false) - } - } + // // 상세 결과 표시 + // if (result.details.length > 0) { + // const failedItems = result.details.filter(d => d.status === 'failed') + // if (failedItems.length > 0) { + // console.warn("POS 동기화 실패 항목:", failedItems) + // } + // } + // } else { + // toast.error(`POS 파일 동기화 실패: ${result.errors.join(', ')}`) + // } + // } catch (error) { + // console.error("POS 동기화 오류:", error) + // toast.error("POS 파일 동기화 중 오류가 발생했습니다") + // } finally { + // setIsSyncing(false) + // } + // } // 첨부파일 타입별 색상 const getAttachmentTypeBadgeVariant = (type: string) => { @@ -224,8 +224,8 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment </DialogDescription> </div> - {/* POS 동기화 버튼 */} - <Button + {/* POS 동기화 버튼 - 주석 처리: ECC 매핑 시 자동 처리로 변경 */} + {/* <Button variant="outline" size="sm" onClick={handlePosSync} @@ -238,7 +238,7 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment <RefreshCw className="h-4 w-4" /> )} {isSyncing ? "동기화 중..." : "POS 파일 동기화"} - </Button> + </Button> */} </div> </DialogHeader> diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts index 4db8d451..99373555 100644 --- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts @@ -9,6 +9,7 @@ import db from '@/db/db'; import { biddings, prItemsForBidding, + prDocuments, } from '@/db/schema/bidding'; import { PR_INFORMATION_T_BID_HEADER, @@ -22,6 +23,11 @@ import { parseSAPDateTime, parseSAPDateToString, } from './common-mapper-utils'; +import { + getDcmtmIdByMaterialCode, + getEncryptDocumentumFile, + downloadPosFile +} from '@/lib/pos'; // ECC 데이터 타입 정의 export type ECCBidHeader = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert; @@ -31,6 +37,117 @@ export type ECCBidItem = typeof PR_INFORMATION_T_BID_ITEM.$inferInsert; export type BiddingData = typeof biddings.$inferInsert; export type PrItemForBiddingData = typeof prItemsForBidding.$inferInsert; +/** + * Bidding용 POS 파일 동기화 함수 + * 자재코드 기준으로 POS 파일을 찾아서 prDocuments 테이블에 저장 + */ +async function syncBiddingPosFiles( + biddingId: number, + materialCodes: string[], + userId: string = '1' +): Promise<{ + success: boolean; + successCount: number; + failedCount: number; + errors: string[]; +}> { + debugLog('Bidding POS 파일 동기화 시작', { biddingId, materialCodes }); + + let successCount = 0; + let failedCount = 0; + const errors: string[] = []; + + // 중복 제거된 자재코드로 처리 + const uniqueMaterialCodes = [...new Set(materialCodes.filter(code => code && code.trim() !== ''))]; + + for (const materialCode of uniqueMaterialCodes) { + try { + debugLog(`자재코드 ${materialCode} POS 파일 조회 시작`); + + // 1. 자재코드로 DCMTM_ID 조회 + const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode }); + + if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) { + debugLog(`자재코드 ${materialCode}: POS 파일 없음`); + continue; // 에러로 카운트하지 않고 스킵 + } + + // 첫 번째 파일만 처리 + const posFile = dcmtmResult.files[0]; + + // 2. POS API로 파일 경로 가져오기 + const posResult = await getEncryptDocumentumFile({ + objectID: posFile.dcmtmId + }); + + if (!posResult.success || !posResult.result) { + errors.push(`${materialCode}: POS 파일 경로 조회 실패`); + failedCount++; + continue; + } + + // 3. 파일 다운로드 + const downloadResult = await downloadPosFile({ + relativePath: posResult.result + }); + + if (!downloadResult.success || !downloadResult.fileBuffer) { + errors.push(`${materialCode}: 파일 다운로드 실패`); + failedCount++; + continue; + } + + // 4. 서버에 파일 저장 (uploads/bidding-pos 디렉토리) + const path = await import('path'); + const fs = await import('fs/promises'); + + const uploadDir = path.join(process.cwd(), 'uploads', 'bidding-pos'); + try { + await fs.access(uploadDir); + } catch { + await fs.mkdir(uploadDir, { recursive: true }); + } + + const timestamp = Date.now(); + const sanitizedFileName = (downloadResult.fileName || `${materialCode}.pdf`).replace(/[^a-zA-Z0-9.-]/g, '_'); + const fileName = `${timestamp}_${sanitizedFileName}`; + const filePath = path.join(uploadDir, fileName); + + await fs.writeFile(filePath, downloadResult.fileBuffer); + + // 5. prDocuments 테이블에 저장 + await db.insert(prDocuments).values({ + biddingId, + documentName: `${materialCode} 설계문서`, + fileName, + originalFileName: posFile.fileName, + fileSize: downloadResult.fileBuffer.length, + mimeType: downloadResult.mimeType || 'application/pdf', + filePath: `uploads/bidding-pos/${fileName}`, + registeredBy: userId, + description: `POS 시스템에서 자동 동기화됨 (DCMTM_ID: ${posFile.dcmtmId}, 자재코드: ${materialCode})`, + version: 'Rev.0' + }); + + successCount++; + debugSuccess(`자재코드 ${materialCode} POS 파일 동기화 완료`); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류'; + errors.push(`${materialCode}: ${errorMessage}`); + failedCount++; + debugError(`자재코드 ${materialCode} POS 파일 동기화 실패`, error); + } + } + + return { + success: successCount > 0, + successCount, + failedCount, + errors + }; +} + /** * Bidding 코드 생성 함수 (배치 처리용) @@ -333,7 +450,12 @@ export async function mapAndSaveECCBiddingData( const inserted = await tx .insert(biddings) .values(biddingRecords) - .returning({ id: biddings.id, biddingNumber: biddings.biddingNumber }); + .returning({ + id: biddings.id, + biddingNumber: biddings.biddingNumber, + ANFNR: biddings.ANFNR, + createdBy: biddings.createdBy + }); const biddingNumberToId = new Map<string, number>(); for (const row of inserted) { @@ -368,13 +490,65 @@ export async function mapAndSaveECCBiddingData( await tx.insert(prItemsForBidding).values(chunk); } - return { processedCount: biddingRecords.length }; + return { + processedCount: biddingRecords.length, + insertedBiddings: inserted as Array<{ id: number; biddingNumber: string; ANFNR: string | null; createdBy: string | null }>, + allEccItems: eccItems // POS 동기화를 위해 필요 + }; }); debugSuccess('ECC Bidding 데이터 일괄 처리 완료', { processedCount: result.processedCount, }); + // 7) 각 Bidding에 대해 POS 파일 자동 동기화 (비동기로 실행하여 메인 플로우 블록하지 않음) + debugLog('Bidding POS 파일 자동 동기화 시작', { biddingCount: result.insertedBiddings.length }); + + // 비동기로 각 Bidding의 POS 파일 동기화 실행 (결과를 기다리지 않음) + result.insertedBiddings.forEach(async (bidding) => { + try { + // 해당 Bidding과 관련된 모든 자재코드 추출 + const relatedMaterialCodes = result.allEccItems + .filter(item => item.ANFNR === bidding.ANFNR) + .map(item => item.MATNR) + .filter(Boolean) as string[]; + + if (relatedMaterialCodes.length === 0) { + debugLog(`Bidding ${bidding.biddingNumber}: 자재코드 없음`); + return; + } + + debugLog(`Bidding ${bidding.biddingNumber} POS 파일 동기화 시작`, { + biddingId: bidding.id, + materialCodes: relatedMaterialCodes + }); + + const syncResult = await syncBiddingPosFiles( + bidding.id, + relatedMaterialCodes, + bidding.createdBy || '1' + ); + + if (syncResult.success) { + debugSuccess(`Bidding ${bidding.biddingNumber} POS 파일 동기화 완료`, { + biddingId: bidding.id, + successCount: syncResult.successCount, + failedCount: syncResult.failedCount + }); + } else { + debugError(`Bidding ${bidding.biddingNumber} POS 파일 동기화 실패`, { + biddingId: bidding.id, + errors: syncResult.errors + }); + } + } catch (error) { + debugError(`Bidding ${bidding.biddingNumber} POS 파일 동기화 중 예외 발생`, { + biddingId: bidding.id, + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } + }); + return { success: true, message: `${result.processedCount}개의 Bidding 데이터가 성공적으로 처리되었습니다.`, diff --git a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts index 8748e244..a517d84c 100644 --- a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts @@ -21,6 +21,7 @@ import { findMaterialNameByMATNR, parseSAPDateTime, } from './common-mapper-utils'; +import { syncRfqPosFiles } from '@/lib/pos'; // ECC 데이터 타입 정의 export type ECCBidHeader = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert; @@ -346,13 +347,45 @@ export async function mapAndSaveECCRfqDataToRfqLast( await tx.insert(rfqPrItems).values(chunk); } - return { processedCount: rfqRecords.length }; + return { + processedCount: rfqRecords.length, + insertedRfqs: inserted // POS 동기화를 위해 inserted 데이터 반환 + }; }); debugSuccess('ECC 데이터 일괄 처리 완료 (rfqsLast)', { processedCount: result.processedCount, }); + // 6) 각 RFQ에 대해 POS 파일 자동 동기화 (비동기로 실행하여 메인 플로우 블록하지 않음) + debugLog('RFQ POS 파일 자동 동기화 시작', { rfqCount: result.insertedRfqs.length }); + + // 비동기로 각 RFQ의 POS 파일 동기화 실행 (결과를 기다리지 않음) + result.insertedRfqs.forEach(async (rfq) => { + try { + debugLog(`RFQ ${rfq.rfqCode} POS 파일 동기화 시작`, { rfqId: rfq.id }); + const syncResult = await syncRfqPosFiles(rfq.id, 1); // 시스템 사용자 ID = 1 + + if (syncResult.success) { + debugSuccess(`RFQ ${rfq.rfqCode} POS 파일 동기화 완료`, { + rfqId: rfq.id, + successCount: syncResult.successCount, + failedCount: syncResult.failedCount + }); + } else { + debugError(`RFQ ${rfq.rfqCode} POS 파일 동기화 실패`, { + rfqId: rfq.id, + errors: syncResult.errors + }); + } + } catch (error) { + debugError(`RFQ ${rfq.rfqCode} POS 파일 동기화 중 예외 발생`, { + rfqId: rfq.id, + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } + }); + return { success: true, message: `${result.processedCount}개의 RFQ 데이터가 성공적으로 처리되었습니다.`, |
