summaryrefslogtreecommitdiff
path: root/lib/basic-contract
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-25 11:48:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-25 11:48:21 +0000
commit6160e8bd61360ada9e8e0574671c38292eaba9e7 (patch)
tree57857b8cd7f1b858deb68622d69feba11fda20a8 /lib/basic-contract
parent6fcb8eda80c5ccac7eb985d3efb2aaafa0711988 (diff)
(임수민) 준법/gtc 코멘트 수정
Diffstat (limited to 'lib/basic-contract')
-rw-r--r--lib/basic-contract/actions/check-gtc-comments.ts62
-rw-r--r--lib/basic-contract/agreement-comments/actions.ts6
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx46
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-table.tsx35
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx63
-rw-r--r--lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx9
6 files changed, 103 insertions, 118 deletions
diff --git a/lib/basic-contract/actions/check-gtc-comments.ts b/lib/basic-contract/actions/check-gtc-comments.ts
index 1ce998bc..6d523cb0 100644
--- a/lib/basic-contract/actions/check-gtc-comments.ts
+++ b/lib/basic-contract/actions/check-gtc-comments.ts
@@ -4,11 +4,9 @@ import db from "@/db/db";
import { eq, and, desc, isNull, isNotNull, ne, or } from "drizzle-orm";
import {
gtcDocuments,
- gtcVendorDocuments,
- gtcVendorClauses,
- gtcNegotiationHistory,
projects,
- BasicContractView
+ BasicContractView,
+ agreementComments
} from "@/db/schema";
/**
@@ -27,7 +25,7 @@ function extractProjectCodeFromTemplateName(templateName: string): string | null
// 프로젝트 코드는 보통 첫 번째 단어
if (words.length > 1 && words[words.length - 1].toLowerCase() === 'gtc') {
- return words[words.length - 1];
+ return words[0]; // 첫 번째 단어가 프로젝트 코드
}
return null;
@@ -105,14 +103,16 @@ async function checkGTCCommentsForContract(
// gtcDocumentId가 없어도 새로운 코멘트 시스템은 작동해야 함
if (basicContractId) {
console.log(`🔍 [checkGTCCommentsForContract] basicContractId: ${basicContractId} 로 코멘트 조회`);
- const { agreementComments } = await import("@/db/schema");
+ // 기존 방식과 동일하게 빈 코멘트는 제외
const newComments = await db
.select({ id: agreementComments.id })
.from(agreementComments)
.where(
and(
eq(agreementComments.basicContractId, basicContractId),
- eq(agreementComments.isDeleted, false)
+ eq(agreementComments.isDeleted, false),
+ isNotNull(agreementComments.comment),
+ ne(agreementComments.comment, '')
)
)
.limit(1);
@@ -130,51 +130,9 @@ async function checkGTCCommentsForContract(
console.log(`⚠️ [checkGTCCommentsForContract] basicContractId ${basicContractId}: agreementComments 없음`);
}
- // GTC Document를 찾지 못한 경우 (기존 방식도 체크할 수 없음)
- if (!gtcDocumentId) {
- console.log(`⚠️ [checkGTCCommentsForContract] gtcDocumentId null - 기존 방식 체크 불가`);
- return { gtcDocumentId: null, hasComments: false };
- }
-
- // 2-2. 기존 방식: gtcDocumentId로 해당 벤더의 vendor documents 찾기
- const vendorDocuments = await db
- .select({ id: gtcVendorDocuments.id })
- .from(gtcVendorDocuments)
- .where(
- and(
- eq(gtcVendorDocuments.baseDocumentId, gtcDocumentId),
- eq(gtcVendorDocuments.vendorId, vendorId),
- eq(gtcVendorDocuments.isActive, true)
- )
- )
- .limit(1)
-
- if (vendorDocuments.length === 0) {
- return { gtcDocumentId, hasComments: false };
- }
-
- // vendor document에 연결된 clauses에서 negotiation history 확인
- const commentsExist = await db
- .select({ count: gtcNegotiationHistory.id })
- .from(gtcNegotiationHistory)
- .innerJoin(
- gtcVendorClauses,
- eq(gtcNegotiationHistory.vendorClauseId, gtcVendorClauses.id)
- )
- .where(
- and(
- eq(gtcVendorClauses.vendorDocumentId, vendorDocuments[0].id),
- eq(gtcVendorClauses.isActive, true),
- isNotNull(gtcNegotiationHistory.comment),
- ne(gtcNegotiationHistory.comment, '')
- )
- )
- .limit(1)
-
- return {
- gtcDocumentId,
- hasComments: commentsExist.length > 0
- };
+ // GTC 조항 기능은 더 이상 사용하지 않으므로, agreement_comments만 체크
+ // agreement_comments에 코멘트가 없으면 hasComments = false 반환
+ return { gtcDocumentId, hasComments: false };
} catch (error) {
console.error('Error checking GTC comments for contract:', error);
diff --git a/lib/basic-contract/agreement-comments/actions.ts b/lib/basic-contract/agreement-comments/actions.ts
index 2f60c3d8..32e9ce4c 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, inArray, sql } from "drizzle-orm";
+import { eq, and, desc, inArray, sql, isNotNull, ne } from "drizzle-orm";
import { agreementComments, basicContract, vendors, users } from "@/db/schema";
import { saveFile, deleteFile } from "@/lib/file-stroage";
import { sendEmail } from "@/lib/mail/sendEmail";
@@ -62,7 +62,9 @@ export async function getAgreementComments(
.where(
and(
eq(agreementComments.basicContractId, basicContractId),
- eq(agreementComments.isDeleted, false)
+ eq(agreementComments.isDeleted, false),
+ isNotNull(agreementComments.comment),
+ ne(agreementComments.comment, '')
)
)
.orderBy(desc(agreementComments.createdAt));
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 d03d0720..047866f7 100644
--- a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx
+++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx
@@ -172,25 +172,8 @@ export function getDetailColumns({
<Mail className="mr-2 h-4 w-4" />
재발송
</DropdownMenuItem>
- <DropdownMenuItem onClick={async () => {
- // 준법서약 템플릿인 경우 compliance 응답 페이지로 이동
- if (contract.templateName?.includes('준법')) {
- try {
- const response = await getComplianceResponseByBasicContractId(contract.id);
-
- if (response) {
- router.push(`/evcp/compliance/${response.templateId}/responses/${response.id}`);
- } else {
- toast.error("준법서약 응답을 찾을 수 없습니다.");
- setRowAction({ type: "view", row });
- }
- } catch (error) {
- console.error("Error fetching compliance response:", error);
- toast.error("응답 정보를 가져오는데 실패했습니다.");
- }
- } else {
- setRowAction({ type: "view", row });
- }
+ <DropdownMenuItem onClick={() => {
+ setRowAction({ type: "view", row });
}}>
<FileText className="mr-2 h-4 w-4" />
상세 정보
@@ -327,19 +310,20 @@ export function getDetailColumns({
const handleOpenGTC = (e: React.MouseEvent) => {
e.stopPropagation()
- // gtcDocumentId가 있으면 그걸 사용, 없으면 templateId 사용
- const documentIdToUse = contractGtcData?.gtcDocumentId || contract.templateId
-
- if (documentIdToUse && contract.vendorId) {
- const gtcUrl = `/evcp/basic-contract/vendor-gtc/${documentIdToUse}?vendorId=${contract.vendorId}&vendorName=${encodeURIComponent(contract.vendorName || '')}&contractId=${contract.id}&templateId=${contract.templateId}`
- window.open(gtcUrl, '_blank')
- } else {
- console.error('GTC 페이지를 열 수 없습니다:', {
- gtcDocumentId: contractGtcData?.gtcDocumentId,
- templateId: contract.templateId,
- vendorId: contract.vendorId
- })
+ // 상세보기와 동일하게 contract.id를 경로 파라미터로 사용
+ 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 gtcUrl = `/evcp/basic-contract/vendor-gtc/${contract.id}${query ? `?${query}` : ""}`;
+ window.open(gtcUrl, '_blank');
}
return (
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 a2e1c5e4..9c5da894 100644
--- a/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx
+++ b/lib/basic-contract/status-detail/basic-contracts-detail-table.tsx
@@ -235,13 +235,36 @@ type RedFlagResolutionState = {
// GTC 템플릿인 경우 GTC 협의 페이지로 이동
const contract = rowAction.row.original;
if (contract.templateName?.includes('GTC')) {
- const contractGtcData = gtcData[contract.id];
- if (contractGtcData?.gtcDocumentId) {
- const gtcUrl = `/evcp/basic-contract/vendor-gtc/${contractGtcData.gtcDocumentId}?vendorId=${contract.vendorId}&vendorName=${encodeURIComponent(contract.vendorName || '')}&contractId=${contract.id}&templateId=${contract.templateId}`;
- router.push(gtcUrl);
- } else {
- toast.error("GTC 문서 정보를 찾을 수 없습니다.");
+ // GTC 템플릿인 경우 GTC 협의 페이지로 이동
+ // 준법과 동일하게 contract.id를 경로 파라미터로 사용
+ 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 gtcUrl = `/evcp/basic-contract/vendor-gtc/${contract.id}${query ? `?${query}` : ""}`;
+ router.push(gtcUrl);
+ } else if (contract.templateName?.includes('준법')) {
+ // 준법 템플릿인 경우 준법 코멘트 페이지로 이동
+ 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}` : ""}`;
+ router.push(complianceUrl);
} else {
// 일반 계약서인 경우: 상세 정보를 보여주는 기능
// 현재는 준비 중이지만, 향후 다이얼로그나 시트를 열 수 있음
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 e5aab10d..407a3c4d 100644
--- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
+++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx
@@ -142,13 +142,20 @@ const canCompleteCurrentContract = React.useMemo(() => {
// 계약서별 상태 초기화
+ // Vendor signed 상태의 계약서도 포함하여 초기화
React.useEffect(() => {
if (contracts.length > 0 && contractStatuses.length === 0) {
setContractStatuses(
- contracts.map(contract => ({
- id: contract.id,
- status: 'pending' as const
- }))
+ contracts.map(contract => {
+ // 이미 서명된 계약서는 vendor_signed 상태로 초기화
+ const isSigned = contract.vendorSignedAt ||
+ contract.status === "COMPLETED" ||
+ contract.status === "VENDOR_SIGNED";
+ return {
+ id: contract.id,
+ status: isSigned ? ('vendor_signed' as const) : ('pending' as const)
+ };
+ })
);
}
}, [contracts, contractStatuses.length]);
@@ -705,7 +712,7 @@ const canCompleteCurrentContract = React.useMemo(() => {
<div className="space-y-2">
{filteredContracts.map((contract) => {
const contractStatus = contractStatuses.find(status => status.id === contract.id);
- const isCompleted = contractStatus?.status === 'completed';
+ const isCompleted = contractStatus?.status === 'completed' || contractStatus?.status === 'vendor_signed';
const hasError = contractStatus?.status === 'error';
// 계약서별 완료 상태 확인
@@ -732,7 +739,8 @@ const canCompleteCurrentContract = React.useMemo(() => {
!isCompleted && !hasError && "hover:bg-blue-50 hover:border-blue-200"
)}
onClick={() => handleSelectContract(contract)}
- disabled={isCompleted}
+ // Vendor signed 상태에서도 코멘트를 볼 수 있도록 비활성화하지 않음
+ disabled={false}
>
<div className="flex flex-col w-full space-y-1">
{/* 첫 번째 줄: 제목 + 상태 */}
@@ -851,7 +859,7 @@ const canCompleteCurrentContract = React.useMemo(() => {
{selectedContract.templateName || t("basicContracts.dialog.document")}
{/* 현재 계약서 상태 표시 */}
- {currentContractStatus?.status === 'completed' ? (
+ {(currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed') ? (
<Badge variant="outline" className="ml-2 bg-green-50 text-green-700 border-green-200">
<CheckCircle2 className="h-3 w-3 mr-1" />
{isBuyerMode ? "승인 완료" : "서명 완료"}
@@ -931,10 +939,10 @@ const canCompleteCurrentContract = React.useMemo(() => {
<div className="p-4 flex justify-between items-center bg-gray-50 border-t flex-shrink-0">
<div className="flex items-center space-x-4">
{/* 현재 계약서가 완료된 경우 */}
- {currentContractStatus?.status === 'completed' ? (
+ {currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed' ? (
<p className="text-sm text-green-600 flex items-center">
<CheckCircle2 className="h-4 w-4 text-green-500 mr-1" />
- 이 계약서는 이미 {isBuyerMode ? "승인이" : "서명이"} 완료되었습니다
+ 이 계약서는 이미 {isBuyerMode ? "승인이" : "서명이"} 완료되었습니다. 코멘트를 확인할 수 있습니다.
</p>
) : currentContractStatus?.status === 'error' ? (
<p className="text-sm text-red-600 flex items-center">
@@ -1015,22 +1023,27 @@ const canCompleteCurrentContract = React.useMemo(() => {
<Trophy className="h-4 w-4" />
모든 {isBuyerMode ? "승인" : "서명"} 완료
</Button>
- ) : currentContractStatus?.status === 'completed' ? (
- // 현재 계약서가 완료된 경우
- <Button
- variant="outline"
- className="gap-2"
- onClick={() => {
- const nextContract = getNextPendingContract();
- if (nextContract) {
- setSelectedContract(nextContract);
- }
- }}
- disabled={!getNextPendingContract()}
- >
- <ArrowRight className="h-4 w-4" />
- 다음 계약서
- </Button>
+ ) : (currentContractStatus?.status === 'completed' || currentContractStatus?.status === 'vendor_signed') ? (
+ // 현재 계약서가 완료된 경우 - 코멘트 확인만 가능
+ <div className="flex items-center gap-2">
+ <Button
+ variant="outline"
+ className="gap-2"
+ onClick={() => {
+ const nextContract = getNextPendingContract();
+ if (nextContract) {
+ setSelectedContract(nextContract);
+ }
+ }}
+ disabled={!getNextPendingContract()}
+ >
+ <ArrowRight className="h-4 w-4" />
+ 다음 계약서
+ </Button>
+ <p className="text-sm text-gray-500">
+ 서명 완료 - 코멘트 확인 가능
+ </p>
+ </div>
) : (
// 현재 계약서를 서명해야 하는 경우
<Button
diff --git a/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx b/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx
index 1fc6fe6b..7bab0ad5 100644
--- a/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx
+++ b/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx
@@ -27,13 +27,18 @@ export function BasicContractTableToolbarActions({ table }: TemplateTableToolbar
return translated === key ? fallback : translated;
}, [t, ready]);
- // PENDING 상태인 선택된 계약서들
+ // PENDING 또는 COMPLETED(VENDOR_SIGNED) 상태인 선택된 계약서들
+ // Vendor signed 상태에서도 코멘트를 확인할 수 있도록 포함
const pendingContracts = React.useMemo(() => {
return table
.getFilteredSelectedRowModel()
.rows
.map(row => row.original)
- .filter(contract => contract.status === "PENDING");
+ .filter(contract =>
+ contract.status === "PENDING" ||
+ contract.status === "COMPLETED" ||
+ contract.status === "VENDOR_SIGNED"
+ );
}, [table.getFilteredSelectedRowModel().rows]);
// 선택된 행이 있는지 확인