diff options
Diffstat (limited to 'lib/compliance/red-flag-resolution.ts')
| -rw-r--r-- | lib/compliance/red-flag-resolution.ts | 247 |
1 files changed, 46 insertions, 201 deletions
diff --git a/lib/compliance/red-flag-resolution.ts b/lib/compliance/red-flag-resolution.ts index 423f5a46..63057523 100644 --- a/lib/compliance/red-flag-resolution.ts +++ b/lib/compliance/red-flag-resolution.ts @@ -2,19 +2,15 @@ import db from "@/db/db" import { and, eq, inArray } from "drizzle-orm" -import { complianceResponses, redFlagManagers } from "@/db/schema/compliance" +import { complianceResponses } from "@/db/schema/compliance" import { basicContract, basicContractTemplates } from "@/db/schema/basicContractDocumnet" import { vendors } from "@/db/schema/vendors" -import { users } from "@/db/schema" import { getTriggeredRedFlagQuestions, type TriggeredRedFlagInfo } from "./red-flag-notifier" -import { getServerSession } from "next-auth" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { ApprovalSubmissionSaga } from "@/lib/approval" -import { htmlListConverter, htmlTableConverter } from "@/lib/approval/template-utils" -import type { ApprovalResult } from "@/lib/approval/types" import { revalidatePath } from "next/cache" +import { requestRedFlagResolutionWithApproval } from "./approval-actions" +import type { ApprovalResult } from "@/lib/approval/types" -type ContractSummary = { +export type ContractSummary = { contractId: number vendorName: string | null vendorCode: string | null @@ -25,122 +21,12 @@ type ContractSummary = { /** * RED FLAG 해소요청 - Approval Saga를 통해 상신 + * + * @deprecated 이 함수는 호환성을 위해 유지됩니다. + * 새로운 코드는 `requestRedFlagResolutionWithApproval`을 사용하세요. */ export async function requestRedFlagResolution(contractIds: number[]): Promise<ApprovalResult> { - if (!contractIds || contractIds.length === 0) { - throw new Error("RED FLAG 해소요청을 위한 계약서를 선택해주세요.") - } - - const uniqueContractIds = Array.from(new Set(contractIds)) - - const session = await getServerSession(authOptions) - if (!session?.user) { - throw new Error("인증이 필요합니다.") - } - - const currentUser = session.user - if (!currentUser.epId) { - throw new Error("Knox EP ID가 필요합니다.") - } - - const currentUserId = Number(currentUser.id) - if (Number.isNaN(currentUserId)) { - throw new Error("유효한 사용자 정보가 필요합니다.") - } - - const purchasingManagerEpId = await getPurchasingManagerEpId() - if (!purchasingManagerEpId) { - throw new Error("구매기획 담당자의 EP ID가 설정되지 않았습니다.") - } - - const contractSummaries = await fetchContractsWithFlags(uniqueContractIds) - if (contractSummaries.length === 0) { - throw new Error("선택한 계약서에 RED FLAG가 존재하지 않습니다.") - } - - const validContractIds = contractSummaries.map((contract) => contract.contractId) - - // 중복 해소요청 방지 (진행 중인 결재가 있는지 확인) - const responses = await db - .select({ - basicContractId: complianceResponses.basicContractId, - redFlagResolvedAt: complianceResponses.redFlagResolvedAt, - redFlagResolutionApprovalId: complianceResponses.redFlagResolutionApprovalId, - }) - .from(complianceResponses) - .where(inArray(complianceResponses.basicContractId, validContractIds)) - - const missingResponses = validContractIds.filter( - (contractId) => !responses.some((response) => response.basicContractId === contractId) - ) - - if (missingResponses.length > 0) { - throw new Error("준법 응답 정보를 찾을 수 없는 계약서가 포함되어 있습니다.") - } - - const blockedContracts = responses - .filter((response) => response.redFlagResolutionApprovalId && !response.redFlagResolvedAt) - .map((response) => response.basicContractId) - - if (blockedContracts.length > 0) { - const blockedSummaries = contractSummaries - .filter((contract) => blockedContracts.includes(contract.contractId)) - .map((contract) => contract.vendorName ?? `계약 ${contract.contractId}`) - - const preview = - blockedSummaries.length > 2 - ? `${blockedSummaries.slice(0, 2).join(", ")} 외 ${blockedSummaries.length - 2}건` - : blockedSummaries.join(", ") - - throw new Error(`이미 해소요청이 진행 중인 계약서가 있습니다: ${preview}`) - } - - const now = new Date() - const variables = await buildTemplateVariables(contractSummaries, { - requesterName: currentUser.name || currentUser.email || "요청자", - requestedAt: now, - }) - - const title = buildApprovalTitle(contractSummaries) - - const saga = new ApprovalSubmissionSaga( - "compliance_red_flag_resolution", - { - contractIds: validContractIds, - requestedBy: currentUserId, - requestedAt: now.toISOString(), - }, - { - title, - description: "컴플라이언스 Red Flag 해소요청", - templateName: "컴플라이언스 Red Flag 해소요청", - variables, - approvers: [purchasingManagerEpId], - currentUser: { - id: currentUserId, - epId: currentUser.epId, - email: currentUser.email ?? undefined, - }, - } - ) - - const result = await saga.execute() - - 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 + return await requestRedFlagResolutionWithApproval({ contractIds }) } /** @@ -179,31 +65,10 @@ export async function resolveRedFlag( } } -async function getPurchasingManagerEpId(): Promise<string | null> { - const [manager] = await db - .select({ - purchasingManagerId: redFlagManagers.purchasingManagerId, - }) - .from(redFlagManagers) - .orderBy(redFlagManagers.createdAt) - .limit(1) - - if (!manager?.purchasingManagerId) { - return null - } - - const [user] = await db - .select({ - epId: users.epId, - }) - .from(users) - .where(eq(users.id, manager.purchasingManagerId)) - .limit(1) - - return user?.epId ?? null -} - -async function fetchContractsWithFlags(contractIds: number[]): Promise<ContractSummary[]> { +/** + * 계약서와 RED FLAG 정보를 함께 조회 + */ +export async function fetchContractsWithFlags(contractIds: number[]): Promise<ContractSummary[]> { const contracts = await db .select({ contractId: basicContract.id, @@ -230,65 +95,45 @@ async function fetchContractsWithFlags(contractIds: number[]): Promise<ContractS return withFlags.filter((contract) => contract.triggeredFlags.length > 0) } -async function buildTemplateVariables( - contracts: ContractSummary[], - meta: { requesterName: string; requestedAt: Date } -): Promise<Record<string, string>> { - const summaryRows = contracts.map((contract) => ({ - contractId: contract.contractId, - vendorName: contract.vendorName ?? "-", - templateName: contract.templateName ?? "-", - redFlagCount: contract.triggeredFlags.length, - })) - - const summaryTable = await htmlTableConverter(summaryRows, [ - { key: "contractId", label: "계약 ID" }, - { key: "vendorName", label: "업체명" }, - { key: "templateName", label: "템플릿" }, - { key: "redFlagCount", label: "RED FLAG 수" }, - ]) - - const detailSections = await Promise.all( - contracts.map(async (contract) => { - const questionList = contract.triggeredFlags.map((flag, index) => { - const prefix = flag.questionNumber || `${index + 1}` - return `${prefix}. ${flag.questionText}` - }) - - const listHtml = await htmlListConverter(questionList) - return ` - <div style="margin-bottom: 24px;"> - <div style="font-weight:600;margin-bottom:8px;"> - 계약 ID: ${contract.contractId} / ${contract.vendorName ?? "-"} - </div> - <div>${listHtml}</div> - </div> - ` +/** + * RED FLAG 해소요청 검증 (중복 요청 방지) + */ +export async function validateRedFlagResolutionRequest( + validContractIds: number[], + contractSummaries: ContractSummary[] +): Promise<void> { + // 중복 해소요청 방지 (진행 중인 결재가 있는지 확인) + const responses = await db + .select({ + basicContractId: complianceResponses.basicContractId, + redFlagResolvedAt: complianceResponses.redFlagResolvedAt, + redFlagResolutionApprovalId: complianceResponses.redFlagResolutionApprovalId, }) - ) + .from(complianceResponses) + .where(inArray(complianceResponses.basicContractId, validContractIds)) - const detailHtml = detailSections.join("") - const formattedDate = new Intl.DateTimeFormat("ko-KR", { - dateStyle: "medium", - timeStyle: "short", - }).format(meta.requestedAt) + const missingResponses = validContractIds.filter( + (contractId) => !responses.some((response) => response.basicContractId === contractId) + ) - return { - 요청자이름: meta.requesterName, - 요청일시: formattedDate, - 요청사유: "컴플라이언스 Red Flag 해소를 위해 구매기획 합의를 요청드립니다.", - RedFlag요약테이블: summaryTable, - RedFlag상세내역: detailHtml, + if (missingResponses.length > 0) { + throw new Error("준법 응답 정보를 찾을 수 없는 계약서가 포함되어 있습니다.") } -} -function buildApprovalTitle(contracts: ContractSummary[]) { - if (contracts.length === 0) return "컴플라이언스 Red Flag 해소요청" - const firstVendor = contracts[0].vendorName ?? `계약 ${contracts[0].contractId}` + const blockedContracts = responses + .filter((response) => response.redFlagResolutionApprovalId && !response.redFlagResolvedAt) + .map((response) => response.basicContractId) - if (contracts.length === 1) { - return `Red Flag 해소요청 - ${firstVendor}` - } + if (blockedContracts.length > 0) { + const blockedSummaries = contractSummaries + .filter((contract) => blockedContracts.includes(contract.contractId)) + .map((contract) => contract.vendorName ?? `계약 ${contract.contractId}`) - return `Red Flag 해소요청 - ${firstVendor} 외 ${contracts.length - 1}건` + const preview = + blockedSummaries.length > 2 + ? `${blockedSummaries.slice(0, 2).join(", ")} 외 ${blockedSummaries.length - 2}건` + : blockedSummaries.join(", ") + + throw new Error(`이미 해소요청이 진행 중인 계약서가 있습니다: ${preview}`) + } } |
