diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-24 11:16:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-24 11:16:32 +0000 |
| commit | a8674e6b91fb4d356c311fad0251878de154da53 (patch) | |
| tree | 8bdf91ef99b2628f319df37912ccede1e2f5009c /lib/bidding/approval-actions.ts | |
| parent | 68160eba15a2c8408329b6e14b94d5e44fa7e3ab (diff) | |
(최겸) 구매 입찰 수정(폐찰, 낙찰 결재 기능 추가 등)
Diffstat (limited to 'lib/bidding/approval-actions.ts')
| -rw-r--r-- | lib/bidding/approval-actions.ts | 325 |
1 files changed, 323 insertions, 2 deletions
diff --git a/lib/bidding/approval-actions.ts b/lib/bidding/approval-actions.ts index 3a82b08f..6f02e80c 100644 --- a/lib/bidding/approval-actions.ts +++ b/lib/bidding/approval-actions.ts @@ -12,7 +12,7 @@ 'use server'; import { ApprovalSubmissionSaga } from '@/lib/approval'; -import { mapBiddingInvitationToTemplateVariables } from './handlers'; +import { mapBiddingInvitationToTemplateVariables, mapBiddingClosureToTemplateVariables, mapBiddingAwardToTemplateVariables } from './handlers'; import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils'; /** @@ -99,7 +99,7 @@ export async function prepareBiddingApprovalData(data: { materialCode: prItemsForBidding.materialNumber, materialCodeName: prItemsForBidding.materialInfo, quantity: prItemsForBidding.quantity, - purchasingUnit: prItemsForBidding.purchaseUnit, + purchasingUnit: prItemsForBidding.priceUnit, targetUnitPrice: prItemsForBidding.targetUnitPrice, quantityUnit: prItemsForBidding.quantityUnit, totalWeight: prItemsForBidding.totalWeight, @@ -241,3 +241,324 @@ export async function requestBiddingInvitationWithApproval(data: { return result; } + +/** + * 폐찰 결재를 거쳐 입찰 폐찰을 처리하는 서버 액션 + * + * ✅ 사용법 (클라이언트 컴포넌트에서): + * ```typescript + * const result = await requestBiddingClosureWithApproval({ + * biddingId: 123, + * description: "폐찰 사유", + * 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 prepareBiddingClosureApprovalData(data: { + biddingId: number; + description: string; +}) { + // 1. 입찰 정보 조회 (템플릿 변수용) + debugLog('[BiddingClosureApproval] 입찰 정보 조회 시작'); + const { default: db } = await import('@/db/db'); + const { biddings } = await import('@/db/schema'); + const { eq } = await import('drizzle-orm'); + + const biddingInfo = await db + .select({ + id: biddings.id, + title: biddings.title, + }) + .from(biddings) + .where(eq(biddings.id, data.biddingId)) + .limit(1); + + if (biddingInfo.length === 0) { + debugError('[BiddingClosureApproval] 입찰 정보를 찾을 수 없음'); + throw new Error('입찰 정보를 찾을 수 없습니다'); + } + + debugLog('[BiddingClosureApproval] 입찰 정보 조회 완료', { + biddingId: data.biddingId, + title: biddingInfo[0].title, + }); + + // 2. 템플릿 변수 매핑 + debugLog('[BiddingClosureApproval] 템플릿 변수 매핑 시작'); + const requestedAt = new Date(); + const { mapBiddingClosureToTemplateVariables } = await import('./handlers'); + const variables = await mapBiddingClosureToTemplateVariables({ + biddingId: data.biddingId, + description: data.description, + requestedAt, + }); + debugLog('[BiddingClosureApproval] 템플릿 변수 매핑 완료', { + variableKeys: Object.keys(variables), + }); + + return { + bidding: biddingInfo[0], + variables, + }; +} + +export async function requestBiddingClosureWithApproval(data: { + biddingId: number; + description: string; + files?: File[]; + currentUser: { id: number; epId: string | null; email?: string }; + approvers?: string[]; // Knox EP ID 배열 (결재선) +}) { + debugLog('[BiddingClosureApproval] 폐찰 결재 서버 액션 시작', { + biddingId: data.biddingId, + description: data.description, + userId: data.currentUser.id, + hasEpId: !!data.currentUser.epId, + }); + + // 1. 입력 검증 + if (!data.currentUser.epId) { + debugError('[BiddingClosureApproval] Knox EP ID 없음'); + throw new Error('Knox EP ID가 필요합니다'); + } + + if (!data.description.trim()) { + debugError('[BiddingClosureApproval] 폐찰 사유 없음'); + throw new Error('폐찰 사유를 입력해주세요'); + } + // 유찰상태인지 확인 + const { bidding } = await db + .select() + .from(biddings) + .where(eq(biddings.id, data.biddingId)) + .limit(1); + + if (bidding.status !== 'bidding_disposal') { + debugError('[BiddingClosureApproval] 유찰 상태가 아닙니다.'); + throw new Error('유찰 상태인 입찰만 폐찰할 수 있습니다.'); + } + + // 2. 입찰 상태를 결재 진행중으로 변경 + debugLog('[BiddingClosureApproval] 입찰 상태 변경 시작'); + const { default: db } = await import('@/db/db'); + const { biddings } = await import('@/db/schema'); + const { eq } = await import('drizzle-orm'); + + await db + .update(biddings) + .set({ + status: 'closure_pending', // 폐찰 결재 진행중 상태 + updatedBy: data.currentUser.epId, + updatedAt: new Date() + }) + .where(eq(biddings.id, data.biddingId)); + + debugLog('[BiddingClosureApproval] 입찰 상태 변경 완료', { + biddingId: data.biddingId, + newStatus: 'closure_pending' + }); + + // 3. 결재 데이터 준비 + const { bidding: approvalBidding, variables } = await prepareBiddingClosureApprovalData({ + biddingId: data.biddingId, + description: data.description, + }); + + // 4. 결재 워크플로우 시작 (Saga 패턴) + debugLog('[BiddingClosureApproval] ApprovalSubmissionSaga 생성'); + const saga = new ApprovalSubmissionSaga( + // actionType: 핸들러를 찾을 때 사용할 키 + 'bidding_closure', + + // actionPayload: 결재 승인 후 핸들러에 전달될 데이터 (최소 데이터만) + { + biddingId: data.biddingId, + description: data.description, + files: data.files, + currentUserId: data.currentUser.id, // ✅ 결재 승인 후 핸들러 실행 시 필요 + }, + + // approvalConfig: 결재 상신 정보 (템플릿 포함) + { + title: `폐찰 - ${approvalBidding.title}`, + description: `${approvalBidding.title} 입찰 폐찰 결재`, + templateName: '폐찰 품의 요청서', // 한국어 템플릿명 + variables, // 치환할 변수들 + approvers: data.approvers, + currentUser: data.currentUser, + } + ); + + debugLog('[BiddingClosureApproval] Saga 실행 시작'); + const result = await saga.execute(); + + debugSuccess('[BiddingClosureApproval] 폐찰 결재 워크플로우 완료', { + approvalId: result.approvalId, + pendingActionId: result.pendingActionId, + status: result.status, + }); + + return result; +} + +/** + * 낙찰 결재를 거쳐 입찰 낙찰을 처리하는 서버 액션 + * + * ✅ 사용법 (클라이언트 컴포넌트에서): + * ```typescript + * const result = await requestBiddingAwardWithApproval({ + * biddingId: 123, + * selectionReason: "낙찰 사유", + * 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 prepareBiddingAwardApprovalData(data: { + biddingId: number; + selectionReason: string; +}) { + // 1. 입찰 정보 조회 (템플릿 변수용) + debugLog('[BiddingAwardApproval] 입찰 정보 조회 시작'); + const { default: db } = await import('@/db/db'); + const { biddings } = await import('@/db/schema'); + const { eq } = await import('drizzle-orm'); + + const biddingInfo = await db + .select({ + id: biddings.id, + title: biddings.title, + }) + .from(biddings) + .where(eq(biddings.id, data.biddingId)) + .limit(1); + + if (biddingInfo.length === 0) { + debugError('[BiddingAwardApproval] 입찰 정보를 찾을 수 없음'); + throw new Error('입찰 정보를 찾을 수 없습니다'); + } + + debugLog('[BiddingAwardApproval] 입찰 정보 조회 완료', { + biddingId: data.biddingId, + title: biddingInfo[0].title, + }); + + // 2. 템플릿 변수 매핑 + debugLog('[BiddingAwardApproval] 템플릿 변수 매핑 시작'); + const requestedAt = new Date(); + const { mapBiddingAwardToTemplateVariables } = await import('./handlers'); + const variables = await mapBiddingAwardToTemplateVariables({ + biddingId: data.biddingId, + selectionReason: data.selectionReason, + requestedAt, + }); + debugLog('[BiddingAwardApproval] 템플릿 변수 매핑 완료', { + variableKeys: Object.keys(variables), + }); + + return { + bidding: biddingInfo[0], + variables, + }; +} + +export async function requestBiddingAwardWithApproval(data: { + biddingId: number; + selectionReason: string; + currentUser: { id: number; epId: string | null; email?: string }; + approvers?: string[]; // Knox EP ID 배열 (결재선) +}) { + debugLog('[BiddingAwardApproval] 낙찰 결재 서버 액션 시작', { + biddingId: data.biddingId, + selectionReason: data.selectionReason, + userId: data.currentUser.id, + hasEpId: !!data.currentUser.epId, + }); + + // 1. 입력 검증 + if (!data.currentUser.epId) { + debugError('[BiddingAwardApproval] Knox EP ID 없음'); + throw new Error('Knox EP ID가 필요합니다'); + } + + if (!data.selectionReason.trim()) { + debugError('[BiddingAwardApproval] 낙찰 사유 없음'); + throw new Error('낙찰 사유를 입력해주세요'); + } + + // 2. 입찰 상태를 결재 진행중으로 변경 + debugLog('[BiddingAwardApproval] 입찰 상태 변경 시작'); + const { default: db } = await import('@/db/db'); + const { biddings } = await import('@/db/schema'); + const { eq } = await import('drizzle-orm'); + + await db + .update(biddings) + .set({ + status: 'award_pending', // 낙찰 결재 진행중 상태 + updatedBy: data.currentUser.epId, + updatedAt: new Date() + }) + .where(eq(biddings.id, data.biddingId)); + + debugLog('[BiddingAwardApproval] 입찰 상태 변경 완료', { + biddingId: data.biddingId, + newStatus: 'award_pending' + }); + + // 3. 결재 데이터 준비 + const { bidding, variables } = await prepareBiddingAwardApprovalData({ + biddingId: data.biddingId, + selectionReason: data.selectionReason, + }); + + // 4. 결재 워크플로우 시작 (Saga 패턴) + debugLog('[BiddingAwardApproval] ApprovalSubmissionSaga 생성'); + const saga = new ApprovalSubmissionSaga( + // actionType: 핸들러를 찾을 때 사용할 키 + 'bidding_award', + + // actionPayload: 결재 승인 후 핸들러에 전달될 데이터 (최소 데이터만) + { + biddingId: data.biddingId, + selectionReason: data.selectionReason, + currentUserId: data.currentUser.id, // ✅ 결재 승인 후 핸들러 실행 시 필요 + }, + + // approvalConfig: 결재 상신 정보 (템플릿 포함) + { + title: `낙찰 - ${bidding.title}`, + description: `${bidding.title} 입찰 낙찰 결재`, + templateName: '입찰 결과 업체 선정 품의 요청서', // 한국어 템플릿명 + variables, // 치환할 변수들 + approvers: data.approvers, + currentUser: data.currentUser, + } + ); + + debugLog('[BiddingAwardApproval] Saga 실행 시작'); + const result = await saga.execute(); + + debugSuccess('[BiddingAwardApproval] 낙찰 결재 워크플로우 완료', { + approvalId: result.approvalId, + pendingActionId: result.pendingActionId, + status: result.status, + }); + + return result; +} |
