/** * 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 { 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}건`; }