diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-21 06:04:56 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-21 06:04:56 +0000 |
| commit | b845ccde2910894911233cda273657d2b52e63f9 (patch) | |
| tree | f12032484952225d099b3399f800d912bfdbda88 /lib | |
| parent | eacfa3f36274e495838a7114d68ff80a0f257a6a (diff) | |
(임수민) 준법 Red Flag 수정
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/basic-contract/actions/check-red-flag-resolution.ts | 97 | ||||
| -rw-r--r-- | lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx | 71 | ||||
| -rw-r--r-- | lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx | 51 | ||||
| -rw-r--r-- | lib/basic-contract/status-detail/basic-contracts-detail-table.tsx | 43 | ||||
| -rw-r--r-- | lib/compliance/red-flag-resolution.ts | 303 | ||||
| -rw-r--r-- | lib/compliance/services.ts | 2 | ||||
| -rw-r--r-- | lib/knox-api/approval/approval.ts | 100 |
7 files changed, 651 insertions, 16 deletions
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<Record<number, { resolved: boolean; resolvedAt: Date | null }>> { + const result: Record<number, { resolved: boolean; resolvedAt: Date | null }> = {}; + + // 준법서약 템플릿인 계약서만 필터링 + 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<BasicContractView> gtcData?: Record<number, { gtcDocumentId: number | null; hasComments: boolean }> + redFlagData?: Record<number, boolean> + 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 = {} }: </span> </Button> + {/* RED FLAG 해소요청 버튼 (준법서약 템플릿만) */} + {isComplianceTemplate && ( + <Button + variant="outline" + size="sm" + onClick={handleRequestRedFlagResolution} + disabled={!canRequestRedFlagResolution || loading} + className="gap-2" + title={!hasSelectedRows + ? "계약서를 선택해주세요" + : !canRequestRedFlagResolution + ? "RED FLAG가 있는 계약서를 선택해주세요" + : `${redFlagResolutionContracts.length}건 RED FLAG 해소요청` + } + > + <Flag className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline"> + RED FLAG 해소요청 {hasSelectedRows ? `(${redFlagResolutionContracts.length})` : ''} + </span> + </Button> + )} + {/* 재요청 버튼 */} <Button variant="outline" diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx index c872aede..b2c811fd 100644 --- a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx +++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx @@ -38,6 +38,8 @@ interface GetColumnsProps { isLoadingGtcData: boolean redFlagData: Record<number, boolean> isLoadingRedFlagData: boolean + redFlagResolutionData: Record<number, { resolved: boolean; resolvedAt: Date | null }> + isLoadingRedFlagResolutionData: boolean isComplianceTemplate: boolean router: NextRouter; } @@ -61,6 +63,8 @@ export function getDetailColumns({ isLoadingGtcData, redFlagData, isLoadingRedFlagData, + redFlagResolutionData, + isLoadingRedFlagResolutionData, isComplianceTemplate, router }: GetColumnsProps): ColumnDef<BasicContractView>[] { @@ -223,6 +227,44 @@ export function getDetailColumns({ enableHiding: false, } + // Red Flag 해제 컬럼 (준법서약 템플릿만) + const redFlagResolutionColumn: ColumnDef<BasicContractView> = { + id: "redFlagResolution", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Red Flag 해제" /> + ), + cell: ({ row }) => { + const contract = row.original; + const contractId = contract.id; + + // 로딩 중이면 로딩 표시 + if (isLoadingRedFlagResolutionData) { + return <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />; + } + + const resolution = redFlagResolutionData[contractId]; + + if (resolution?.resolved && resolution.resolvedAt) { + return ( + <div className="text-sm"> + <Badge variant="default" className="font-medium bg-green-600"> + 해제됨 + </Badge> + <div className="text-xs text-gray-500 mt-1"> + {formatDateTime(resolution.resolvedAt, "KR")} + </div> + </div> + ); + } + + return ( + <div className="text-sm text-gray-400">-</div> + ); + }, + minSize: 140, + enableHiding: false, + } + // 기본 컬럼 배열 const baseColumns: ColumnDef<BasicContractView>[] = [ selectColumn, @@ -421,8 +463,9 @@ export function getDetailColumns({ ), cell: ({ row }) => { const status = row.getValue("legalReviewStatus") as string | null - const requestedDate = row.getValue("legalReviewRequestedAt") as Date | null - const completedDate = row.getValue("legalReviewCompletedAt") as Date | null + const contract = row.original + const requestedDate = contract.legalReviewRequestedAt as Date | null + const completedDate = contract.legalReviewCompletedAt as Date | null // 법무검토 상태 우선, 없으면 기존 로직으로 판단 if (status) { @@ -554,7 +597,7 @@ export function getDetailColumns({ actionsColumn, ] - // 준법서약 템플릿인 경우 Red Flag 컬럼을 법무검토 상태 뒤에 추가 + // 준법서약 템플릿인 경우 Red Flag 컬럼과 해제 컬럼을 법무검토 상태 뒤에 추가 if (isComplianceTemplate) { const legalReviewStatusIndex = baseColumns.findIndex((col) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -562,7 +605,7 @@ export function getDetailColumns({ }) if (legalReviewStatusIndex !== -1) { - baseColumns.splice(legalReviewStatusIndex + 1, 0, redFlagColumn) + baseColumns.splice(legalReviewStatusIndex + 1, 0, redFlagColumn, redFlagResolutionColumn) } } diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx index 93853560..2d747c85 100644 --- a/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx +++ b/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx @@ -12,6 +12,7 @@ import { getDetailColumns } from "./basic-contracts-detail-columns" import { getBasicContractsByTemplateId } from "@/lib/basic-contract/service" import { checkGTCCommentsForContracts } from "@/lib/basic-contract/actions/check-gtc-comments" import { checkRedFlagsForContracts } from "@/lib/basic-contract/actions/check-red-flags" +import { checkRedFlagResolutionForContracts } from "@/lib/basic-contract/actions/check-red-flag-resolution" import { BasicContractView } from "@/db/schema" import { BasicContractDetailTableToolbarActions } from "./basic-contract-detail-table-toolbar-actions" import { toast } from "sonner" @@ -37,6 +38,10 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac // Red Flag data 상태 관리 const [redFlagData, setRedFlagData] = React.useState<Record<number, boolean>>({}) const [isLoadingRedFlagData, setIsLoadingRedFlagData] = React.useState(false) + + // Red Flag 해제 data 상태 관리 + const [redFlagResolutionData, setRedFlagResolutionData] = React.useState<Record<number, { resolved: boolean; resolvedAt: Date | null }>>({}) + const [isLoadingRedFlagResolutionData, setIsLoadingRedFlagResolutionData] = React.useState(false) const [{ data, pageCount }] = React.use(promises) const router = useRouter() @@ -98,6 +103,33 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac loadRedFlagData(); }, [data]); + // Red Flag 해제 data 로딩 + React.useEffect(() => { + const loadRedFlagResolutionData = async () => { + if (!data || data.length === 0) return; + + // 준법서약 템플릿이 있는지 확인 + const hasComplianceTemplates = data.some(contract => + contract.templateName?.includes('준법') + ); + + if (!hasComplianceTemplates) return; + + setIsLoadingRedFlagResolutionData(true); + try { + const resolutionResults = await checkRedFlagResolutionForContracts(data); + setRedFlagResolutionData(resolutionResults); + } catch (error) { + console.error('Error checking Red Flag Resolution data:', error); + toast.error("Red Flag 해제 데이터를 불러오는데 실패했습니다."); + } finally { + setIsLoadingRedFlagResolutionData(false); + } + }; + + loadRedFlagResolutionData(); + }, [data]); + // 준법서약 템플릿인지 확인 const isComplianceTemplate = React.useMemo(() => { if (!data || data.length === 0) return false; @@ -111,10 +143,12 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac isLoadingGtcData, redFlagData, isLoadingRedFlagData, + redFlagResolutionData, + isLoadingRedFlagResolutionData, isComplianceTemplate, router }), - [setRowAction, gtcData, isLoadingGtcData, redFlagData, isLoadingRedFlagData, isComplianceTemplate, router] + [setRowAction, gtcData, isLoadingGtcData, redFlagData, isLoadingRedFlagData, redFlagResolutionData, isLoadingRedFlagResolutionData, isComplianceTemplate, router] ) const advancedFilterFields: DataTableAdvancedFilterField<BasicContractView>[] = [ @@ -192,7 +226,12 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac table={table} filterFields={advancedFilterFields} > - <BasicContractDetailTableToolbarActions table={table} gtcData={gtcData} /> + <BasicContractDetailTableToolbarActions + table={table} + gtcData={gtcData} + redFlagData={redFlagData} + isComplianceTemplate={isComplianceTemplate} + /> </DataTableAdvancedToolbar> </DataTable> ) 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, }) diff --git a/lib/knox-api/approval/approval.ts b/lib/knox-api/approval/approval.ts index f9ed8cfb..5599c066 100644 --- a/lib/knox-api/approval/approval.ts +++ b/lib/knox-api/approval/approval.ts @@ -4,6 +4,7 @@ import { getKnoxConfig, createJsonHeaders, createFormHeaders } from '../common'; import { randomUUID } from 'crypto'; import { saveApprovalToDatabase, deleteApprovalFromDatabase, upsertApprovalStatus } from './service'; import { debugLog, debugError } from '@/lib/debug-utils' +import { resolveRedFlag } from '@/lib/compliance/red-flag-resolution' // Knox API Approval 서버 액션들 // 가이드: lib/knox-api/approval/guide.html @@ -830,6 +831,34 @@ export async function syncApprovalStatusAction(): Promise<{ // upsert를 사용한 상태 업데이트 await upsertApprovalStatus(statusData.apInfId, statusData.status); updated++; + + // RED FLAG 해소 결재 완료 확인 및 처리 + if (['2', '5', '6'].includes(statusData.status)) { + // 완결(2), 전결(5), 후완결(6) 상태인 경우 RED FLAG 해소 처리 + try { + const db = (await import('@/db/db')).default; + const { complianceResponses } = await import('@/db/schema/compliance'); + const { eq } = await import('drizzle-orm'); + + // 해당 결재 ID로 compliance response 조회 + const response = await db + .select({ + id: complianceResponses.id, + basicContractId: complianceResponses.basicContractId, + }) + .from(complianceResponses) + .where(eq(complianceResponses.redFlagResolutionApprovalId, statusData.apInfId)) + .limit(1); + + if (response[0]) { + // RED FLAG 해소 처리 + await resolveRedFlag(response[0].basicContractId, statusData.apInfId); + } + } catch (redFlagError) { + console.error(`RED FLAG 해소 처리 실패 (${statusData.apInfId}):`, redFlagError); + // RED FLAG 해소 실패는 결재 상태 업데이트를 무효화하지 않음 + } + } } } catch (updateError) { console.error(`결재상태 업데이트 실패 (${statusData.apInfId}):`, updateError); @@ -915,14 +944,42 @@ export async function syncSpecificApprovalStatusAction( if (statusResponse.result === 'success' && statusResponse.data) { // 조회된 상태로 데이터베이스 업데이트 for (const statusData of statusResponse.data) { - try { - // upsert를 사용한 상태 업데이트 - await upsertApprovalStatus(statusData.apInfId, statusData.status); - updated++; - } catch (updateError) { - console.error(`결재상태 업데이트 실패 (${statusData.apInfId}):`, updateError); - failed.push(statusData.apInfId); - } + try { + // upsert를 사용한 상태 업데이트 + await upsertApprovalStatus(statusData.apInfId, statusData.status); + updated++; + + // RED FLAG 해소 결재 완료 확인 및 처리 + if (['2', '5', '6'].includes(statusData.status)) { + // 완결(2), 전결(5), 후완결(6) 상태인 경우 RED FLAG 해소 처리 + try { + const db = (await import('@/db/db')).default; + const { complianceResponses } = await import('@/db/schema/compliance'); + const { eq } = await import('drizzle-orm'); + + // 해당 결재 ID로 compliance response 조회 + const response = await db + .select({ + id: complianceResponses.id, + basicContractId: complianceResponses.basicContractId, + }) + .from(complianceResponses) + .where(eq(complianceResponses.redFlagResolutionApprovalId, statusData.apInfId)) + .limit(1); + + if (response[0]) { + // RED FLAG 해소 처리 + await resolveRedFlag(response[0].basicContractId, statusData.apInfId); + } + } catch (redFlagError) { + console.error(`RED FLAG 해소 처리 실패 (${statusData.apInfId}):`, redFlagError); + // RED FLAG 해소 실패는 결재 상태 업데이트를 무효화하지 않음 + } + } + } catch (updateError) { + console.error(`결재상태 업데이트 실패 (${statusData.apInfId}):`, updateError); + failed.push(statusData.apInfId); + } } } else { console.error('Knox API 결재상황조회 실패:', statusResponse); @@ -1071,6 +1128,33 @@ export async function getApprovalLogsAction(): Promise<{ // 메모리상의 데이터도 업데이트 currentLog.status = statusData.status; updatedCount++; + + // RED FLAG 해소 결재 완료 확인 및 처리 + if (['2', '5', '6'].includes(statusData.status)) { + // 완결(2), 전결(5), 후완결(6) 상태인 경우 RED FLAG 해소 처리 + try { + const { complianceResponses } = await import('@/db/schema/compliance'); + const { eq } = await import('drizzle-orm'); + + // 해당 결재 ID로 compliance response 조회 + const response = await db + .select({ + id: complianceResponses.id, + basicContractId: complianceResponses.basicContractId, + }) + .from(complianceResponses) + .where(eq(complianceResponses.redFlagResolutionApprovalId, statusData.apInfId)) + .limit(1); + + if (response[0]) { + // RED FLAG 해소 처리 + await resolveRedFlag(response[0].basicContractId, statusData.apInfId); + } + } catch (redFlagError) { + console.error(`RED FLAG 해소 처리 실패 (${statusData.apInfId}):`, redFlagError); + // RED FLAG 해소 실패는 결재 상태 업데이트를 무효화하지 않음 + } + } } catch (updateError) { console.error(`결재상태 업데이트 실패 (${statusData.apInfId}):`, updateError); } |
