/** * 입찰초대 관련 결재 액션 핸들러 * * ✅ 베스트 프랙티스: * - 'use server' 지시어 없음 (순수 비즈니스 로직만) * - 결재 승인 후 실행될 최소한의 데이터만 처리 * - DB 조작 및 실제 비즈니스 로직만 포함 */ import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils'; /** * 입찰초대 핸들러 (결재 승인 후 실행됨) * * ✅ Internal 함수: 결재 워크플로우에서 자동 호출됨 (직접 호출 금지) * * @param payload - withApproval()에서 전달한 actionPayload (최소 데이터만) */ export async function requestBiddingInvitationInternal(payload: { biddingId: number; vendors: Array<{ vendorId: number; vendorName: string; vendorCode?: string | null; vendorCountry?: string; vendorEmail?: string | null; contactPerson?: string | null; contactEmail?: string | null; ndaYn?: boolean; generalGtcYn?: boolean; projectGtcYn?: boolean; agreementYn?: boolean; biddingCompanyId: number; biddingId: number; }>; message?: string; currentUserId: number; // ✅ 결재 상신한 사용자 ID }) { debugLog('[BiddingInvitationHandler] 입찰초대 핸들러 시작', { biddingId: payload.biddingId, vendorCount: payload.vendors.length, currentUserId: payload.currentUserId, }); // ✅ userId 검증: 핸들러에서 userId가 없으면 잘못된 상황 (예외 처리) if (!payload.currentUserId || payload.currentUserId <= 0) { const errorMessage = 'currentUserId가 없습니다. actionPayload에 currentUserId가 포함되지 않았습니다.'; debugError('[BiddingInvitationHandler]', errorMessage); throw new Error(errorMessage); } try { // 1. 기본계약 발송 const { sendBiddingBasicContracts } = await import('@/lib/bidding/pre-quote/service'); const vendorDataForContract = payload.vendors.map(vendor => ({ vendorId: vendor.vendorId, vendorName: vendor.vendorName, vendorCode: vendor.vendorCode || undefined, vendorCountry: vendor.vendorCountry, selectedMainEmail: vendor.vendorEmail || '', additionalEmails: [], customEmails: [], contractRequirements: { ndaYn: vendor.ndaYn || false, generalGtcYn: vendor.generalGtcYn || false, projectGtcYn: vendor.projectGtcYn || false, agreementYn: vendor.agreementYn || false, }, biddingCompanyId: vendor.biddingCompanyId, biddingId: vendor.biddingId, hasExistingContracts: false, // 결재 후처리에서는 기존 계약 확인 생략 })); const contractResult = await sendBiddingBasicContracts( payload.biddingId, vendorDataForContract, [], // generatedPdfs - 결재 템플릿이므로 PDF는 빈 배열 payload.message ); if (!contractResult.success) { debugError('[BiddingInvitationHandler] 기본계약 발송 실패', contractResult.error); throw new Error(contractResult.error || '기본계약 발송에 실패했습니다.'); } debugLog('[BiddingInvitationHandler] 기본계약 발송 완료'); // 2. 입찰 등록 진행 (상태를 bidding_opened로 변경) const { registerBidding } = await import('@/lib/bidding/detail/service'); const registerResult = await registerBidding(payload.biddingId, payload.currentUserId.toString()); if (!registerResult.success) { debugError('[BiddingInvitationHandler] 입찰 등록 실패', registerResult.error); throw new Error(registerResult.error || '입찰 등록에 실패했습니다.'); } debugSuccess('[BiddingInvitationHandler] 입찰초대 완료', { biddingId: payload.biddingId, vendorCount: payload.vendors.length, message: registerResult.message, }); return { success: true, biddingId: payload.biddingId, vendorCount: payload.vendors.length, message: `기본계약 발송 및 본입찰 초대가 완료되었습니다.`, }; } catch (error) { debugError('[BiddingInvitationHandler] 입찰초대 중 에러', error); throw error; } } /** * 입찰초대 데이터를 결재 템플릿 변수로 매핑 * * @param payload - 입찰초대 데이터 * @returns 템플릿 변수 객체 (Record) */ export async function mapBiddingInvitationToTemplateVariables(payload: { bidding: { id: number; title: string; biddingNumber: string; projectName?: string; itemName?: string; biddingType: string; bidPicName?: string; supplyPicName?: string; submissionStartDate?: Date; submissionEndDate?: Date; hasSpecificationMeeting?: boolean; isUrgent?: boolean; remarks?: string; targetPrice?: number; }; biddingItems: Array<{ id: number; projectName?: string; materialGroup?: string; materialGroupName?: string; materialCode?: string; materialCodeName?: string; quantity?: number; purchasingUnit?: string; targetUnitPrice?: number; quantityUnit?: string; totalWeight?: number; weightUnit?: string; budget?: number; targetAmount?: number; currency?: string; }>; vendors: Array<{ vendorId: number; vendorName: string; vendorCode?: string | null; vendorCountry?: string; vendorEmail?: string | null; contactPerson?: string | null; contactEmail?: string | null; }>; message?: string; requestedAt: Date; }): Promise> { const { bidding, biddingItems, vendors, message, requestedAt } = payload; // 제목 const title = bidding.title || '입찰'; // 입찰명 const biddingTitle = bidding.title || ''; // 입찰번호 const biddingNumber = bidding.biddingNumber || ''; // 낙찰업체수 const winnerCount = '1'; // 기본값, 실제로는 bidding 설정에서 가져와야 함 // 계약구분 const contractType = bidding.biddingType || ''; // P/R번호 - bidding 테이블에 없으므로 빈 값 const prNumber = ''; // 예산 const budget = bidding.targetPrice ? bidding.targetPrice.toLocaleString() : ''; // 내정가 const targetPrice = bidding.targetPrice ? bidding.targetPrice.toLocaleString() : ''; // 입찰요청 시스템 const requestSystem = 'eVCP'; // 입찰담당자 const biddingManager = bidding.bidPicName || bidding.supplyPicName || ''; // 내정가 산정 기준 - bidding 테이블에 없으므로 빈 값 const targetPriceBasis = ''; // 입찰 개요 const biddingOverview = bidding.itemName || message || ''; // 입찰 공고문 const biddingNotice = message || ''; // 입찰담당자 (중복이지만 템플릿에 맞춤) const biddingManagerDup = bidding.bidPicName || bidding.supplyPicName || ''; // 협력사 정보들 const vendorVariables: Record = {}; vendors.forEach((vendor, index) => { const num = index + 1; vendorVariables[`협력사_코드_${num}`] = vendor.vendorCode || ''; vendorVariables[`협력사명_${num}`] = vendor.vendorName || ''; vendorVariables[`담당자_${num}`] = vendor.contactPerson || ''; vendorVariables[`이메일_${num}`] = vendor.contactEmail || vendor.vendorEmail || ''; vendorVariables[`전화번호_${num}`] = ''; // 연락처 정보가 없으므로 빈 값 }); // 사양설명회 정보 const hasSpecMeeting = bidding.hasSpecificationMeeting ? '예' : '아니오'; const specMeetingStart = bidding.submissionStartDate ? bidding.submissionStartDate.toLocaleString('ko-KR') : ''; const specMeetingEnd = bidding.submissionEndDate ? bidding.submissionEndDate.toLocaleString('ko-KR') : ''; const specMeetingStartDup = specMeetingStart; const specMeetingEndDup = specMeetingEnd; // 입찰서제출기간 정보 const submissionPeriodExecution = '예'; // 입찰 기간이 있으므로 예 const submissionPeriodStart = bidding.submissionStartDate ? bidding.submissionStartDate.toLocaleString('ko-KR') : ''; const submissionPeriodEnd = bidding.submissionEndDate ? bidding.submissionEndDate.toLocaleString('ko-KR') : ''; // 대상 자재 수 const targetMaterialCount = biddingItems.length.toString(); // 자재 정보들 const materialVariables: Record = {}; biddingItems.forEach((item, index) => { const num = index + 1; materialVariables[`프로젝트_${num}`] = item.projectName || ''; materialVariables[`자재그룹_${num}`] = item.materialGroup || ''; materialVariables[`자재그룹명_${num}`] = item.materialGroupName || ''; materialVariables[`자재코드_${num}`] = item.materialCode || ''; materialVariables[`자재코드명_${num}`] = item.materialCodeName || ''; materialVariables[`수량_${num}`] = item.quantity ? item.quantity.toLocaleString() : ''; materialVariables[`구매단위_${num}`] = item.purchasingUnit || ''; materialVariables[`내정단가_${num}`] = item.targetUnitPrice ? item.targetUnitPrice.toLocaleString() : ''; materialVariables[`수량단위_${num}`] = item.quantityUnit || ''; materialVariables[`총중량_${num}`] = item.totalWeight ? item.totalWeight.toLocaleString() : ''; materialVariables[`중량단위_${num}`] = item.weightUnit || ''; materialVariables[`예산_${num}`] = item.budget ? item.budget.toLocaleString() : ''; materialVariables[`내정금액_${num}`] = item.targetAmount ? item.targetAmount.toLocaleString() : ''; materialVariables[`통화_${num}`] = item.currency || ''; }); return { 제목: title, 입찰명: biddingTitle, 입찰번호: biddingNumber, 낙찰업체수: winnerCount, 계약구분: contractType, 'P/R번호': prNumber, 예산: budget, 내정가: targetPrice, 입찰요청_시스템: requestSystem, 입찰담당자: biddingManager, 내정가_산정_기준: targetPriceBasis, 입찰개요: biddingOverview, 입찰공고문: biddingNotice, ...vendorVariables, 사양설명회_실행여부: hasSpecMeeting, 사양설명회_시작예정일시: specMeetingStart, 사양설명회_종료예정일시: specMeetingEnd, 입찰서제출기간_실행여부: submissionPeriodExecution, 입찰서제출기간_시작예정일시: submissionPeriodStart, 입찰서제출기간_종료예정일시: submissionPeriodEnd, 대상_자재_수: targetMaterialCount, ...materialVariables, }; }