summaryrefslogtreecommitdiff
path: root/lib/bidding/approval-actions.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-24 20:16:56 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-24 20:16:56 +0900
commit6bc4162b19f06ad4f919270ebcd4ef18f31cd490 (patch)
treebe37a152174789d269ef718c2a1f3794531e1c37 /lib/bidding/approval-actions.ts
parent775997501ef36bf07d7f1f2e1d4abe7c97505e96 (diff)
parenta8674e6b91fb4d356c311fad0251878de154da53 (diff)
(김준회) 최겸프로 작업사항 병합
Diffstat (limited to 'lib/bidding/approval-actions.ts')
-rw-r--r--lib/bidding/approval-actions.ts325
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;
+}