summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-21 06:04:56 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-21 06:04:56 +0000
commitb845ccde2910894911233cda273657d2b52e63f9 (patch)
treef12032484952225d099b3399f800d912bfdbda88 /lib
parenteacfa3f36274e495838a7114d68ff80a0f257a6a (diff)
(임수민) 준법 Red Flag 수정
Diffstat (limited to 'lib')
-rw-r--r--lib/basic-contract/actions/check-red-flag-resolution.ts97
-rw-r--r--lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx71
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx51
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-table.tsx43
-rw-r--r--lib/compliance/red-flag-resolution.ts303
-rw-r--r--lib/compliance/services.ts2
-rw-r--r--lib/knox-api/approval/approval.ts100
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);
}