summaryrefslogtreecommitdiff
path: root/lib/basic-contract
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract')
-rw-r--r--lib/basic-contract/actions/check-red-flag-resolution.ts82
-rw-r--r--lib/basic-contract/agreement-comments/actions.ts76
-rw-r--r--lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx16
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx74
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-table.tsx46
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx52
-rw-r--r--lib/basic-contract/viewer/basic-contract-sign-viewer.tsx17
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>