diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-24 06:03:15 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-24 06:03:15 +0000 |
| commit | a55198e00ff982f6310a08c76a0502c90dd7d859 (patch) | |
| tree | 94dd9816e18c63333e30b1ac8d14d3100f0e175e /lib | |
| parent | d79f56ae5a9e5f72781f78fe0399018cfac44081 (diff) | |
(임수민) 준법 Red Flag 결재 수정
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/basic-contract/viewer/basic-contract-sign-viewer.tsx | 4 | ||||
| -rw-r--r-- | lib/compliance/approval-actions.ts | 179 | ||||
| -rw-r--r-- | lib/compliance/approval-handlers.ts | 183 | ||||
| -rw-r--r-- | lib/compliance/red-flag-resolution.ts | 247 |
4 files changed, 371 insertions, 242 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx index 6bf0dfb1..8185e33e 100644 --- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -1411,7 +1411,7 @@ export function BasicContractSignViewer({ {file.name} </span> {isSurveyTab && !surveyData.completed && ( - <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-amber-500 text-white"> + <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-red-500 text-white transition-none hover:bg-red-500"> 필수 </Badge> )} @@ -1640,7 +1640,7 @@ export function BasicContractSignViewer({ {file.name} </span> {isSurveyTab && !surveyData.completed && ( - <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-amber-500 text-white"> + <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-red-500 text-white transition-none hover:bg-red-500"> 필수 </Badge> )} 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}건`; +} + diff --git a/lib/compliance/approval-handlers.ts b/lib/compliance/approval-handlers.ts index 05f92a28..11f95a3c 100644 --- a/lib/compliance/approval-handlers.ts +++ b/lib/compliance/approval-handlers.ts @@ -1,64 +1,169 @@ "use server" -import db from "@/db/db" -import { and, inArray, isNull } from "drizzle-orm" -import { complianceResponses } from "@/db/schema/compliance" -import { resolveRedFlag } from "./red-flag-resolution" +import { resolveRedFlag, type ContractSummary } from "./red-flag-resolution" import { revalidatePath } from "next/cache" - -interface RedFlagResolutionPayload { - contractIds: number[] -} +import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils" +import { htmlListConverter, htmlTableConverter } from "@/lib/approval/template-utils" +import db from "@/db/db" +import { eq } from "drizzle-orm" +import { redFlagManagers } from "@/db/schema/compliance" +import { users } from "@/db/schema" /** - * 결재 승인 후 RED FLAG 해제를 처리하는 핸들러 + * RED FLAG 해소 결재 승인 핸들러 * - * approval-workflow에서 자동으로 호출됩니다. + * 결재 승인 후 자동으로 호출되어 RED FLAG를 해제합니다. + * + * @param payload - 결재 상신 시 저장한 actionPayload */ -export async function resolveRedFlagAfterApproval(payload: RedFlagResolutionPayload) { - if (!payload?.contractIds || payload.contractIds.length === 0) { - return { - success: false, - message: "처리할 계약서가 없습니다.", +export async function resolveRedFlagAfterApproval(payload: { + contractIds: number[] + requestedBy: number + requestedAt: string +}) { + debugLog("[RedFlagResolutionHandler] RED FLAG 해소 결재 승인 핸들러 시작", payload) + + try { + if (!payload?.contractIds || payload.contractIds.length === 0) { + debugError("[RedFlagResolutionHandler] 계약서 ID가 없습니다", payload) + return { + success: false, + message: "처리할 계약서가 없습니다.", + } } - } - const uniqueContractIds = Array.from(new Set(payload.contractIds)) + const uniqueContractIds = Array.from(new Set(payload.contractIds)) + debugLog("[RedFlagResolutionHandler] 처리할 계약서 수", { count: uniqueContractIds.length }) - // 이미 해제된 계약을 제외한 대상을 조회 - const targets = await db - .select({ - basicContractId: complianceResponses.basicContractId, - approvalId: complianceResponses.redFlagResolutionApprovalId, - }) - .from(complianceResponses) - .where( - and( - inArray(complianceResponses.basicContractId, uniqueContractIds), - isNull(complianceResponses.redFlagResolvedAt) - ) + // 각 계약서에 대해 RED FLAG 해소 처리 + // approvalId는 resolveRedFlag 내부에서 조회하므로 여기서는 전달하지 않음 + const results = await Promise.allSettled( + uniqueContractIds.map(async (contractId) => { + const result = await resolveRedFlag(contractId, { + revalidate: false, + }) + return { contractId, result } + }) ) - if (targets.length === 0) { + const successful = results.filter((r) => r.status === "fulfilled").length + const failed = results.filter((r) => r.status === "rejected").length + + debugLog("[RedFlagResolutionHandler] 처리 결과", { + total: uniqueContractIds.length, + successful, + failed, + }) + + if (failed > 0) { + const errors = results + .filter((r) => r.status === "rejected") + .map((r) => (r as PromiseRejectedResult).reason) + debugError("[RedFlagResolutionHandler] 일부 계약서 처리 실패", errors) + } + + await revalidatePath("/evcp/basic-contract") + await revalidatePath("/evcp/compliance") + + debugSuccess("[RedFlagResolutionHandler] RED FLAG 해소 완료", { + successful, + failed, + }) + return { success: true, - message: "해제 대상이 없습니다.", + message: `${successful}개 계약서의 RED FLAG가 해제되었습니다.`, + updated: successful, } + } catch (error) { + debugError("[RedFlagResolutionHandler] RED FLAG 해소 처리 중 오류 발생", error) + throw error } +} - for (const target of targets) { - await resolveRedFlag(target.basicContractId, { - approvalId: target.approvalId ?? undefined, - revalidate: false, +/** + * 구매기획 담당자 EP ID 조회 + */ +export 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 } - await revalidatePath("/evcp/basic-contract") - await revalidatePath("/evcp/compliance") + const [user] = await db + .select({ + epId: users.epId, + }) + .from(users) + .where(eq(users.id, manager.purchasingManagerId)) + .limit(1) + + return user?.epId ?? null +} + +/** + * RED FLAG 해소요청 데이터를 결재 템플릿 변수로 매핑 + * + * @param contracts - RED FLAG가 있는 계약서 목록 + * @param meta - 요청자 정보 + * @returns 템플릿 변수 객체 (Record<string, string>) + */ +export async function mapRedFlagResolutionToTemplateVariables( + 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> + ` + }) + ) + + const detailHtml = detailSections.join("") + const formattedDate = new Intl.DateTimeFormat("ko-KR", { + dateStyle: "medium", + timeStyle: "short", + }).format(meta.requestedAt) return { - success: true, - updated: targets.length, + 요청자이름: meta.requesterName, + 요청일시: formattedDate, + 요청사유: "컴플라이언스 Red Flag 해소를 위해 구매기획 합의를 요청드립니다.", + RedFlag요약테이블: summaryTable, + RedFlag상세내역: detailHtml, } } 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}`) + } } |
