diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-18 10:30:31 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-18 10:30:31 +0000 |
| commit | c4f5472b961afb237dc819f9dd3f42a7b8f71075 (patch) | |
| tree | a1c0d00e46a005ff472bf1125e739bae73b0a53e /lib/bidding/handlers.ts | |
| parent | 1d1f6010704a1d655b3007887db0fe3ac866177a (diff) | |
(최겸) 구매 입찰 수정, 입찰초대 결재 등록, 재입찰, 차수증가, 폐찰, 유찰취소 로직 수정, readonly 추가 등
Diffstat (limited to 'lib/bidding/handlers.ts')
| -rw-r--r-- | lib/bidding/handlers.ts | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/lib/bidding/handlers.ts b/lib/bidding/handlers.ts new file mode 100644 index 00000000..fc2951d4 --- /dev/null +++ b/lib/bidding/handlers.ts @@ -0,0 +1,283 @@ +/** + * 입찰초대 관련 결재 액션 핸들러 + * + * ✅ 베스트 프랙티스: + * - '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<string, string>) + */ +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<Record<string, string>> { + 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<string, string> = {}; + 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<string, string> = {}; + 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, + }; +} |
