summaryrefslogtreecommitdiff
path: root/lib/compliance/red-flag-resolution.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/compliance/red-flag-resolution.ts')
-rw-r--r--lib/compliance/red-flag-resolution.ts247
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}`)
+ }
}