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 --- .../actions/check-red-flag-resolution.ts | 97 +++++++ ...basic-contract-detail-table-toolbar-actions.tsx | 71 ++++- .../basic-contracts-detail-columns.tsx | 51 +++- .../status-detail/basic-contracts-detail-table.tsx | 43 ++- lib/compliance/red-flag-resolution.ts | 303 +++++++++++++++++++++ lib/compliance/services.ts | 2 + lib/knox-api/approval/approval.ts | 100 ++++++- 7 files changed, 651 insertions(+), 16 deletions(-) create mode 100644 lib/basic-contract/actions/check-red-flag-resolution.ts create mode 100644 lib/compliance/red-flag-resolution.ts (limited to 'lib') diff --git a/lib/basic-contract/actions/check-red-flag-resolution.ts b/lib/basic-contract/actions/check-red-flag-resolution.ts new file mode 100644 index 00000000..84dcdf75 --- /dev/null +++ b/lib/basic-contract/actions/check-red-flag-resolution.ts @@ -0,0 +1,97 @@ +"use server"; + +import { BasicContractView } from "@/db/schema"; +import { getComplianceResponseByBasicContractId } from "@/lib/compliance/services"; +import { syncSpecificApprovalStatusAction } from "@/lib/knox-api/approval/approval"; + +/** + * 여러 계약서에 대한 RED FLAG 해제 상태를 한 번에 확인 + */ +export async function checkRedFlagResolutionForContracts( + contracts: BasicContractView[] +): Promise> { + const result: Record = {}; + + // 준법서약 템플릿인 계약서만 필터링 + const complianceContracts = contracts.filter(contract => + contract.templateName?.includes('준법') + ); + + if (complianceContracts.length === 0) { + return result; + } + + // 1. 먼저 DB에서 현재 상태 조회 + const initialChecks = await Promise.all( + complianceContracts.map(async (contract) => { + try { + const response = await getComplianceResponseByBasicContractId(contract.id); + return { + contractId: contract.id, + response + }; + } catch (error) { + console.error(`Error fetching compliance response for contract ${contract.id}:`, error); + return { + contractId: contract.id, + response: null + }; + } + }) + ); + + // 2. 진행 중인 결재(해소요청)가 있는지 확인하고 Knox 상태 동기화 + const pendingApprovalIds: string[] = []; + + initialChecks.forEach(check => { + const { response } = check; + // 해소요청은 했으나(approvalId 있음) 아직 해소되지 않은(resolvedAt 없음) 경우 + if (response?.redFlagResolutionApprovalId && !response.redFlagResolvedAt) { + pendingApprovalIds.push(response.redFlagResolutionApprovalId); + } + }); + + if (pendingApprovalIds.length > 0) { + try { + // Knox API를 통해 최신 결재 상태 동기화 + // 이 과정에서 결재가 완료되었다면 DB의 redFlagResolvedAt도 업데이트됨 (syncSpecificApprovalStatusAction 내부 로직) + await syncSpecificApprovalStatusAction(pendingApprovalIds); + } catch (error) { + console.error('Error syncing approval status:', error); + } + } + + // 3. 동기화 후 최종 상태 다시 확인 + // (동기화 과정에서 DB가 업데이트되었을 수 있으므로 다시 조회하거나, + // 성능을 위해 위에서 동기화된 건만 다시 조회하는 방식도 가능하지만, + // 여기서는 안전하게 다시 조회하는 방식을 택함) + const finalChecks = await Promise.all( + complianceContracts.map(async (contract) => { + try { + const response = await getComplianceResponseByBasicContractId(contract.id); + return { + contractId: contract.id, + resolved: response?.redFlagResolvedAt !== null && response?.redFlagResolvedAt !== undefined, + resolvedAt: response?.redFlagResolvedAt || null + }; + } catch (error) { + return { + contractId: contract.id, + resolved: false, + resolvedAt: null + }; + } + }) + ); + + // 결과를 Record 형태로 변환 + finalChecks.forEach(check => { + result[check.contractId] = { + resolved: check.resolved, + resolvedAt: check.resolvedAt + }; + }); + + return result; +} + diff --git a/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx b/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx index 3e965fac..daa410f0 100644 --- a/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx +++ b/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { type Table } from "@tanstack/react-table" -import { Download, FileDown, Mail, CheckCircle, AlertTriangle, Send, Check, FileSignature, FileText, ExternalLink, Globe } from "lucide-react" +import { Download, FileDown, Mail, CheckCircle, AlertTriangle, Send, Check, FileSignature, FileText, ExternalLink, Globe, Flag } from "lucide-react" import { exportTableToExcel } from "@/lib/export" import { downloadFile } from "@/lib/file-download" @@ -21,13 +21,21 @@ import { Badge } from "@/components/ui/badge" import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction, updateLegalReviewStatusFromSSLVW } from "../service" import { BasicContractSignDialog } from "../vendor-table/basic-contract-sign-dialog" import { SSLVWPurInqReqDialog } from "@/components/common/legal/sslvw-pur-inq-req-dialog" +import { requestRedFlagResolution } from "@/lib/compliance/red-flag-resolution" interface BasicContractDetailTableToolbarActionsProps { table: Table gtcData?: Record + redFlagData?: Record + isComplianceTemplate?: boolean } -export function BasicContractDetailTableToolbarActions({ table, gtcData = {} }: BasicContractDetailTableToolbarActionsProps) { +export function BasicContractDetailTableToolbarActions({ + table, + gtcData = {}, + redFlagData = {}, + isComplianceTemplate = false +}: BasicContractDetailTableToolbarActionsProps) { // 선택된 행들 가져오기 const selectedRows = table.getSelectedRowModel().rows const hasSelectedRows = selectedRows.length > 0 @@ -383,6 +391,43 @@ export function BasicContractDetailTableToolbarActions({ table, gtcData = {} }: } } + // RED FLAG 해소요청 가능 여부 + const canRequestRedFlagResolution = hasSelectedRows && isComplianceTemplate && selectedRows.some(row => { + const contract = row.original + return redFlagData[contract.id] === true + }) + + // RED FLAG 해소요청 가능한 계약서들 + const redFlagResolutionContracts = selectedRows + .map(row => row.original) + .filter(contract => redFlagData[contract.id] === true) + + // RED FLAG 해소요청 + const handleRequestRedFlagResolution = async () => { + if (!canRequestRedFlagResolution) { + toast.error("RED FLAG가 있는 계약서를 선택해주세요") + return + } + + setLoading(true) + try { + const contractIds = redFlagResolutionContracts.map(c => c.id) + const result = await requestRedFlagResolution(contractIds) + + if (result.success) { + toast.success(result.message) + table.toggleAllPageRowsSelected(false) + } else { + toast.error(result.message) + } + } catch (error) { + console.error("RED FLAG 해소요청 오류:", error) + toast.error("RED FLAG 해소요청 중 오류가 발생했습니다") + } finally { + setLoading(false) + } + } + // 법무검토 요청 링크 목록 const legalReviewLinks = [ { @@ -445,6 +490,28 @@ export function BasicContractDetailTableToolbarActions({ table, gtcData = {} }: + {/* RED FLAG 해소요청 버튼 (준법서약 템플릿만) */} + {isComplianceTemplate && ( + + )} + {/* 재요청 버튼 */}