diff options
Diffstat (limited to 'lib/basic-contract')
7 files changed, 261 insertions, 102 deletions
diff --git a/lib/basic-contract/actions/check-red-flag-resolution.ts b/lib/basic-contract/actions/check-red-flag-resolution.ts index 84dcdf75..3ce21bde 100644 --- a/lib/basic-contract/actions/check-red-flag-resolution.ts +++ b/lib/basic-contract/actions/check-red-flag-resolution.ts @@ -2,96 +2,60 @@ import { BasicContractView } from "@/db/schema"; import { getComplianceResponseByBasicContractId } from "@/lib/compliance/services"; -import { syncSpecificApprovalStatusAction } from "@/lib/knox-api/approval/approval"; + +type RedFlagResolutionState = { + resolved: boolean; + resolvedAt: Date | null; + pendingApprovalId: string | null; +}; /** * 여러 계약서에 대한 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 }> = {}; - +): Promise<Record<number, RedFlagResolutionState>> { + const result: Record<number, RedFlagResolutionState> = {}; + // 준법서약 템플릿인 계약서만 필터링 - const complianceContracts = contracts.filter(contract => - contract.templateName?.includes('준법') + const complianceContracts = contracts.filter((contract) => + contract.templateName?.includes("준법") ); - + if (complianceContracts.length === 0) { return result; } - // 1. 먼저 DB에서 현재 상태 조회 - const initialChecks = await Promise.all( + const checks = await Promise.all( complianceContracts.map(async (contract) => { try { const response = await getComplianceResponseByBasicContractId(contract.id); return { contractId: contract.id, - response + resolved: Boolean(response?.redFlagResolvedAt), + resolvedAt: response?.redFlagResolvedAt || null, + pendingApprovalId: response?.redFlagResolutionApprovalId ?? null, }; } 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 + resolvedAt: null, + pendingApprovalId: null, }; } }) ); - - // 결과를 Record 형태로 변환 - finalChecks.forEach(check => { + + checks.forEach((check) => { result[check.contractId] = { resolved: check.resolved, - resolvedAt: check.resolvedAt + resolvedAt: check.resolvedAt, + pendingApprovalId: check.pendingApprovalId, }; }); - + return result; } diff --git a/lib/basic-contract/agreement-comments/actions.ts b/lib/basic-contract/agreement-comments/actions.ts index c4ded36e..2f60c3d8 100644 --- a/lib/basic-contract/agreement-comments/actions.ts +++ b/lib/basic-contract/agreement-comments/actions.ts @@ -2,7 +2,7 @@ import { revalidateTag } from "next/cache"; import db from "@/db/db"; -import { eq, and, desc } from "drizzle-orm"; +import { eq, and, desc, inArray, sql } from "drizzle-orm"; import { agreementComments, basicContract, vendors, users } from "@/db/schema"; import { saveFile, deleteFile } from "@/lib/file-stroage"; import { sendEmail } from "@/lib/mail/sendEmail"; @@ -32,6 +32,23 @@ export interface AgreementCommentData { updatedAt: Date; } +export interface AgreementCommentSummary { + hasComments: boolean; + commentCount: number; +} + +function getContractDocumentLabel(templateName?: string | null) { + if (!templateName) return "기본계약서"; + const normalized = templateName.toLowerCase(); + if (normalized.includes('준법')) { + return "준법서약"; + } + if (normalized.includes('gtc')) { + return "GTC 기본계약서"; + } + return templateName; +} + /** * 특정 기본계약서의 모든 코멘트 조회 */ @@ -496,6 +513,55 @@ export async function checkNegotiationStatus( } /** + * 다수의 계약서에 대한 협의 코멘트 상태 조회 + */ +export async function checkAgreementCommentsForContracts( + contracts: { id: number }[] +): Promise<Record<number, AgreementCommentSummary>> { + if (!contracts || contracts.length === 0) { + return {}; + } + + try { + const contractIds = contracts.map(contract => contract.id); + + const commentCounts = await db + .select({ + contractId: agreementComments.basicContractId, + count: sql<number>`count(*)`, + }) + .from(agreementComments) + .where( + and( + inArray(agreementComments.basicContractId, contractIds), + eq(agreementComments.isDeleted, false) + ) + ) + .groupBy(agreementComments.basicContractId); + + const countsMap = new Map<number, number>(); + commentCounts.forEach(({ contractId, count }) => { + countsMap.set(contractId, Number(count || 0)); + }); + + const result: Record<number, AgreementCommentSummary> = {}; + + contractIds.forEach((contractId) => { + const count = countsMap.get(contractId) ?? 0; + result[contractId] = { + hasComments: count > 0, + commentCount: count, + }; + }); + + return result; + } catch (error) { + console.error("협의 상태 조회 실패:", error); + return {}; + } +} + +/** * 이메일 알림 발송 */ async function sendCommentNotificationEmail(params: { @@ -509,6 +575,7 @@ async function sendCommentNotificationEmail(params: { attachmentCount?: number; }) { const { comment, contract, vendor, requester, templateName, authorType, authorName, attachmentCount = 0 } = params; + const documentTypeLabel = getContractDocumentLabel(templateName); // 수신자 결정 let recipientEmail: string | undefined; @@ -536,7 +603,7 @@ async function sendCommentNotificationEmail(params: { // 이메일 발송 await sendEmail({ to: recipientEmail, - subject: `[eVCP] GTC 기본계약서 협의 코멘트 제출 - ${templateName || '기본계약서'}`, + subject: `[eVCP] ${documentTypeLabel} 협의 코멘트 제출 - ${templateName || '기본계약서'}`, template: "agreement-comment-notification", context: { language: "ko", @@ -545,6 +612,7 @@ async function sendCommentNotificationEmail(params: { authorType: authorType === 'SHI' ? '삼성중공업' : '협력업체', comment: comment.comment, templateName: templateName || '기본계약서', + documentTypeLabel, vendorName: vendor?.vendorName || '', attachmentCount, contractUrl: `${process.env.NEXT_PUBLIC_APP_URL}/evcp/basic-contract/${contract.id}`, @@ -617,18 +685,20 @@ export async function completeNegotiation( .limit(1); templateName = template?.templateName || null; } + const documentTypeLabel = getContractDocumentLabel(templateName); // 이메일 알림 발송 try { if (requester) { await sendEmail({ to: requester.email || '', - subject: `[eVCP] GTC 기본계약서 협의 완료 - ${templateName || '기본계약서'}`, + subject: `[eVCP] ${documentTypeLabel} 협의 완료 - ${templateName || '기본계약서'}`, template: "negotiation-complete-notification", context: { language: "ko", recipientName: requester.name || "담당자", templateName: templateName || '기본계약서', + documentTypeLabel, vendorName: vendor?.vendorName || '', contractUrl: `${process.env.NEXT_PUBLIC_APP_URL}/evcp/basic-contract/${contract.id}`, systemUrl: process.env.NEXT_PUBLIC_APP_URL || 'https://evcp.com', 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 daa410f0..e62a6cb7 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 @@ -414,15 +414,17 @@ export function BasicContractDetailTableToolbarActions({ 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) - } + toast.success("RED FLAG 해소요청 결재가 상신되었습니다.", { + description: `결재 ID: ${result.approvalId}`, + }) + table.toggleAllPageRowsSelected(false) } catch (error) { console.error("RED FLAG 해소요청 오류:", error) - toast.error("RED FLAG 해소요청 중 오류가 발생했습니다") + toast.error( + error instanceof Error + ? error.message + : "RED FLAG 해소요청 중 오류가 발생했습니다." + ) } finally { setLoading(false) } 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 b2c811fd..2ab39880 100644 --- a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx +++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx @@ -32,13 +32,21 @@ import { toast } from "sonner" import { useRouter } from "next/navigation" import { getComplianceResponseByBasicContractId } from "@/lib/compliance/services" -interface GetColumnsProps { +type RedFlagResolutionState = { + resolved: boolean + resolvedAt: Date | null + pendingApprovalId: string | null +} + +export interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<BasicContractView> | null>> gtcData: Record<number, { gtcDocumentId: number | null; hasComments: boolean }> isLoadingGtcData: boolean + agreementCommentData: Record<number, { hasComments: boolean; commentCount: number }> + isLoadingAgreementCommentData: boolean redFlagData: Record<number, boolean> isLoadingRedFlagData: boolean - redFlagResolutionData: Record<number, { resolved: boolean; resolvedAt: Date | null }> + redFlagResolutionData: Record<number, RedFlagResolutionState> isLoadingRedFlagResolutionData: boolean isComplianceTemplate: boolean router: NextRouter; @@ -61,6 +69,8 @@ export function getDetailColumns({ setRowAction, gtcData, isLoadingGtcData, + agreementCommentData, + isLoadingAgreementCommentData, redFlagData, isLoadingRedFlagData, redFlagResolutionData, @@ -134,7 +144,7 @@ export function getDetailColumns({ } const handleResend = () => { - setRowAction({ type: "resend", row }) + setRowAction({ type: "resend", row } as DataTableRowAction<BasicContractView>) } return ( @@ -256,6 +266,19 @@ export function getDetailColumns({ </div> ); } + + if (resolution?.pendingApprovalId) { + return ( + <div className="text-sm"> + <Badge variant="secondary" className="font-medium"> + 해소요청 진행중 + </Badge> + <div className="text-xs text-gray-500 mt-1"> + 결재 ID: {resolution.pendingApprovalId.slice(-6)} + </div> + </div> + ); + } return ( <div className="text-sm text-gray-400">-</div> @@ -296,7 +319,10 @@ export function getDetailColumns({ const name = row.getValue("vendorName") as string | null const contract = row.original const isGTCTemplate = contract.templateName?.includes('GTC') + const isComplianceContract = contract.templateName?.includes('준법') const contractGtcData = gtcData[contract.id] + const complianceNegotiation = agreementCommentData[contract.id] + const isNegotiationCompleted = !!contract.negotiationCompletedAt const handleOpenGTC = (e: React.MouseEvent) => { e.stopPropagation() @@ -345,6 +371,48 @@ export function getDetailColumns({ )} </div> )} + {isComplianceContract && ( + <div className="flex items-center gap-1"> + {isLoadingAgreementCommentData ? ( + <Loader2 className="h-3 w-3 animate-spin text-gray-400" /> + ) : isNegotiationCompleted ? ( + <Badge + variant="outline" + className="text-xs bg-green-50 text-green-700 border-green-200" + > + <MessageCircle className="h-3 w-3 mr-1" /> + 협의 완료 + </Badge> + ) : complianceNegotiation?.hasComments ? ( + <Badge + variant="outline" + className="text-xs bg-orange-50 text-orange-700 border-orange-200" + title={`협의 코멘트 ${complianceNegotiation.commentCount}개`} + onClick={(event) => { + event.stopPropagation(); + if (typeof window === "undefined") return; + const params = new URLSearchParams(); + if (contract.templateId) { + params.set("templateId", contract.templateId.toString()); + } + if (contract.vendorId) { + params.set("vendorId", contract.vendorId.toString()); + } + if (contract.vendorName) { + params.set("vendorName", contract.vendorName); + } + const query = params.toString(); + const complianceUrl = `/evcp/basic-contract/compliance-comments/${contract.id}${query ? `?${query}` : ""}`; + window.open(complianceUrl, "_blank", "noopener,noreferrer"); + }} + style={{ cursor: "pointer" }} + > + <MessageCircle className="h-3 w-3 mr-1" /> + 협의 진행중 ({complianceNegotiation.commentCount}) + </Badge> + ) : null} + </div> + )} </div> ) }, 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 2d747c85..010b4713 100644 --- a/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx +++ b/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx @@ -11,6 +11,7 @@ import type { 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 { checkAgreementCommentsForContracts } from "@/lib/basic-contract/agreement-comments/actions" 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" @@ -35,20 +36,27 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac const [gtcData, setGtcData] = React.useState<Record<number, { gtcDocumentId: number | null; hasComments: boolean }>>({}) const [isLoadingGtcData, setIsLoadingGtcData] = React.useState(false) + // 협의 코멘트 상태 (준법 포함) 관리 + const [agreementCommentData, setAgreementCommentData] = React.useState<Record<number, { hasComments: boolean; commentCount: number }>>({}) + const [isLoadingAgreementCommentData, setIsLoadingAgreementCommentData] = React.useState(false) + // Red Flag data 상태 관리 const [redFlagData, setRedFlagData] = React.useState<Record<number, boolean>>({}) const [isLoadingRedFlagData, setIsLoadingRedFlagData] = React.useState(false) +type RedFlagResolutionState = { + resolved: boolean + resolvedAt: Date | null + pendingApprovalId: string | null +} + // Red Flag 해제 data 상태 관리 - const [redFlagResolutionData, setRedFlagResolutionData] = React.useState<Record<number, { resolved: boolean; resolvedAt: Date | null }>>({}) + const [redFlagResolutionData, setRedFlagResolutionData] = React.useState<Record<number, RedFlagResolutionState>>({}) const [isLoadingRedFlagResolutionData, setIsLoadingRedFlagResolutionData] = React.useState(false) const [{ data, pageCount }] = React.use(promises) const router = useRouter() - console.log(gtcData, "gtcData") - console.log(data, "data") - // GTC data 로딩 React.useEffect(() => { const loadGtcData = async () => { @@ -76,6 +84,32 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac loadGtcData(); }, [data]); + // 협의 코멘트 상태 로딩 (준법 포함) + React.useEffect(() => { + const loadAgreementComments = async () => { + if (!data || data.length === 0) return; + + const hasNegotiationTemplates = data.some(contract => + contract.templateName?.includes('준법') || contract.templateName?.includes('GTC') + ); + + if (!hasNegotiationTemplates) return; + + setIsLoadingAgreementCommentData(true); + try { + const results = await checkAgreementCommentsForContracts(data); + setAgreementCommentData(results); + } catch (error) { + console.error('협의 코멘트 정보를 불러오는데 실패했습니다:', error); + toast.error("협의 코멘트 정보를 불러오는데 실패했습니다."); + } finally { + setIsLoadingAgreementCommentData(false); + } + }; + + loadAgreementComments(); + }, [data]); + // Red Flag data 로딩 React.useEffect(() => { const loadRedFlagData = async () => { @@ -141,6 +175,8 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac setRowAction, gtcData, isLoadingGtcData, + agreementCommentData, + isLoadingAgreementCommentData, redFlagData, isLoadingRedFlagData, redFlagResolutionData, @@ -148,7 +184,7 @@ export function BasicContractsDetailTable({ templateId, promises }: BasicContrac isComplianceTemplate, router }), - [setRowAction, gtcData, isLoadingGtcData, redFlagData, isLoadingRedFlagData, redFlagResolutionData, isLoadingRedFlagResolutionData, isComplianceTemplate, router] + [setRowAction, gtcData, isLoadingGtcData, agreementCommentData, isLoadingAgreementCommentData, redFlagData, isLoadingRedFlagData, redFlagResolutionData, isLoadingRedFlagResolutionData, isComplianceTemplate, router] ) const advancedFilterFields: DataTableAdvancedFilterField<BasicContractView>[] = [ diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx index 662d7ea9..e5aab10d 100644 --- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx @@ -126,17 +126,18 @@ const canCompleteCurrentContract = React.useMemo(() => { const isComplianceTemplate = selectedContract.templateName?.includes('준법'); const isGTCTemplate = selectedContract.templateName?.includes('GTC'); + const requiresNegotiationComplete = isComplianceTemplate || isGTCTemplate; const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true; - // GTC 체크 수정 - const gtcStatus = gtcCommentStatus[contractId]; - const gtcCompleted = isGTCTemplate ? - (!gtcStatus?.hasComments || gtcStatus?.isComplete === true) : true; + const negotiationStatus = gtcCommentStatus[contractId]; + const negotiationCleared = requiresNegotiationComplete + ? (!negotiationStatus?.hasComments || negotiationStatus?.isComplete === true) + : true; const signatureCompleted = signatureStatus[contractId] === true; - return surveyCompleted && gtcCompleted && signatureCompleted; + return surveyCompleted && negotiationCleared && signatureCompleted; }, [selectedContract, surveyCompletionStatus, signatureStatus, gtcCommentStatus, isBuyerMode]); @@ -341,7 +342,11 @@ const canCompleteCurrentContract = React.useMemo(() => { const isComplianceTemplate = selectedContract.templateName?.includes('준법'); const isGTCTemplate = selectedContract.templateName?.includes('GTC'); const surveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contractId] === true : true; - const gtcCompleted = isGTCTemplate ? (gtcCommentStatus[contractId]?.isComplete !== true) : true; + const requiresNegotiationComplete = isComplianceTemplate || isGTCTemplate; + const negotiationStatus = gtcCommentStatus[contractId]; + const negotiationCleared = requiresNegotiationComplete + ? (!negotiationStatus?.hasComments || negotiationStatus?.isComplete === true) + : true; const signatureCompleted = signatureStatus[contractId] === true; if (!surveyCompleted) { @@ -353,7 +358,7 @@ const canCompleteCurrentContract = React.useMemo(() => { return; } - if (!gtcCompleted) { + if (!negotiationCleared) { toast({ title: "코멘트가 있어 서명할 수 없습니다.", description: "협의 코멘트 탭에서 모든 코멘트를 삭제하거나 협의를 완료해주세요.", @@ -705,9 +710,13 @@ const canCompleteCurrentContract = React.useMemo(() => { // 계약서별 완료 상태 확인 const isComplianceTemplate = contract.templateName?.includes('준법'); - const isGTCTemplate = contract.templateName?.includes('GTC'); + const isGTCTemplate = contract.templateName?.includes('GTC'); + const requiresNegotiation = isComplianceTemplate || isGTCTemplate; const hasSurveyCompleted = isComplianceTemplate ? surveyCompletionStatus[contract.id] === true : true; - const hasGtcCompleted = isGTCTemplate ? (gtcCommentStatus[contract.id]?.hasComments !== true) : true; + const negotiationStatus = gtcCommentStatus[contract.id]; + const hasNegotiationCompleted = requiresNegotiation + ? (!negotiationStatus?.hasComments || negotiationStatus?.isComplete === true) + : true; const hasSignatureCompleted = signatureStatus[contract.id] === true; return ( @@ -776,9 +785,9 @@ const canCompleteCurrentContract = React.useMemo(() => { 설문 </span> )} - {isGTCTemplate && ( - <span className={`flex items-center ${hasGtcCompleted ? 'text-green-600' : 'text-red-600'}`}> - <CheckCircle2 className={`h-3 w-3 mr-1 ${hasGtcCompleted ? 'text-green-500' : 'text-red-500'}`} /> + {requiresNegotiation && ( + <span className={`flex items-center ${hasNegotiationCompleted ? 'text-green-600' : 'text-red-600'}`}> + <CheckCircle2 className={`h-3 w-3 mr-1 ${hasNegotiationCompleted ? 'text-green-500' : 'text-red-500'}`} /> 협의 </span> )} @@ -953,11 +962,20 @@ const canCompleteCurrentContract = React.useMemo(() => { 설문조사 {surveyCompletionStatus[selectedContract.id] ? '완료' : '미완료'} </span> )} - {selectedContract.templateName?.includes('GTC') && ( - <span className={`flex items-center ${(gtcCommentStatus[selectedContract.id]?.isComplete === true) ? 'text-green-600' : 'text-red-600'}`}> - <CheckCircle2 className={`h-3 w-3 mr-1 ${(gtcCommentStatus[selectedContract.id]?.isComplete === true) ? 'text-green-500' : 'text-red-500'}`} /> - 협의 {(gtcCommentStatus[selectedContract.id]?.isComplete === true) ? '완료' : - `미완료 (코멘트 ${gtcCommentStatus[selectedContract.id]?.commentCount || 0}개)`} + {(selectedContract.templateName?.includes('GTC') || selectedContract.templateName?.includes('준법')) && ( + <span className={`flex items-center ${ + (gtcCommentStatus[selectedContract.id]?.isComplete === true) || !gtcCommentStatus[selectedContract.id]?.hasComments + ? 'text-green-600' + : 'text-red-600' + }`}> + <CheckCircle2 className={`h-3 w-3 mr-1 ${ + (gtcCommentStatus[selectedContract.id]?.isComplete === true) || !gtcCommentStatus[selectedContract.id]?.hasComments + ? 'text-green-500' + : 'text-red-500' + }`} /> + 협의 {(!gtcCommentStatus[selectedContract.id]?.hasComments || gtcCommentStatus[selectedContract.id]?.isComplete === true) + ? '완료' + : `미완료 (코멘트 ${gtcCommentStatus[selectedContract.id]?.commentCount || 0}개)`} </span> )} <span className={`flex items-center ${signatureStatus[selectedContract.id] ? 'text-green-600' : 'text-red-600'}`}> diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx index 54e9c18c..6bf0dfb1 100644 --- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx +++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx @@ -745,6 +745,7 @@ export function BasicContractSignViewer({ const isComplianceTemplate = mode === 'buyer' ? false : templateName.includes('준법'); const isNDATemplate = mode === 'buyer' ? false : (templateName.includes('비밀유지') || templateName.includes('NDA')); const isGTCTemplate = mode === 'buyer' ? false : templateName.includes('GTC'); + const hasNegotiationTab = mode === 'buyer' ? false : (isGTCTemplate || isComplianceTemplate); const allFiles: FileInfo[] = React.useMemo(() => { const files: FileInfo[] = []; @@ -780,7 +781,7 @@ export function BasicContractSignViewer({ }); } - if (isGTCTemplate) { + if (hasNegotiationTab) { files.push({ path: "", name: "협의 코멘트", @@ -789,7 +790,7 @@ export function BasicContractSignViewer({ } return files; - }, [filePath, additionalFiles, templateName, isComplianceTemplate, isGTCTemplate, mode]); + }, [filePath, additionalFiles, templateName, isComplianceTemplate, hasNegotiationTab, mode]); const cleanupHtmlStyle = () => { const elements = document.querySelectorAll('.Document_container'); @@ -982,7 +983,7 @@ export function BasicContractSignViewer({ if (!isWidgetSignature) { if (annot.Subject === 'Signature') { // 지정된 서명란 외 서명 → 즉시 삭제 - annotationManager.deleteAnnotation(annot, false); + annotationManager.deleteAnnotation(annot, { imported: false }); } continue; } @@ -994,7 +995,7 @@ export function BasicContractSignViewer({ if (name && allowed.length > 0 && !allowed.includes(name)) { // 우리가 만든 서명 필드가 아니면 막기 - annotationManager.deleteAnnotation(annot, false); + annotationManager.deleteAnnotation(annot, { imported: false }); continue; } @@ -1285,7 +1286,7 @@ export function BasicContractSignViewer({ return; } - if (isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete) { + if (hasNegotiationTab && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete) { toast({ title: "미해결 코멘트가 있어 서명할 수 없습니다.", description: "모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요.", @@ -1555,7 +1556,7 @@ export function BasicContractSignViewer({ {mode !== 'buyer' && isComplianceTemplate && ( <span className="block mt-1 text-amber-600">📋 준법 설문조사를 먼저 완료해주세요.</span> )} - {mode !== 'buyer' && isGTCTemplate && ( + {mode !== 'buyer' && hasNegotiationTab && ( <span className="block mt-1 text-blue-600">📋 협의 코멘트를 확인하고 모든 협의가 완료되었는지 확인해주세요.</span> )} {hasSignatureFields && !isAutoSignProcessing && ( @@ -1568,12 +1569,12 @@ export function BasicContractSignViewer({ ✅ {mode === 'buyer' ? '승인이' : '서명이'} 완료되었습니다. </span> )} - {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete && ( + {mode !== 'buyer' && hasNegotiationTab && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete && ( <span className="block mt-1 text-red-600"> ⚠️ {gtcCommentStatus.commentCount}개의 미해결 코멘트가 있어 서명할 수 없습니다. </span> )} - {mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && gtcCommentStatus.isComplete && ( + {mode !== 'buyer' && hasNegotiationTab && gtcCommentStatus.hasComments && gtcCommentStatus.isComplete && ( <span className="block mt-1 text-green-600"> ✅ 협의가 완료되어 서명 가능합니다. </span> |
