From b845ccde2910894911233cda273657d2b52e63f9 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 21 Nov 2025 06:04:56 +0000 Subject: (임수민) 준법 Red Flag 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/compliance/red-flag-resolution.ts | 303 ++++++++++++++++++++++++++++++++++ lib/compliance/services.ts | 2 + 2 files changed, 305 insertions(+) create mode 100644 lib/compliance/red-flag-resolution.ts (limited to 'lib/compliance') diff --git a/lib/compliance/red-flag-resolution.ts b/lib/compliance/red-flag-resolution.ts new file mode 100644 index 00000000..184630f6 --- /dev/null +++ b/lib/compliance/red-flag-resolution.ts @@ -0,0 +1,303 @@ +"use server" + +import db from "@/db/db" +import { eq, and } from "drizzle-orm" +import { complianceResponses, redFlagManagers } from "@/db/schema/compliance" +import { users } from "@/db/schema" +import { basicContract } from "@/db/schema/basicContractDocumnet" +import { vendors } from "@/db/schema/vendors" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { + submitApproval, + createSubmitApprovalRequest, + createApprovalLine, + type ApprovalLine +} from "@/lib/knox-api/approval/approval" +import { getTriggeredRedFlagQuestions } from "./red-flag-notifier" +import { revalidatePath } from "next/cache" + +/** + * RED FLAG 해소요청 - 구매기획 담당자에게 합의 요청 + */ +export async function requestRedFlagResolution(contractIds: number[]): Promise<{ + success: boolean + message: string + failed: number[] +}> { + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + return { + success: false, + message: "인증이 필요합니다.", + failed: contractIds + } + } + + const currentUser = session.user + const userId = currentUser.id + const epId = currentUser.epId || "" + const emailAddress = currentUser.email || "" + + if (!epId || !emailAddress) { + return { + success: false, + message: "사용자 정보가 불완전합니다. epId와 email이 필요합니다.", + failed: contractIds + } + } + + // 구매기획 담당자 조회 + const managerRow = await db + .select({ + purchasingManagerId: redFlagManagers.purchasingManagerId, + }) + .from(redFlagManagers) + .orderBy(redFlagManagers.createdAt) + .limit(1) + + const purchasingManagerId = managerRow[0]?.purchasingManagerId + if (!purchasingManagerId) { + return { + success: false, + message: "구매기획 담당자가 설정되지 않았습니다.", + failed: contractIds + } + } + + // 구매기획 담당자 정보 조회 + const purchasingManager = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + epId: users.epId, + }) + .from(users) + .where(eq(users.id, purchasingManagerId)) + .limit(1) + + if (!purchasingManager[0] || !purchasingManager[0].epId || !purchasingManager[0].email) { + return { + success: false, + message: "구매기획 담당자 정보를 찾을 수 없습니다.", + failed: contractIds + } + } + + const pm = purchasingManager[0] + + // 각 계약서에 대해 RED FLAG 해소요청 처리 + const failed: number[] = [] + + for (const contractId of contractIds) { + try { + // 계약서 정보 조회 + const contractInfo = await db + .select({ + id: basicContract.id, + vendorId: basicContract.vendorId, + templateId: basicContract.templateId, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + }) + .from(basicContract) + .leftJoin(vendors, eq(basicContract.vendorId, vendors.id)) + .where(eq(basicContract.id, contractId)) + .limit(1) + + if (!contractInfo[0]) { + failed.push(contractId) + continue + } + + const contract = contractInfo[0] + + // RED FLAG 발생 여부 확인 + const triggeredFlags = await getTriggeredRedFlagQuestions(contractId) + if (triggeredFlags.length === 0) { + // RED FLAG가 없는 경우는 스킵 + continue + } + + // 이미 해소요청이 진행 중인지 확인 + const existingResponse = await db + .select() + .from(complianceResponses) + .where(eq(complianceResponses.basicContractId, contractId)) + .limit(1) + + if (existingResponse[0]?.redFlagResolutionApprovalId) { + // 이미 해소요청이 진행 중 + continue + } + + // 합의 요청 본문 생성 + const triggeredQuestionsText = triggeredFlags + .map((flag, idx) => `${idx + 1}. ${flag.questionText}`) + .join("\n") + + const contents = ` +RED FLAG 해소요청 + +계약서 ID: ${contractId} +업체명: ${contract.vendorName || "정보 없음"} +업체코드: ${contract.vendorCode || "정보 없음"} + +발생한 RED FLAG 질문: +${triggeredQuestionsText} + +위 RED FLAG에 대한 해소를 요청드립니다. +합의해 주시면 RED FLAG가 해제됩니다. + `.trim() + + const subject = `[RED FLAG 해소요청] ${contract.vendorName || "협력업체"} - 계약서 ID: ${contractId}` + + // 결재 경로 생성 + // 기안자: 현재 사용자 + const drafterLine: ApprovalLine = await createApprovalLine( + { epId, emailAddress }, + "0", // 기안 + "1" + ) + + // 합의자: 구매기획 담당자 + const approverLine: ApprovalLine = await createApprovalLine( + { epId: pm.epId, emailAddress: pm.email }, + "2", // 합의 + "2" + ) + + const approvalLines = [drafterLine, approverLine] + + // 결재 상신 요청 생성 + const approvalRequest = await createSubmitApprovalRequest( + contents, + subject, + approvalLines, + { + contentsType: "TEXT", + docSecuType: "PERSONAL", + notifyOption: "0", + urgYn: "N", + importantYn: "N", + } + ) + + // 결재 상신 + const approvalResponse = await submitApproval( + approvalRequest, + { + userId, + epId, + emailAddress, + } + ) + + if (approvalResponse.result === "success") { + // compliance_responses 업데이트 (red_flag_resolution_approval_id 저장) + if (existingResponse[0]) { + await db + .update(complianceResponses) + .set({ + redFlagResolutionApprovalId: approvalResponse.data.apInfId, + updatedAt: new Date(), + }) + .where(eq(complianceResponses.id, existingResponse[0].id)) + } else { + // compliance_response가 없는 경우 생성 (템플릿 ID는 계약서에서 가져와야 함) + // 이 경우는 실제로는 발생하지 않을 수 있지만, 안전을 위해 처리 + console.warn(`Compliance response not found for contract ${contractId}`) + } + } else { + failed.push(contractId) + } + } catch (error) { + console.error(`Error processing contract ${contractId}:`, error) + failed.push(contractId) + } + } + + revalidatePath("/evcp/basic-contract") + + if (failed.length === 0) { + return { + success: true, + message: `${contractIds.length}건의 RED FLAG 해소요청이 완료되었습니다.`, + failed: [] + } + } else if (failed.length < contractIds.length) { + return { + success: true, + message: `${contractIds.length - failed.length}건 성공, ${failed.length}건 실패`, + failed + } + } else { + return { + success: false, + message: "모든 RED FLAG 해소요청이 실패했습니다.", + failed + } + } + } catch (error) { + console.error("RED FLAG 해소요청 오류:", error) + return { + success: false, + message: `RED FLAG 해소요청 중 오류가 발생했습니다: ${error instanceof Error ? error.message : "알 수 없는 오류"}`, + failed: contractIds + } + } +} + +/** + * RED FLAG 해소 처리 (합의 완료 시 호출) + */ +export async function resolveRedFlag(contractId: number, approvalId: string): Promise<{ + success: boolean + message: string +}> { + try { + // compliance_responses 조회 + const response = await db + .select() + .from(complianceResponses) + .where( + and( + eq(complianceResponses.basicContractId, contractId), + eq(complianceResponses.redFlagResolutionApprovalId, approvalId) + ) + ) + .limit(1) + + if (!response[0]) { + return { + success: false, + message: "해소요청 정보를 찾을 수 없습니다.", + } + } + + // RED FLAG 해제 처리 + await db + .update(complianceResponses) + .set({ + redFlagResolvedAt: new Date(), + updatedAt: new Date(), + }) + .where(eq(complianceResponses.id, response[0].id)) + + revalidatePath("/evcp/basic-contract") + + return { + success: true, + message: "RED FLAG가 해제되었습니다.", + } + } catch (error) { + console.error("RED FLAG 해제 오류:", error) + return { + success: false, + message: `RED FLAG 해제 중 오류가 발생했습니다: ${error instanceof Error ? error.message : "알 수 없는 오류"}`, + } + } +} + diff --git a/lib/compliance/services.ts b/lib/compliance/services.ts index 6541bdd1..2856cb0a 100644 --- a/lib/compliance/services.ts +++ b/lib/compliance/services.ts @@ -1123,6 +1123,8 @@ export async function getComplianceResponseByBasicContractId(basicContractId: nu basicContractId: complianceResponses.basicContractId, status: complianceResponses.status, completedAt: complianceResponses.completedAt, + redFlagResolvedAt: complianceResponses.redFlagResolvedAt, + redFlagResolutionApprovalId: complianceResponses.redFlagResolutionApprovalId, createdAt: complianceResponses.createdAt, updatedAt: complianceResponses.updatedAt, }) -- cgit v1.2.3