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/approval-actions.ts | |
| parent | 1d1f6010704a1d655b3007887db0fe3ac866177a (diff) | |
(최겸) 구매 입찰 수정, 입찰초대 결재 등록, 재입찰, 차수증가, 폐찰, 유찰취소 로직 수정, readonly 추가 등
Diffstat (limited to 'lib/bidding/approval-actions.ts')
| -rw-r--r-- | lib/bidding/approval-actions.ts | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/lib/bidding/approval-actions.ts b/lib/bidding/approval-actions.ts new file mode 100644 index 00000000..3a82b08f --- /dev/null +++ b/lib/bidding/approval-actions.ts @@ -0,0 +1,243 @@ +/** + * 입찰초대 관련 결재 서버 액션 + * + * ✅ 베스트 프랙티스: + * - '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; +} |
