summaryrefslogtreecommitdiff
path: root/lib/compliance/approval-actions.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/compliance/approval-actions.ts')
-rw-r--r--lib/compliance/approval-actions.ts179
1 files changed, 179 insertions, 0 deletions
diff --git a/lib/compliance/approval-actions.ts b/lib/compliance/approval-actions.ts
new file mode 100644
index 00000000..3cded178
--- /dev/null
+++ b/lib/compliance/approval-actions.ts
@@ -0,0 +1,179 @@
+/**
+ * RED FLAG 해소요청 결재 서버 액션
+ *
+ * ✅ 베스트 프랙티스:
+ * - 'use server' 지시어 포함 (서버 액션)
+ * - UI에서 호출하는 진입점 함수들
+ * - ApprovalSubmissionSaga를 사용하여 결재 프로세스 시작
+ * - 템플릿 변수 준비 및 입력 검증
+ * - 핸들러(Internal)에는 최소 데이터만 전달
+ */
+
+'use server';
+
+import { ApprovalSubmissionSaga } from '@/lib/approval';
+import type { ApprovalResult } from '@/lib/approval/types';
+import { getServerSession } from 'next-auth';
+import { authOptions } from '@/app/api/auth/[...nextauth]/route';
+import { mapRedFlagResolutionToTemplateVariables, getPurchasingManagerEpId } from './approval-handlers';
+import { fetchContractsWithFlags, validateRedFlagResolutionRequest } from './red-flag-resolution';
+import db from '@/db/db';
+import { inArray } from 'drizzle-orm';
+import { complianceResponses } from '@/db/schema/compliance';
+import { revalidatePath } from 'next/cache';
+import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils';
+
+/**
+ * 결재를 거쳐 RED FLAG 해소요청을 상신하는 서버 액션
+ *
+ * ✅ 사용법 (클라이언트 컴포넌트에서):
+ * ```typescript
+ * const result = await requestRedFlagResolutionWithApproval({
+ * contractIds: [1, 2, 3],
+ * });
+ *
+ * if (result.status === 'pending_approval') {
+ * toast.success(`결재가 상신되었습니다. (ID: ${result.approvalId})`);
+ * }
+ * ```
+ */
+export async function requestRedFlagResolutionWithApproval(data: {
+ contractIds: number[];
+}): Promise<ApprovalResult> {
+ debugLog('[RedFlagResolutionApproval] RED FLAG 해소요청 결재 서버 액션 시작', {
+ contractCount: data.contractIds.length,
+ });
+
+ // 1. 입력 검증
+ if (!data.contractIds || data.contractIds.length === 0) {
+ debugError('[RedFlagResolutionApproval] 계약서 ID 없음');
+ throw new Error('RED FLAG 해소요청을 위한 계약서를 선택해주세요.');
+ }
+
+ const uniqueContractIds = Array.from(new Set(data.contractIds));
+
+ // 2. 세션 및 사용자 정보 확인
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ debugError('[RedFlagResolutionApproval] 인증되지 않은 사용자');
+ throw new Error('인증이 필요합니다.');
+ }
+
+ const currentUser = session.user;
+ if (!currentUser.epId) {
+ debugError('[RedFlagResolutionApproval] Knox EP ID 없음');
+ throw new Error('Knox EP ID가 필요합니다.');
+ }
+
+ const currentUserId = Number(currentUser.id);
+ if (Number.isNaN(currentUserId)) {
+ debugError('[RedFlagResolutionApproval] 유효하지 않은 사용자 ID');
+ throw new Error('유효한 사용자 정보가 필요합니다.');
+ }
+
+ // 3. 구매기획 담당자 EP ID 조회
+ const purchasingManagerEpId = await getPurchasingManagerEpId();
+ if (!purchasingManagerEpId || purchasingManagerEpId.trim() === '') {
+ debugError('[RedFlagResolutionApproval] 구매기획 담당자 EP ID 없음');
+ throw new Error('구매기획 담당자의 EP ID가 설정되지 않았습니다. 준법서약 관리 페이지에서 레드플래그 담당자를 설정해주세요.');
+ }
+
+ const trimmedEpId = purchasingManagerEpId.trim();
+ debugLog('[RedFlagResolutionApproval] 구매기획 담당자 EP ID', { epId: trimmedEpId });
+
+ // 4. 계약서 및 RED FLAG 확인
+ const contractSummaries = await fetchContractsWithFlags(uniqueContractIds);
+ if (contractSummaries.length === 0) {
+ debugError('[RedFlagResolutionApproval] RED FLAG가 있는 계약서 없음');
+ throw new Error('선택한 계약서에 RED FLAG가 존재하지 않습니다.');
+ }
+
+ const validContractIds = contractSummaries.map((contract) => contract.contractId);
+ debugLog('[RedFlagResolutionApproval] 처리할 계약서', { count: validContractIds.length, ids: validContractIds });
+
+ // 5. 중복 해소요청 방지 검증
+ await validateRedFlagResolutionRequest(validContractIds, contractSummaries);
+
+ // 6. 템플릿 변수 매핑
+ debugLog('[RedFlagResolutionApproval] 템플릿 변수 매핑 시작');
+ const requestedAt = new Date();
+ const variables = await mapRedFlagResolutionToTemplateVariables(contractSummaries, {
+ requesterName: currentUser.name || currentUser.email || '요청자',
+ requestedAt,
+ });
+ debugLog('[RedFlagResolutionApproval] 템플릿 변수 매핑 완료', {
+ variableKeys: Object.keys(variables),
+ });
+
+ // 7. 결재 제목 생성
+ const title = buildApprovalTitle(contractSummaries);
+
+ // 8. 결재 워크플로우 시작 (Saga 패턴)
+ debugLog('[RedFlagResolutionApproval] ApprovalSubmissionSaga 생성');
+ const saga = new ApprovalSubmissionSaga(
+ // actionType: 핸들러를 찾을 때 사용할 키
+ 'compliance_red_flag_resolution',
+
+ // actionPayload: 결재 승인 후 핸들러에 전달될 데이터 (최소 데이터만)
+ {
+ contractIds: validContractIds,
+ requestedBy: currentUserId,
+ requestedAt: requestedAt.toISOString(),
+ },
+
+ // approvalConfig: 결재 상신 정보 (템플릿 포함)
+ {
+ title,
+ description: '컴플라이언스 Red Flag 해소요청',
+ templateName: '컴플라이언스 Red Flag 해소요청',
+ variables,
+ approvers: [trimmedEpId],
+ currentUser: {
+ id: currentUserId,
+ epId: currentUser.epId,
+ email: currentUser.email ?? undefined,
+ },
+ }
+ );
+
+ debugLog('[RedFlagResolutionApproval] Saga 실행 시작');
+ const result = await saga.execute();
+
+ debugSuccess('[RedFlagResolutionApproval] 결재 워크플로우 완료', {
+ approvalId: result.approvalId,
+ pendingActionId: result.pendingActionId,
+ status: result.status,
+ });
+
+ // 9. 결재 상신 성공 시 compliance_responses 업데이트
+ if (result.status === 'pending_approval') {
+ await db
+ .update(complianceResponses)
+ .set({
+ redFlagResolutionApprovalId: result.approvalId,
+ redFlagResolvedAt: null,
+ updatedAt: new Date(),
+ })
+ .where(inArray(complianceResponses.basicContractId, validContractIds));
+
+ await revalidatePath('/evcp/basic-contract');
+ await revalidatePath('/evcp/compliance');
+ }
+
+ return result;
+}
+
+/**
+ * 결재 제목 생성
+ */
+function buildApprovalTitle(contracts: Array<{ contractId: number; vendorName: string | null }>): string {
+ if (contracts.length === 0) return '컴플라이언스 Red Flag 해소요청';
+ const firstVendor = contracts[0].vendorName ?? `계약 ${contracts[0].contractId}`;
+
+ if (contracts.length === 1) {
+ return `Red Flag 해소요청 - ${firstVendor}`;
+ }
+
+ return `Red Flag 해소요청 - ${firstVendor} 외 ${contracts.length - 1}건`;
+}
+