/** * 입찰초대 관련 결재 서버 액션 * * ✅ 베스트 프랙티스: * - 'use server' 지시어 포함 (서버 액션) * - UI에서 호출하는 진입점 함수들 * - withApproval()을 사용하여 결재 프로세스 시작 * - 템플릿 변수 준비 및 입력 검증 * - 핸들러(Internal)에는 최소 데이터만 전달 */ 'use server'; import { ApprovalSubmissionSaga } from '@/lib/approval'; import { mapBiddingInvitationToTemplateVariables } from './handlers'; import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils'; /** * 입찰초대 결재를 거쳐 입찰등록을 처리하는 서버 액션 * * ✅ 사용법 (클라이언트 컴포넌트에서): * ```typescript * const result = await requestBiddingInvitationWithApproval({ * biddingId: 123, * vendors: [...], * message: "입찰 초대 메시지", * currentUser: { id: 1, epId: 'EP001', email: 'user@example.com' }, * approvers: ['EP002', 'EP003'] * }); * * if (result.status === 'pending_approval') { * toast.success(`입찰초대 결재가 상신되었습니다. (ID: ${result.approvalId})`); * } * ``` */ /** * 입찰초대 결재를 위한 공통 데이터 준비 헬퍼 함수 */ export async function prepareBiddingApprovalData(data: { 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; }) { // 1. 입찰 정보 조회 (템플릿 변수용) debugLog('[BiddingInvitationApproval] 입찰 정보 조회 시작'); const { default: db } = await import('@/db/db'); const { biddings, prItemsForBidding } = await import('@/db/schema'); const { eq } = await import('drizzle-orm'); const biddingInfo = await db .select({ id: biddings.id, title: biddings.title, biddingNumber: biddings.biddingNumber, projectName: biddings.projectName, itemName: biddings.itemName, biddingType: biddings.biddingType, bidPicName: biddings.bidPicName, supplyPicName: biddings.supplyPicName, submissionStartDate: biddings.submissionStartDate, submissionEndDate: biddings.submissionEndDate, hasSpecificationMeeting: biddings.hasSpecificationMeeting, isUrgent: biddings.isUrgent, remarks: biddings.remarks, targetPrice: biddings.targetPrice, }) .from(biddings) .where(eq(biddings.id, data.biddingId)) .limit(1); if (biddingInfo.length === 0) { debugError('[BiddingInvitationApproval] 입찰 정보를 찾을 수 없음'); throw new Error('입찰 정보를 찾을 수 없습니다'); } const bidding = biddingInfo[0]; // 입찰 대상 자재 정보 조회 const biddingItemsInfo = await db .select({ id: prItemsForBidding.id, projectName: prItemsForBidding.projectInfo, materialGroup: prItemsForBidding.materialGroupNumber, materialGroupName: prItemsForBidding.materialGroupInfo, materialCode: prItemsForBidding.materialNumber, materialCodeName: prItemsForBidding.materialInfo, quantity: prItemsForBidding.quantity, purchasingUnit: prItemsForBidding.purchaseUnit, targetUnitPrice: prItemsForBidding.targetUnitPrice, quantityUnit: prItemsForBidding.quantityUnit, totalWeight: prItemsForBidding.totalWeight, weightUnit: prItemsForBidding.weightUnit, budget: prItemsForBidding.budgetAmount, targetAmount: prItemsForBidding.targetAmount, currency: prItemsForBidding.targetCurrency, }) .from(prItemsForBidding) .where(eq(prItemsForBidding.biddingId, data.biddingId)); debugLog('[BiddingInvitationApproval] 입찰 정보 조회 완료', { biddingId: bidding.id, title: bidding.title, itemCount: biddingItemsInfo.length, }); // 2. 템플릿 변수 매핑 debugLog('[BiddingInvitationApproval] 템플릿 변수 매핑 시작'); const requestedAt = new Date(); const { mapBiddingInvitationToTemplateVariables } = await import('./handlers'); const variables = await mapBiddingInvitationToTemplateVariables({ bidding, biddingItems: biddingItemsInfo, vendors: data.vendors, message: data.message, requestedAt, }); debugLog('[BiddingInvitationApproval] 템플릿 변수 매핑 완료', { variableKeys: Object.keys(variables), }); return { bidding, biddingItems: biddingItemsInfo, variables, }; } export async function requestBiddingInvitationWithApproval(data: { 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; currentUser: { id: number; epId: string | null; email?: string }; approvers?: string[]; // Knox EP ID 배열 (결재선) }) { debugLog('[BiddingInvitationApproval] 입찰초대 결재 서버 액션 시작', { biddingId: data.biddingId, vendorCount: data.vendors.length, userId: data.currentUser.id, hasEpId: !!data.currentUser.epId, }); // 1. 입력 검증 if (!data.currentUser.epId) { debugError('[BiddingInvitationApproval] Knox EP ID 없음'); throw new Error('Knox EP ID가 필요합니다'); } if (data.vendors.length === 0) { debugError('[BiddingInvitationApproval] 선정된 업체 없음'); throw new Error('입찰 초대할 업체를 선택해주세요'); } // 2. 입찰 상태를 결재 진행중으로 변경 debugLog('[BiddingInvitationApproval] 입찰 상태 변경 시작'); const { default: db } = await import('@/db/db'); const { biddings, biddingCompanies, prItemsForBidding } = await import('@/db/schema'); const { eq } = await import('drizzle-orm'); await db .update(biddings) .set({ status: 'approval_pending', // 결재 진행중 상태 updatedBy: data.currentUser.epId, updatedAt: new Date() }) .where(eq(biddings.id, data.biddingId)); debugLog('[BiddingInvitationApproval] 입찰 상태 변경 완료', { biddingId: data.biddingId, newStatus: 'approval_pending' }); // 3. 결재 데이터 준비 const { bidding, biddingItems: biddingItemsInfo, variables } = await prepareBiddingApprovalData({ biddingId: data.biddingId, vendors: data.vendors, message: data.message, }); // 4. 결재 워크플로우 시작 (Saga 패턴) debugLog('[BiddingInvitationApproval] ApprovalSubmissionSaga 생성'); const saga = new ApprovalSubmissionSaga( // actionType: 핸들러를 찾을 때 사용할 키 'bidding_invitation', // actionPayload: 결재 승인 후 핸들러에 전달될 데이터 (최소 데이터만) { biddingId: data.biddingId, vendors: data.vendors, message: data.message, currentUserId: data.currentUser.id, // ✅ 결재 승인 후 핸들러 실행 시 필요 }, // approvalConfig: 결재 상신 정보 (템플릿 포함) { title: `입찰초대 - ${bidding.title}`, description: `${bidding.title} 입찰 초대 결재`, templateName: '입찰초대 결재', // 한국어 템플릿명 variables, // 치환할 변수들 approvers: data.approvers, currentUser: data.currentUser, } ); debugLog('[BiddingInvitationApproval] Saga 실행 시작'); const result = await saga.execute(); debugSuccess('[BiddingInvitationApproval] 입찰초대 결재 워크플로우 완료', { approvalId: result.approvalId, pendingActionId: result.pendingActionId, status: result.status, }); return result; }