summaryrefslogtreecommitdiff
path: root/lib/bidding/handlers.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/handlers.ts')
-rw-r--r--lib/bidding/handlers.ts283
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,
+ };
+}