summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-24 12:18:26 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-24 12:18:26 +0000
commit7010b6a8c4d05cfb670aec6048f225db21c8c092 (patch)
tree8bc7ec0c2551f84abd6d5229dd09cb5df4fc6ea4 /lib
parent41bdba862edf3095c240ed316c3df31b31021ed8 (diff)
(임수민) 준법 Red Flag 버튼 수정
Diffstat (limited to 'lib')
-rw-r--r--lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx70
-rw-r--r--lib/compliance/red-flag-resolution.ts271
2 files changed, 249 insertions, 92 deletions
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 42fb2b5f..575582cf 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
@@ -21,8 +21,10 @@ 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"
+import { prepareRedFlagResolutionApproval, requestRedFlagResolution } from "@/lib/compliance/red-flag-resolution"
import { useRouter } from "next/navigation"
+import { useSession } from "next-auth/react"
+import { ApprovalPreviewDialog } from "@/lib/approval/client"
interface RedFlagResolutionState {
resolved: boolean
@@ -56,7 +58,16 @@ export function BasicContractDetailTableToolbarActions({
const [loading, setLoading] = React.useState(false)
const [buyerSignDialog, setBuyerSignDialog] = React.useState(false)
const [contractsToSign, setContractsToSign] = React.useState<any[]>([])
+ const [redFlagApprovalPreview, setRedFlagApprovalPreview] = React.useState<{
+ contractIds: number[]
+ templateName: string
+ variables: Record<string, string>
+ title: string
+ defaultApprovers?: string[]
+ } | null>(null)
+ const [showRedFlagApprovalDialog, setShowRedFlagApprovalDialog] = React.useState(false)
const router = useRouter()
+ const { data: session } = useSession()
// 각 버튼별 활성화 조건 계산
const canBulkDownload = hasSelectedRows && selectedRows.some(row =>
@@ -448,12 +459,45 @@ export function BasicContractDetailTableToolbarActions({
setLoading(true)
try {
const contractIds = redFlagEligibleContracts.map(c => c.id)
- const result = await requestRedFlagResolution(contractIds)
+ const preview = await prepareRedFlagResolutionApproval(contractIds)
+ setRedFlagApprovalPreview(preview)
+ setShowRedFlagApprovalDialog(true)
+ } catch (error) {
+ console.error("RED FLAG 해소요청 준비 오류:", error)
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "RED FLAG 해소요청 정보를 준비하는 중 오류가 발생했습니다."
+ )
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleRedFlagApprovalConfirm = async (approvalData: {
+ approvers: string[]
+ title: string
+ attachments?: File[]
+ }) => {
+ if (!redFlagApprovalPreview) {
+ toast.error("결재 정보를 찾을 수 없습니다. 다시 시도해주세요.")
+ return
+ }
+
+ setLoading(true)
+ try {
+ const result = await requestRedFlagResolution({
+ contractIds: redFlagApprovalPreview.contractIds,
+ approvers: approvalData.approvers,
+ title: approvalData.title,
+ })
toast.success("RED FLAG 해소요청 결재가 상신되었습니다.", {
description: `결재 ID: ${result.approvalId}`,
})
table.toggleAllPageRowsSelected(false)
+ setShowRedFlagApprovalDialog(false)
+ setRedFlagApprovalPreview(null)
} catch (error) {
console.error("RED FLAG 해소요청 오류:", error)
toast.error(
@@ -833,6 +877,28 @@ export function BasicContractDetailTableToolbarActions({
</DialogFooter>
</DialogContent>
</Dialog>
+ {redFlagApprovalPreview && session?.user?.epId && (
+ <ApprovalPreviewDialog
+ open={showRedFlagApprovalDialog}
+ onOpenChange={(open) => {
+ setShowRedFlagApprovalDialog(open)
+ if (!open) {
+ setRedFlagApprovalPreview(null)
+ }
+ }}
+ templateName={redFlagApprovalPreview.templateName}
+ variables={redFlagApprovalPreview.variables}
+ title={redFlagApprovalPreview.title}
+ defaultApprovers={redFlagApprovalPreview.defaultApprovers}
+ currentUser={{
+ id: Number(session.user.id),
+ epId: session.user.epId,
+ name: session.user.name || undefined,
+ email: session.user.email || undefined,
+ }}
+ onConfirm={handleRedFlagApprovalConfirm}
+ />
+ )}
</>
)
} \ No newline at end of file
diff --git a/lib/compliance/red-flag-resolution.ts b/lib/compliance/red-flag-resolution.ts
index 47a805bb..02e0179b 100644
--- a/lib/compliance/red-flag-resolution.ts
+++ b/lib/compliance/red-flag-resolution.ts
@@ -2,15 +2,19 @@
import db from "@/db/db"
import { and, eq, inArray } from "drizzle-orm"
-import { complianceResponses } from "@/db/schema/compliance"
+import { complianceResponses, redFlagManagers } 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 { revalidatePath } from "next/cache"
-import { requestRedFlagResolutionWithApproval } from "./approval-actions"
+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"
-export type ContractSummary = {
+type ContractSummary = {
contractId: number
vendorName: string | null
vendorCode: string | null
@@ -19,107 +23,83 @@ export type ContractSummary = {
triggeredFlags: TriggeredRedFlagInfo[]
}
-/**
- * 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 RED_FLAG_TEMPLATE_NAME = "컴플라이언스 Red Flag 해소요청"
- const uniqueContractIds = Array.from(new Set(contractIds))
+type SessionResult = Awaited<ReturnType<typeof getServerSession>>
+type SessionUser = (SessionResult extends { user: infer U } ? NonNullable<U> : never) & {
+ epId?: string | null
+ id?: string | number
+}
- const session = await getServerSession(authOptions)
- if (!session?.user) {
- throw new Error("인증이 필요합니다.")
- }
+type RedFlagResolutionRequestInput = {
+ contractIds: number[]
+ approvers?: string[]
+ title?: string
+}
- const currentUser = session.user
- if (!currentUser.epId) {
- throw new Error("Knox EP ID가 필요합니다.")
- }
+type RedFlagResolutionPreparationResult = {
+ currentUser: SessionUser
+ currentUserId: number
+ validContractIds: number[]
+ variables: Record<string, string>
+ title: string
+ defaultApprovers: string[]
+ now: Date
+}
- const currentUserId = Number(currentUser.id)
- if (Number.isNaN(currentUserId)) {
- throw new Error("유효한 사용자 정보가 필요합니다.")
- }
+/**
+ * RED FLAG 해소요청 미리보기 데이터 준비
+ */
+export async function prepareRedFlagResolutionApproval(contractIds: number[]) {
+ const context = await prepareRedFlagResolutionContext(contractIds)
- const purchasingManagerEpId = await getPurchasingManagerEpId()
- if (!purchasingManagerEpId) {
- throw new Error("구매기획 담당자의 EP ID가 설정되지 않았습니다.")
+ return {
+ contractIds: context.validContractIds,
+ templateName: RED_FLAG_TEMPLATE_NAME,
+ title: context.title,
+ variables: context.variables,
+ defaultApprovers: context.defaultApprovers,
}
+}
- // 준법 응답 및 중복 해소요청 여부를 먼저 확인 (Heavy query 전에 선행)
- const responses = await db
- .select({
- basicContractId: complianceResponses.basicContractId,
- redFlagResolvedAt: complianceResponses.redFlagResolvedAt,
- redFlagResolutionApprovalId: complianceResponses.redFlagResolutionApprovalId,
- })
- .from(complianceResponses)
- .where(inArray(complianceResponses.basicContractId, uniqueContractIds))
-
- const blockedContracts = responses
- .filter((response) => response.redFlagResolutionApprovalId && !response.redFlagResolvedAt)
- .map((response) => response.basicContractId)
-
- if (blockedContracts.length > 0) {
- const blockedSummaries = await fetchContractVendorSummaries(blockedContracts)
- const blockedNames = blockedSummaries.map(
- (contract) => contract.vendorName ?? `계약 ${contract.contractId}`
- )
-
- const preview =
- blockedNames.length > 2
- ? `${blockedNames.slice(0, 2).join(", ")} 외 ${blockedNames.length - 2}건`
- : blockedNames.join(", ")
+/**
+ * RED FLAG 해소요청 - Approval Saga를 통해 상신
+ */
+export async function requestRedFlagResolution(
+ input: RedFlagResolutionRequestInput
+): Promise<ApprovalResult> {
+ const context = await prepareRedFlagResolutionContext(input.contractIds)
- throw new Error(`이미 해소요청이 진행 중인 계약서가 있습니다: ${preview}`)
- }
+ const requestedTitle = input.title?.trim() || context.title
- const contractSummaries = await fetchContractsWithFlags(uniqueContractIds)
- if (contractSummaries.length === 0) {
- throw new Error("선택한 계약서에 RED FLAG가 존재하지 않습니다.")
- }
+ const baseApprovers =
+ input.approvers && input.approvers.length > 0 ? input.approvers : context.defaultApprovers
- const validContractIds = contractSummaries.map((contract) => contract.contractId)
-
- const missingResponses = validContractIds.filter(
- (contractId) => !responses.some((response) => response.basicContractId === contractId)
+ const approverEpIds = Array.from(
+ new Set(baseApprovers.filter((epId): epId is string => typeof epId === "string" && epId.length > 0))
)
- if (missingResponses.length > 0) {
- throw new Error("준법 응답 정보를 찾을 수 없는 계약서가 포함되어 있습니다.")
+ if (approverEpIds.length === 0) {
+ throw new Error("결재선을 설정해주세요.")
}
- 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(),
+ contractIds: context.validContractIds,
+ requestedBy: context.currentUserId,
+ requestedAt: context.now.toISOString(),
},
{
- title,
- description: "컴플라이언스 Red Flag 해소요청",
- templateName: "컴플라이언스 Red Flag 해소요청",
- variables,
- approvers: [purchasingManagerEpId],
+ title: requestedTitle,
+ description: RED_FLAG_TEMPLATE_NAME,
+ templateName: RED_FLAG_TEMPLATE_NAME,
+ variables: context.variables,
+ approvers: approverEpIds,
currentUser: {
- id: currentUserId,
- epId: currentUser.epId,
- email: currentUser.email ?? undefined,
+ id: context.currentUserId,
+ epId: context.currentUser.epId!,
+ email: context.currentUser.email ?? undefined,
},
}
)
@@ -134,7 +114,7 @@ export async function requestRedFlagResolution(contractIds: number[]): Promise<A
redFlagResolvedAt: null,
updatedAt: new Date(),
})
- .where(inArray(complianceResponses.basicContractId, validContractIds))
+ .where(inArray(complianceResponses.basicContractId, context.validContractIds))
await revalidatePath("/evcp/basic-contract")
await revalidatePath("/evcp/compliance")
@@ -179,10 +159,31 @@ export async function resolveRedFlag(
}
}
-/**
- * 계약서와 RED FLAG 정보를 함께 조회
- */
-export async function fetchContractsWithFlags(contractIds: number[]): Promise<ContractSummary[]> {
+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[]> {
const contracts = await db
.select({
contractId: basicContract.id,
@@ -288,3 +289,93 @@ function buildApprovalTitle(contracts: ContractSummary[]) {
return `Red Flag 해소요청 - ${firstVendor} 외 ${contracts.length - 1}건`
}
+
+async function prepareRedFlagResolutionContext(
+ contractIds: number[]
+): Promise<RedFlagResolutionPreparationResult> {
+ 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 responses = await db
+ .select({
+ basicContractId: complianceResponses.basicContractId,
+ redFlagResolvedAt: complianceResponses.redFlagResolvedAt,
+ redFlagResolutionApprovalId: complianceResponses.redFlagResolutionApprovalId,
+ })
+ .from(complianceResponses)
+ .where(inArray(complianceResponses.basicContractId, uniqueContractIds))
+
+ const blockedContracts = responses
+ .filter((response) => response.redFlagResolutionApprovalId && !response.redFlagResolvedAt)
+ .map((response) => response.basicContractId)
+
+ if (blockedContracts.length > 0) {
+ const blockedSummaries = await fetchContractVendorSummaries(blockedContracts)
+ const blockedNames = blockedSummaries.map(
+ (contract) => contract.vendorName ?? `계약 ${contract.contractId}`
+ )
+
+ const preview =
+ blockedNames.length > 2
+ ? `${blockedNames.slice(0, 2).join(", ")} 외 ${blockedNames.length - 2}건`
+ : blockedNames.join(", ")
+
+ throw new Error(`이미 해소요청이 진행 중인 계약서가 있습니다: ${preview}`)
+ }
+
+ const contractSummaries = await fetchContractsWithFlags(uniqueContractIds)
+ if (contractSummaries.length === 0) {
+ throw new Error("선택한 계약서에 RED FLAG가 존재하지 않습니다.")
+ }
+
+ const validContractIds = contractSummaries.map((contract) => contract.contractId)
+
+ const missingResponses = validContractIds.filter(
+ (contractId) => !responses.some((response) => response.basicContractId === contractId)
+ )
+
+ if (missingResponses.length > 0) {
+ throw new Error("준법 응답 정보를 찾을 수 없는 계약서가 포함되어 있습니다.")
+ }
+
+ const now = new Date()
+ const variables = await buildTemplateVariables(contractSummaries, {
+ requesterName: currentUser.name || currentUser.email || "요청자",
+ requestedAt: now,
+ })
+
+ const title = buildApprovalTitle(contractSummaries)
+
+ return {
+ currentUser,
+ currentUserId,
+ validContractIds,
+ variables,
+ title,
+ defaultApprovers: [purchasingManagerEpId],
+ now,
+ }
+}