diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-01 09:12:09 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-01 09:12:09 +0000 |
| commit | 18954df6565108a469fb1608ea3715dd9bb1b02d (patch) | |
| tree | 2675d254c547861a903a32459d89283a324e0e0d /lib/basic-contract/service.ts | |
| parent | f91cd16a872d9cda04aeb5c4e31538e3e2bd1895 (diff) | |
(대표님) 구매 기본계약, gtc 개발
Diffstat (limited to 'lib/basic-contract/service.ts')
| -rw-r--r-- | lib/basic-contract/service.ts | 556 |
1 files changed, 500 insertions, 56 deletions
diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts index 194d27eb..8189381b 100644 --- a/lib/basic-contract/service.ts +++ b/lib/basic-contract/service.ts @@ -31,7 +31,8 @@ import { type GtcVendorClause,
type GtcClause,
projects,
- legalWorks
+ legalWorks,
+ BasicContractView, users
} from "@/db/schema";
import path from "path";
@@ -39,6 +40,9 @@ import { GetBasicContractTemplatesSchema,
CreateBasicContractTemplateSchema,
GetBasciContractsSchema,
+ GetBasciContractsVendorSchema,
+ GetBasciContractsByIdSchema,
+ updateStatusSchema,
} from "./validations";
import { readFile } from "fs/promises"
@@ -689,8 +693,8 @@ export async function getBasicContractsByVendorId( input: GetBasciContractsVendorSchema,
vendorId: number
) {
- // return unstable_cache(
- // async () => {
+ return unstable_cache(
+ async () => {
try {
const offset = (input.page - 1) * input.perPage;
@@ -757,13 +761,13 @@ export async function getBasicContractsByVendorId( // 에러 발생 시 디폴트
return { data: [], pageCount: 0 };
}
- // },
- // [JSON.stringify(input), String(vendorId)], // 캐싱 키에 vendorId 추가
- // {
- // revalidate: 3600,
- // tags: ["basicContractView-vendor"], // revalidateTag("basicContractView") 호출 시 무효화
- // }
- // )();
+ },
+ [JSON.stringify(input), String(vendorId)], // 캐싱 키에 vendorId 추가
+ {
+ revalidate: 3600,
+ tags: ["basicContractView-vendor"], // revalidateTag("basicContractView") 호출 시 무효화
+ }
+ )();
}
@@ -2115,10 +2119,10 @@ export async function getVendorGtcData(contractId?: number): Promise<GtcVendorDa isSubtitleModified: gtcVendorClauses.isSubtitleModified,
isContentModified: gtcVendorClauses.isContentModified,
})
- .from(gtcClauses)
+ .from(gtcClauses)
.leftJoin(gtcVendorClauses, and(
eq(gtcVendorClauses.baseClauseId, gtcClauses.id),
- vendorDocument.id ? eq(gtcVendorClauses.vendorDocumentId, vendorDocument.id) : sql`false`, // 벤더 문서가 없으면 조인하지 않음
+ vendorDocument.id ? eq(gtcVendorClauses.vendorDocumentId, vendorDocument.id) : sql`false`,
eq(gtcVendorClauses.isActive, true)
))
.where(
@@ -2129,23 +2133,57 @@ export async function getVendorGtcData(contractId?: number): Promise<GtcVendorDa )
.orderBy(gtcClauses.sortOrder);
+ let negotiationHistoryMap = new Map();
+
+ if (vendorDocument.id) {
+ const vendorClauseIds = clausesResult
+ .filter(c => c.vendorClauseId)
+ .map(c => c.vendorClauseId);
+
+ if (vendorClauseIds.length > 0) {
+ const histories = await db
+ .select({
+ vendorClauseId: gtcNegotiationHistory.vendorClauseId,
+ action: gtcNegotiationHistory.action,
+ previousStatus: gtcNegotiationHistory.previousStatus,
+ newStatus: gtcNegotiationHistory.newStatus,
+ comment: gtcNegotiationHistory.comment,
+ actorType: gtcNegotiationHistory.actorType,
+ actorId: gtcNegotiationHistory.actorId,
+ actorName: gtcNegotiationHistory.actorName,
+ actorEmail: gtcNegotiationHistory.actorEmail,
+ createdAt: gtcNegotiationHistory.createdAt,
+ changedFields: gtcNegotiationHistory.changedFields,
+ })
+ .from(gtcNegotiationHistory)
+ .leftJoin(users, eq(gtcNegotiationHistory.actorId, users.id))
+ .where(inArray(gtcNegotiationHistory.vendorClauseId, vendorClauseIds))
+ .orderBy(desc(gtcNegotiationHistory.createdAt));
+
+ // 벤더 조항별로 이력 그룹화
+ histories.forEach(history => {
+ if (!negotiationHistoryMap.has(history.vendorClauseId)) {
+ negotiationHistoryMap.set(history.vendorClauseId, []);
+ }
+ negotiationHistoryMap.get(history.vendorClauseId).push(history);
+ });
+ }
+ }
+
+
+
// 6. 데이터 변환 및 추가 정보 계산
- const clauses = clausesResult.map(clause => {
- // 벤더별 수정사항이 있는지 확인
+ const clauses = clausesResult.map(clause => {
const hasVendorData = !!clause.vendorClauseId;
+ const negotiationHistory = hasVendorData ?
+ (negotiationHistoryMap.get(clause.vendorClauseId) || []) : [];
- const hasModifications = hasVendorData && (
- clause.isNumberModified ||
- clause.isCategoryModified ||
- clause.isSubtitleModified ||
- clause.isContentModified
- );
-
- const hasComment = hasVendorData && !!clause.negotiationNote;
+ // 코멘트가 있는 이력들만 필터링
+ const commentHistory = negotiationHistory.filter(h => h.comment);
+ const latestComment = commentHistory[0]?.comment || null;
+ const hasComment = commentHistory.length > 0;
return {
- // 벤더 조항 ID (있는 경우만, 없으면 null)
- // id: clause.vendorClauseId,
id: clause.baseClauseId,
vendorClauseId: clause.vendorClauseId,
vendorDocumentId: hasVendorData ? clause.vendorDocumentId : null,
@@ -2174,15 +2212,16 @@ export async function getVendorGtcData(contractId?: number): Promise<GtcVendorDa baseContent: clause.baseContent,
// 수정 여부
- hasModifications,
+ // hasModifications,
isNumberModified: clause.isNumberModified || false,
isCategoryModified: clause.isCategoryModified || false,
isSubtitleModified: clause.isSubtitleModified || false,
isContentModified: clause.isContentModified || false,
- // 코멘트 관련
- hasComment,
- pendingComment: null, // 클라이언트에서 관리
+ hasComment,
+ latestComment,
+ commentHistory, // 전체 코멘트 이력
+ negotiationHistory, // 전체 협의 이력
};
});
@@ -2218,8 +2257,8 @@ interface VendorDocument { export async function updateVendorClause(
baseClauseId: number,
vendorClauseId: number | null,
- clauseData: ClauseUpdateData,
- vendorDocument?: VendorDocument
+ clauseData: any,
+ vendorDocument: any
): Promise<{ success: boolean; error?: string; vendorClauseId?: number; vendorDocumentId?: number }> {
try {
const session = await getServerSession(authOptions);
@@ -2228,10 +2267,10 @@ export async function updateVendorClause( }
const companyId = session.user.companyId;
- const vendorId = companyId; // companyId를 vendorId로 사용
+ const vendorId = companyId;
const userId = Number(session.user.id);
- // 1. 기본 조항 정보 가져오기 (비교용)
+ // 1. 기본 조항 정보 가져오기
const baseClause = await db.query.gtcClauses.findFirst({
where: eq(gtcClauses.id, baseClauseId),
});
@@ -2240,11 +2279,22 @@ export async function updateVendorClause( return { success: false, error: "기본 조항을 찾을 수 없습니다." };
}
- // 2. 벤더 문서 ID 확보 (없으면 생성)
+ // 2. 이전 코멘트 가져오기 (vendorClauseId가 있는 경우)
+ let previousComment = null;
+ if (vendorClauseId) {
+ const previousData = await db
+ .select({ comment: gtcVendorClauses.negotiationNote })
+ .from(gtcVendorClauses)
+ .where(eq(gtcVendorClauses.id, vendorClauseId))
+ .limit(1);
+
+ previousComment = previousData?.[0]?.comment || null;
+ }
+
+ // 3. 벤더 문서 ID 확보 (없으면 생성)
let finalVendorDocumentId = vendorDocument?.id;
if (!finalVendorDocumentId && vendorDocument) {
- // 벤더 문서 생성
const newVendorDoc = await db.insert(gtcVendorDocuments).values({
vendorId: vendorId,
baseDocumentId: vendorDocument.baseDocumentId,
@@ -2268,7 +2318,7 @@ export async function updateVendorClause( return { success: false, error: "벤더 문서 ID를 확보할 수 없습니다." };
}
- // 3. 수정 여부 확인
+ // 4. 수정 여부 확인
const isNumberModified = clauseData.itemNumber !== baseClause.itemNumber;
const isCategoryModified = clauseData.category !== baseClause.category;
const isSubtitleModified = clauseData.subtitle !== baseClause.subtitle;
@@ -2277,7 +2327,7 @@ export async function updateVendorClause( const hasAnyModifications = isNumberModified || isCategoryModified || isSubtitleModified || isContentModified;
const hasComment = !!(clauseData.comment?.trim());
- // 4. 벤더 조항 데이터 준비
+ // 5. 벤더 조항 데이터 준비
const vendorClauseData = {
vendorDocumentId: finalVendorDocumentId,
baseClauseId: baseClauseId,
@@ -2286,22 +2336,19 @@ export async function updateVendorClause( sortOrder: baseClause.sortOrder,
fullPath: baseClause.fullPath,
- // 수정된 값들 (수정되지 않았으면 null로 저장)
modifiedItemNumber: isNumberModified ? clauseData.itemNumber : null,
modifiedCategory: isCategoryModified ? clauseData.category : null,
modifiedSubtitle: isSubtitleModified ? clauseData.subtitle : null,
modifiedContent: isContentModified ? clauseData.content : null,
- // 수정 여부 플래그
isNumberModified,
isCategoryModified,
isSubtitleModified,
isContentModified,
- // 상태 정보
reviewStatus: (hasAnyModifications || hasComment) ? 'reviewing' : 'draft',
negotiationNote: clauseData.comment?.trim() || null,
- editReason: clauseData.comment?.trim() || null, // 수정 이유도 동일하게 저장
+ editReason: clauseData.comment?.trim() || null,
updatedAt: new Date(),
updatedById: userId,
@@ -2309,9 +2356,8 @@ export async function updateVendorClause( let finalVendorClauseId = vendorClauseId;
- // 5. 벤더 조항 생성 또는 업데이트
+ // 6. 벤더 조항 생성 또는 업데이트
if (vendorClauseId) {
- // 기존 벤더 조항 업데이트
await db
.update(gtcVendorClauses)
.set(vendorClauseData)
@@ -2319,7 +2365,6 @@ export async function updateVendorClause( console.log(`벤더 조항 업데이트: ${vendorClauseId}`);
} else {
- // 새 벤더 조항 생성
const newVendorClause = await db.insert(gtcVendorClauses).values({
...vendorClauseData,
createdById: userId,
@@ -2333,21 +2378,24 @@ export async function updateVendorClause( console.log(`새 벤더 조항 생성: ${finalVendorClauseId}`);
}
- // 6. 협의 이력에 기록
- if (hasAnyModifications || hasComment) {
- const historyAction = hasAnyModifications ? 'modified' : 'commented';
- const historyComment = hasAnyModifications
- ? `조항 수정: ${clauseData.comment || '수정 이유 없음'}`
- : clauseData.comment;
-
+ // 7. 협의 이력에 기록 (코멘트가 변경된 경우만)
+ if (clauseData.comment !== previousComment) {
await db.insert(gtcNegotiationHistory).values({
vendorClauseId: finalVendorClauseId,
- action: historyAction,
- comment: historyComment?.trim(),
- actorType: 'vendor',
- actorId: session.user.id,
+ action: previousComment ? "modified" : "commented",
+ comment: clauseData.comment || null,
+ previousStatus: null,
+ newStatus: 'reviewing',
+ actorType: "vendor",
+ actorId: userId,
actorName: session.user.name,
actorEmail: session.user.email,
+ changedFields: {
+ comment: {
+ from: previousComment,
+ to: clauseData.comment || null
+ }
+ }
});
}
@@ -2365,7 +2413,6 @@ export async function updateVendorClause( };
}
}
-
// 기존 함수는 호환성을 위해 유지하되, 새 함수를 호출하도록 변경
export async function updateVendorClauseComment(
clauseId: number,
@@ -2635,6 +2682,140 @@ export async function requestLegalReviewAction( }
}
+export async function resendContractsAction(contractIds: number[]) {
+ try {
+ // 세션 확인
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.id) {
+ throw new Error('인증이 필요합니다.')
+ }
+
+ // 계약서 정보 조회
+ const contracts = await db
+ .select({
+ id: basicContract.id,
+ vendorId: basicContract.vendorId,
+ fileName: basicContract.fileName,
+ deadline: basicContract.deadline,
+ status: basicContract.status,
+ createdAt: basicContract.createdAt,
+ })
+ .from(basicContract)
+ .where(inArray(basicContract.id, contractIds))
+
+ if (contracts.length === 0) {
+ throw new Error('발송할 계약서를 찾을 수 없습니다.')
+ }
+
+ // 각 계약서에 대해 이메일 발송
+ const emailPromises = contracts.map(async (contract) => {
+ // 벤더 정보 조회
+ const vendor = await db
+ .select({
+ id: vendors.id,
+ vendorName: vendors.vendorName,
+ vendorCode: vendors.vendorCode,
+ country: vendors.country,
+ email: vendors.email,
+ })
+ .from(vendors)
+ .where(eq(vendors.id, contract.vendorId!))
+ .limit(1)
+
+ if (!vendor[0]) {
+ console.error(`벤더를 찾을 수 없습니다: vendorId ${contract.vendorId}`)
+ return null
+ }
+
+ // 벤더 연락처 조회 (Primary 연락처 우선, 없으면 첫 번째 연락처)
+ const contacts = await db
+ .select({
+ contactName: vendorContacts.contactName,
+ contactEmail: vendorContacts.contactEmail,
+ isPrimary: vendorContacts.isPrimary,
+ })
+ .from(vendorContacts)
+ .where(eq(vendorContacts.vendorId, vendor[0].id))
+ .orderBy(vendorContacts.isPrimary)
+
+ // 이메일 수신자 결정 (Primary 연락처 > 첫 번째 연락처 > 벤더 기본 이메일)
+ const primaryContact = contacts.find(c => c.isPrimary)
+ const recipientEmail = primaryContact?.contactEmail || contacts[0]?.contactEmail || vendor[0].email
+ const recipientName = primaryContact?.contactName || contacts[0]?.contactName || vendor[0].vendorName
+
+ if (!recipientEmail) {
+ console.error(`이메일 주소를 찾을 수 없습니다: vendorId ${vendor[0].id}`)
+ return null
+ }
+
+ // 언어 결정 (한국 = 한글, 그 외 = 영어)
+ const isKorean = vendor[0].country === 'KR'
+ const template = isKorean ? 'contract-reminder-kr' : 'contract-reminder-en'
+ const subject = isKorean
+ ? '[eVCP] 계약서 서명 요청 리마인더'
+ : '[eVCP] Contract Signature Reminder'
+
+ // 마감일 포맷팅
+ const deadlineDate = new Date(contract.deadline)
+ const formattedDeadline = isKorean
+ ? `${deadlineDate.getFullYear()}년 ${deadlineDate.getMonth() + 1}월 ${deadlineDate.getDate()}일`
+ : deadlineDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
+
+ // 남은 일수 계산
+ const today = new Date()
+ today.setHours(0, 0, 0, 0)
+ const deadline = new Date(contract.deadline)
+ deadline.setHours(0, 0, 0, 0)
+ const daysRemaining = Math.ceil((deadline.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
+
+ // 이메일 발송
+ await sendEmail({
+ from: session.user.email,
+ to: recipientEmail,
+ subject,
+ template,
+ context: {
+ recipientName,
+ vendorName: vendor[0].vendorName,
+ vendorCode: vendor[0].vendorCode,
+ contractFileName: contract.fileName,
+ deadline: formattedDeadline,
+ daysRemaining,
+ senderName: session.user.name || session.user.email,
+ senderEmail: session.user.email,
+ // 계약서 링크 (실제 환경에 맞게 수정 필요)
+ contractLink: `${process.env.NEXT_PUBLIC_APP_URL}/contracts/${contract.id}`,
+ },
+ })
+
+ console.log(`리마인더 이메일 발송 완료: ${recipientEmail} (계약서 ID: ${contract.id})`)
+ return { contractId: contract.id, email: recipientEmail }
+ })
+
+ const results = await Promise.allSettled(emailPromises)
+
+ // 성공/실패 카운트
+ const successful = results.filter(r => r.status === 'fulfilled' && r.value !== null).length
+ const failed = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value === null)).length
+
+ if (failed > 0) {
+ console.warn(`${failed}건의 이메일 발송 실패`)
+ }
+
+ return {
+ success: true,
+ message: `${successful}건의 리마인더 이메일을 발송했습니다.`,
+ successful,
+ failed,
+ }
+
+ } catch (error) {
+ console.error('계약서 재발송 중 오류:', error)
+ throw new Error('계약서 재발송 중 오류가 발생했습니다.')
+ }
+}
+
+
export async function processBuyerSignatureAction(
contractId: number,
signedFileData: ArrayBuffer,
@@ -2962,4 +3143,267 @@ export async function getVendorSignatureFile() { error: error instanceof Error ? error.message : "파일을 읽는 중 오류가 발생했습니다."
}
}
-}
\ No newline at end of file +}
+
+
+
+
+// templateName에서 project code 추출
+function extractProjectCodeFromTemplateName(templateName: string): string | null {
+ if (!templateName.includes('GTC')) return null;
+ if (templateName.toLowerCase().includes('general')) return null;
+
+ // GTC 앞의 문자열을 추출
+ const gtcIndex = templateName.indexOf('GTC');
+ if (gtcIndex > 0) {
+ const beforeGTC = templateName.substring(0, gtcIndex).trim();
+ // 마지막 단어를 project code로 간주
+ const words = beforeGTC.split(/\s+/);
+ return words[words.length - 1];
+ }
+
+ return null;
+}
+
+// 단일 contract에 대한 GTC 정보 확인
+async function checkGTCCommentsForContract(
+ templateName: string,
+ vendorId: number
+): Promise<{ gtcDocumentId: number | null; hasComments: boolean }> {
+ try {
+ const projectCode = extractProjectCodeFromTemplateName(templateName);
+ let gtcDocumentId: number | null = null;
+
+ console.log(projectCode,"projectCode")
+
+ // 1. GTC Document ID 찾기
+ if (projectCode && projectCode.trim() !== '') {
+ // Project GTC인 경우
+ const project = await db
+ .select({ id: projects.id })
+ .from(projects)
+ .where(eq(projects.code, projectCode.trim()))
+ .limit(1)
+
+ if (project.length > 0) {
+ const projectGtcDoc = await db
+ .select({ id: gtcDocuments.id })
+ .from(gtcDocuments)
+ .where(
+ and(
+ eq(gtcDocuments.projectId, project[0].id),
+ eq(gtcDocuments.isActive, true)
+ )
+ )
+ .orderBy(desc(gtcDocuments.revision))
+ .limit(1)
+
+ if (projectGtcDoc.length > 0) {
+ gtcDocumentId = projectGtcDoc[0].id
+ }
+ }
+ } else {
+ // Standard GTC인 경우 (general 포함하거나 project code가 없는 경우)
+ const standardGtcDoc = await db
+ .select({ id: gtcDocuments.id })
+ .from(gtcDocuments)
+ .where(
+ and(
+ eq(gtcDocuments.type, "standard"),
+ eq(gtcDocuments.isActive, true),
+ isNull(gtcDocuments.projectId)
+ )
+ )
+ .orderBy(desc(gtcDocuments.revision))
+ .limit(1)
+
+ if (standardGtcDoc.length > 0) {
+ gtcDocumentId = standardGtcDoc[0].id
+ }
+ }
+
+ console.log(gtcDocumentId,"gtcDocumentId")
+
+ // GTC Document를 찾지 못한 경우
+ if (!gtcDocumentId) {
+ return { gtcDocumentId: null, hasComments: false };
+ }
+
+ // 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
+ };
+
+ } catch (error) {
+ console.error('Error checking GTC comments for contract:', error);
+ return { gtcDocumentId: null, hasComments: false };
+ }
+}
+
+// 전체 contract 리스트에 대해 GTC document ID와 comment 정보 수집
+export async function checkGTCCommentsForContracts(
+ contracts: BasicContractView[]
+): Promise<Record<number, { gtcDocumentId: number | null; hasComments: boolean }>> {
+ const gtcData: Record<number, { gtcDocumentId: number | null; hasComments: boolean }> = {};
+
+ // GTC가 포함된 contract만 필터링
+ const gtcContracts = contracts.filter(contract =>
+ contract.templateName?.includes('GTC')
+ );
+
+ if (gtcContracts.length === 0) {
+ return gtcData;
+ }
+
+ // Promise.all을 사용해서 병렬 처리
+ const checkPromises = gtcContracts.map(async (contract) => {
+ try {
+ const result = await checkGTCCommentsForContract(
+ contract.templateName!,
+ contract.vendorId!
+ );
+
+ return {
+ contractId: contract.id,
+ gtcDocumentId: result.gtcDocumentId,
+ hasComments: result.hasComments
+ };
+ } catch (error) {
+ console.error(`Error checking GTC for contract ${contract.id}:`, error);
+ return {
+ contractId: contract.id,
+ gtcDocumentId: null,
+ hasComments: false
+ };
+ }
+ });
+
+ const results = await Promise.all(checkPromises);
+
+ // 결과를 Record 형태로 변환
+ results.forEach(({ contractId, gtcDocumentId, hasComments }) => {
+ gtcData[contractId] = { gtcDocumentId, hasComments };
+ });
+
+ return gtcData;
+}
+
+
+
+export async function updateVendorDocumentStatus(
+ formData: FormData | {
+ status: string;
+ vendorDocumentId: number;
+ documentId: number;
+ vendorId: number;
+ }
+) {
+ try {
+ // 세션 확인
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ return { success: false, error: "인증되지 않은 사용자입니다." }
+ }
+
+ // 데이터 파싱
+ const rawData = formData instanceof FormData
+ ? {
+ status: formData.get("status") as string,
+ vendorDocumentId: Number(formData.get("vendorDocumentId")),
+ documentId: Number(formData.get("documentId")),
+ vendorId: Number(formData.get("vendorId")),
+ }
+ : formData
+
+ // 유효성 검사
+ const validatedData = updateStatusSchema.safeParse(rawData)
+ if (!validatedData.success) {
+ return { success: false, error: "유효하지 않은 데이터입니다." }
+ }
+
+ const { status, vendorDocumentId, documentId, vendorId } = validatedData.data
+
+ // 완료 상태로 변경 시, 모든 조항이 approved 상태인지 확인
+ if (status === "complete") {
+ // 승인되지 않은 조항 확인
+ const pendingClauses = await db
+ .select({ id: gtcVendorClauses.id })
+ .from(gtcVendorClauses)
+ .where(
+ and(
+ eq(gtcVendorClauses.vendorDocumentId, vendorDocumentId),
+ eq(gtcVendorClauses.isActive, true),
+ not(eq(gtcVendorClauses.reviewStatus, "approved")),
+ not(eq(gtcVendorClauses.isExcluded, true)) // 제외된 조항은 검사에서 제외
+ )
+ )
+ .limit(1)
+
+ if (pendingClauses.length > 0) {
+ return {
+ success: false,
+ error: "모든 조항이 승인되어야 협의 완료 처리가 가능합니다."
+ }
+ }
+ }
+
+ // 업데이트 실행
+ await db
+ .update(gtcVendorDocuments)
+ .set({
+ reviewStatus: status,
+ updatedAt: new Date(),
+ updatedById: Number(session.user.id),
+ // 완료 처리 시 협의 종료일 설정
+ ...(status === "complete" ? {
+ negotiationEndDate: new Date(),
+ approvalDate: new Date()
+ } : {})
+ })
+ .where(eq(gtcVendorDocuments.id, vendorDocumentId))
+
+ // 캐시 무효화
+ // revalidatePath(`/evcp/gtc/${documentId}?vendorId=${vendorId}`)
+
+ return { success: true }
+ } catch (error) {
+ console.error("Error updating vendor document status:", error)
+ return { success: false, error: "상태 업데이트 중 오류가 발생했습니다." }
+ }
+}
|
