From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/general-contracts/service.ts | 1102 ++++++++++++++++++++++++++++++++------ 1 file changed, 950 insertions(+), 152 deletions(-) (limited to 'lib/general-contracts/service.ts') diff --git a/lib/general-contracts/service.ts b/lib/general-contracts/service.ts index 2422706a..77593f29 100644 --- a/lib/general-contracts/service.ts +++ b/lib/general-contracts/service.ts @@ -9,7 +9,7 @@ import { generalContracts, generalContractItems, generalContractAttachments } fr import { contracts, contractItems, contractEnvelopes, contractSigners } from '@/db/schema/contract' import { basicContract, basicContractTemplates } from '@/db/schema/basicContractDocumnet' import { vendors } from '@/db/schema/vendors' -import { users } from '@/db/schema/users' +import { users, roles, userRoles } from '@/db/schema/users' import { projects } from '@/db/schema/projects' import { items } from '@/db/schema/items' import { filterColumns } from '@/lib/filter-columns' @@ -225,10 +225,6 @@ export async function getGeneralContracts(input: GetGeneralContractsSchema) { vendorId: generalContracts.vendorId, vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, - // Project info - projectId: generalContracts.projectId, - projectName: projects.name, - projectCode: projects.code, // User info managerName: users.name, lastUpdatedByName: users.name, @@ -236,7 +232,6 @@ export async function getGeneralContracts(input: GetGeneralContractsSchema) { .from(generalContracts) .leftJoin(vendors, eq(generalContracts.vendorId, vendors.id)) .leftJoin(users, eq(generalContracts.registeredById, users.id)) - .leftJoin(projects, eq(generalContracts.projectId, projects.id)) .where(finalWhere) .orderBy(...orderByColumns) .limit(input.perPage) @@ -287,13 +282,9 @@ export async function getContractById(id: number) { .from(vendors) .where(eq(vendors.id, contract[0].vendorId)) .limit(1) - - // Get project info - const project = contract[0].projectId ? await db - .select() - .from(projects) - .where(eq(projects.id, contract[0].projectId)) - .limit(1) : null + + // vendor의 country 정보 가져오기 (없으면 기본값 'KR') + const vendorCountry = vendor[0]?.country || 'KR' // Get manager info const manager = await db @@ -309,9 +300,7 @@ export async function getContractById(id: number) { vendor: vendor[0] || null, vendorCode: vendor[0]?.vendorCode || null, vendorName: vendor[0]?.vendorName || null, - project: project ? project[0] : null, - projectName: project ? project[0].name : null, - projectCode: project ? project[0].code : null, + vendorCountry: vendorCountry, manager: manager[0] || null } } catch (error) { @@ -392,7 +381,6 @@ export async function createContract(data: Record) { executionMethod: data.executionMethod as string, name: data.name as string, vendorId: data.vendorId as number, - projectId: data.projectId as number, startDate: data.startDate as string, endDate: data.endDate as string, validityEndDate: data.validityEndDate as string, @@ -424,10 +412,6 @@ export async function createContract(data: Record) { contractTerminationConditions: data.contractTerminationConditions || {}, terms: data.terms || {}, complianceChecklist: data.complianceChecklist || {}, - communicationChannels: data.communicationChannels || {}, - locations: data.locations || {}, - fieldServiceRates: data.fieldServiceRates || {}, - offsetDetails: data.offsetDetails || {}, totalAmount: data.totalAmount as number, availableBudget: data.availableBudget as number, registeredById: data.registeredById as number, @@ -451,6 +435,7 @@ export async function updateContractBasicInfo(id: number, data: Record = { + contractScope, specificationType, specificationManualText, unitPriceType, @@ -531,6 +519,8 @@ export async function updateContractBasicInfo(id: number, data: Record) => ({ contractId, + projectId: item.projectId ? (item.projectId as number) : null, itemCode: item.itemCode as string, itemInfo: item.itemInfo as string, specification: item.specification as string, @@ -829,7 +848,8 @@ export async function getBasicInfo(contractId: number) { contractEstablishmentConditions: contract.contractEstablishmentConditions, interlockingSystem: contract.interlockingSystem, mandatoryDocuments: contract.mandatoryDocuments, - contractTerminationConditions: contract.contractTerminationConditions + contractTerminationConditions: contract.contractTerminationConditions, + externalYardEntry: contract.externalYardEntry || 'N' } } } catch (error) { @@ -838,87 +858,6 @@ export async function getBasicInfo(contractId: number) { } } - -export async function getCommunicationChannel(contractId: number) { - try { - const [contract] = await db - .select({ - communicationChannels: generalContracts.communicationChannels - }) - .from(generalContracts) - .where(eq(generalContracts.id, contractId)) - .limit(1) - - if (!contract) { - return null - } - - return contract.communicationChannels as any - } catch (error) { - console.error('Error getting communication channel:', error) - throw new Error('Failed to get communication channel') - } -} - -export async function updateCommunicationChannel(contractId: number, communicationData: Record, userId: number) { - try { - await db - .update(generalContracts) - .set({ - communicationChannels: communicationData, - lastUpdatedAt: new Date(), - lastUpdatedById: userId - }) - .where(eq(generalContracts.id, contractId)) - - revalidatePath('/general-contracts') - return { success: true } - } catch (error) { - console.error('Error updating communication channel:', error) - throw new Error('Failed to update communication channel') - } -} - -export async function updateLocation(contractId: number, locationData: Record, userId: number) { - try { - await db - .update(generalContracts) - .set({ - locations: locationData, - lastUpdatedAt: new Date(), - lastUpdatedById: userId - }) - .where(eq(generalContracts.id, contractId)) - - revalidatePath('/general-contracts') - return { success: true } - } catch (error) { - console.error('Error updating location:', error) - throw new Error('Failed to update location') - } -} - -export async function getLocation(contractId: number) { - try { - const [contract] = await db - .select({ - locations: generalContracts.locations - }) - .from(generalContracts) - .where(eq(generalContracts.id, contractId)) - .limit(1) - - if (!contract) { - return null - } - - return contract.locations as any - } catch (error) { - console.error('Error getting location:', error) - throw new Error('Failed to get location') - } -} - export async function updateContract(id: number, data: Record) { try { // 숫자 필드에서 빈 문자열을 null로 변환 @@ -990,7 +929,7 @@ export async function updateContract(id: number, data: Record) .insert(generalContractItems) .values( data.contractItems.map((item: any) => ({ - project: item.project, + projectId: item.projectId ? (item.projectId as number) : null, itemCode: item.itemCode, itemInfo: item.itemInfo, specification: item.specification, @@ -1452,6 +1391,49 @@ export async function sendContractApprovalRequest( signerStatus: 'PENDING', }) + // 사외업체 야드투입이 'Y'인 경우 안전담당자 자동 지정 + if (contractSummary.basicInfo?.externalYardEntry === 'Y') { + try { + // 안전담당자 역할을 가진 사용자 조회 (역할명에 '안전' 또는 'safety' 포함) + const safetyManagers = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + }) + .from(users) + .innerJoin(userRoles, eq(users.id, userRoles.userId)) + .innerJoin(roles, eq(userRoles.roleId, roles.id)) + .where( + and( + or( + like(roles.name, '%안전%'), + like(roles.name, '%safety%'), + like(roles.name, '%Safety%') + ), + eq(users.isActive, true) + ) + ) + .limit(1) + + // 첫 번째 안전담당자를 자동 추가 + if (safetyManagers.length > 0) { + const safetyManager = safetyManagers[0] + await db.insert(contractSigners).values({ + envelopeId: newEnvelope.id, + signerType: 'SAFETY_MANAGER', + signerEmail: safetyManager.email || '', + signerName: safetyManager.name || '안전담당자', + signerPosition: '안전담당자', + signerStatus: 'PENDING', + }) + } + } catch (error) { + console.error('Error adding safety manager:', error) + // 안전담당자 추가 실패해도 계약 승인 요청은 계속 진행 + } + } + // generalContractAttachments에 contractId 업데이트 (일반계약의 첨부파일들을 PO 계약과 연결) const generalContractId = contractSummary.basicInfo?.id || contractSummary.id if (generalContractId) { @@ -1705,93 +1687,142 @@ async function mapContractSummaryToDb(contractSummary: any) { } } -// Field Service Rate 관련 서버 액션들 -export async function getFieldServiceRate(contractId: number) { + +// 계약번호 생성 함수 +// 임치계약 정보 조회 +export async function getStorageInfo(contractId: number) { try { - const result = await db - .select({ fieldServiceRates: generalContracts.fieldServiceRates }) + const contract = await db + .select({ terms: generalContracts.terms }) .from(generalContracts) .where(eq(generalContracts.id, contractId)) .limit(1) - if (result.length === 0) { - return null + if (!contract.length || !contract[0].terms) { + return [] } - return result[0].fieldServiceRates as Record || null + const terms = contract[0].terms as any + return terms.storageInfo || [] } catch (error) { - console.error('Failed to get field service rate:', error) - throw new Error('Field Service Rate 데이터를 불러오는데 실패했습니다.') + console.error('Error getting storage info:', error) + throw new Error('Failed to get storage info') } } -export async function updateFieldServiceRate( - contractId: number, - fieldServiceRateData: Record, - userId: number -) { +// 임치계약 정보 저장 +export async function saveStorageInfo(contractId: number, items: Array<{ poNumber: string; hullNumber: string; remainingAmount: number }>, userId: number) { try { + const contract = await db + .select({ terms: generalContracts.terms }) + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract.length) { + throw new Error('Contract not found') + } + + const currentTerms = (contract[0].terms || {}) as any + const updatedTerms = { + ...currentTerms, + storageInfo: items + } + await db .update(generalContracts) .set({ - fieldServiceRates: fieldServiceRateData, + terms: updatedTerms, lastUpdatedAt: new Date(), - lastUpdatedById: userId + lastUpdatedById: userId, }) .where(eq(generalContracts.id, contractId)) - revalidatePath('/evcp/general-contracts') - return { success: true } + revalidatePath(`/general-contracts/detail/${contractId}`) } catch (error) { - console.error('Failed to update field service rate:', error) - throw new Error('Field Service Rate 업데이트에 실패했습니다.') + console.error('Error saving storage info:', error) + throw new Error('Failed to save storage info') } } -// Offset Details 관련 서버 액션들 -export async function getOffsetDetails(contractId: number) { +// 야드투입 정보 조회 +export async function getYardEntryInfo(contractId: number) { try { - const result = await db - .select({ offsetDetails: generalContracts.offsetDetails }) + const contract = await db + .select({ terms: generalContracts.terms }) .from(generalContracts) .where(eq(generalContracts.id, contractId)) .limit(1) - if (result.length === 0) { + if (!contract.length || !contract[0].terms) { return null } - return result[0].offsetDetails as Record || null + const terms = contract[0].terms as any + return terms.yardEntryInfo || null } catch (error) { - console.error('Failed to get offset details:', error) - throw new Error('회입/상계내역 데이터를 불러오는데 실패했습니다.') + console.error('Error getting yard entry info:', error) + throw new Error('Failed to get yard entry info') } } -export async function updateOffsetDetails( - contractId: number, - offsetDetailsData: Record, - userId: number -) { +// 야드투입 정보 저장 +export async function saveYardEntryInfo(contractId: number, data: { projectId: number | null; projectCode: string; projectName: string; managerName: string; managerDepartment: string; rehandlingContractor: string }, userId: number) { try { + const contract = await db + .select({ terms: generalContracts.terms }) + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract.length) { + throw new Error('Contract not found') + } + + const currentTerms = (contract[0].terms || {}) as any + const updatedTerms = { + ...currentTerms, + yardEntryInfo: data + } + await db .update(generalContracts) .set({ - offsetDetails: offsetDetailsData, + terms: updatedTerms, lastUpdatedAt: new Date(), - lastUpdatedById: userId + lastUpdatedById: userId, }) .where(eq(generalContracts.id, contractId)) - revalidatePath('/evcp/general-contracts') - return { success: true } + revalidatePath(`/general-contracts/detail/${contractId}`) } catch (error) { - console.error('Failed to update offset details:', error) - throw new Error('회입/상계내역 업데이트에 실패했습니다.') + console.error('Error saving yard entry info:', error) + throw new Error('Failed to save yard entry info') + } +} + +// 계약 문서 댓글 저장 +export async function saveContractAttachmentComment(attachmentId: number, contractId: number, commentType: 'shi' | 'vendor', comment: string, userId: number) { + try { + const updateData: Record = {} + if (commentType === 'shi') { + updateData.shiComment = comment + } else { + updateData.vendorComment = comment + } + + await db + .update(generalContractAttachments) + .set(updateData) + .where(eq(generalContractAttachments.id, attachmentId)) + + revalidatePath(`/general-contracts/detail/${contractId}`) + } catch (error) { + console.error('Error saving attachment comment:', error) + throw new Error('Failed to save attachment comment') } } -// 계약번호 생성 함수 export async function generateContractNumber( userId?: string, contractType: string @@ -1805,15 +1836,14 @@ export async function generateContractNumber( 'AL': 'AL', // 연간운송계약 'OS': 'OS', // 외주용역계약 'OW': 'OW', // 도급계약 - 'IS': 'IS', // 검사계약 'LO': 'LO', // LOI (의향서) 'FA': 'FA', // FA (Frame Agreement) 'SC': 'SC', // 납품합의계약 (Supply Contract) 'OF': 'OF', // 클레임상계계약 (Offset Agreement) 'AW': 'AW', // 사전작업합의 (Advanced Work) 'AD': 'AD', // 사전납품합의 (Advanced Delivery) - 'AM': 'AM', // 설계계약 - 'SC_SELL': 'SC' // 폐기물매각계약 (Scrap) - 납품합의계약과 동일한 코드 사용 + 'SG': 'SG', // 임치(물품보관)계약 + 'SR': 'SR' // 폐기물매각계약 (Scrap) } const typeCode = contractTypeMap[contractType] || 'XX' // 기본값 @@ -1912,7 +1942,7 @@ export async function generateContractNumber( } } -// 프로젝트 목록 조회 +// 프로젝트 목록 조회 (코드와 이름만 반환) export async function getProjects() { try { const projectList = await db @@ -1920,14 +1950,782 @@ export async function getProjects() { id: projects.id, code: projects.code, name: projects.name, - type: projects.type, }) .from(projects) .orderBy(asc(projects.name)) return projectList } catch (error) { - console.error('Error fetching projects:', error) - throw new Error('Failed to fetch projects') + console.error('프로젝트 목록 조회 오류:', error) + throw new Error('프로젝트 목록을 불러오는데 실패했습니다') + } +} + +// ═══════════════════════════════════════════════════════════════ +// 협력업체 전용 조건검토 조회 함수 +// ═══════════════════════════════════════════════════════════════ + +// 협력업체 전용 조건검토 계약 조회 +export async function getVendorContractReviews( + vendorId: number, + page: number = 1, + perPage: number = 10, + search?: string +) { + try { + const offset = (page - 1) * perPage + + // 조건검토 관련 상태들 + const reviewStatuses = ['Request to Review', 'Vendor Replied Review', 'SHI Confirmed Review'] + + // 기본 조건: vendorId와 status 필터 + const conditions: SQL[] = [ + eq(generalContracts.vendorId, vendorId), + or(...reviewStatuses.map(status => eq(generalContracts.status, status)))! + ] + + // 검색 조건 추가 + if (search) { + const searchPattern = `%${search}%` + conditions.push( + or( + ilike(generalContracts.contractNumber, searchPattern), + ilike(generalContracts.name, searchPattern), + ilike(generalContracts.notes, searchPattern) + )! + ) + } + + const whereCondition = and(...conditions) + + // 전체 개수 조회 + const totalResult = await db + .select({ count: count() }) + .from(generalContracts) + .where(whereCondition) + + const total = totalResult[0]?.count || 0 + + if (total === 0) { + return { data: [], pageCount: 0, total: 0 } + } + + // 데이터 조회 + const data = await db + .select({ + id: generalContracts.id, + contractNumber: generalContracts.contractNumber, + revision: generalContracts.revision, + status: generalContracts.status, + category: generalContracts.category, + type: generalContracts.type, + executionMethod: generalContracts.executionMethod, + name: generalContracts.name, + contractSourceType: generalContracts.contractSourceType, + startDate: generalContracts.startDate, + endDate: generalContracts.endDate, + validityEndDate: generalContracts.validityEndDate, + contractScope: generalContracts.contractScope, + specificationType: generalContracts.specificationType, + specificationManualText: generalContracts.specificationManualText, + contractAmount: generalContracts.contractAmount, + totalAmount: generalContracts.totalAmount, + currency: generalContracts.currency, + registeredAt: generalContracts.registeredAt, + signedAt: generalContracts.signedAt, + linkedRfqOrItb: generalContracts.linkedRfqOrItb, + linkedPoNumber: generalContracts.linkedPoNumber, + linkedBidNumber: generalContracts.linkedBidNumber, + lastUpdatedAt: generalContracts.lastUpdatedAt, + notes: generalContracts.notes, + vendorId: generalContracts.vendorId, + registeredById: generalContracts.registeredById, + lastUpdatedById: generalContracts.lastUpdatedById, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + managerName: users.name, + }) + .from(generalContracts) + .leftJoin(vendors, eq(generalContracts.vendorId, vendors.id)) + .leftJoin(users, eq(generalContracts.registeredById, users.id)) + .where(whereCondition) + .orderBy(desc(generalContracts.registeredAt)) + .limit(perPage) + .offset(offset) + + const pageCount = Math.ceil(total / perPage) + + // 날짜 변환 헬퍼 함수 + const formatDate = (date: unknown): string => { + if (!date) return '' + if (date instanceof Date) { + return date.toISOString() + } + if (typeof date === 'string') { + return date + } + return String(date) + } + + return { + data: data.map((row) => ({ + id: row.id, + contractNumber: row.contractNumber || '', + revision: row.revision || 0, + status: row.status || '', + category: row.category || '', + type: row.type || '', + executionMethod: row.executionMethod || '', + name: row.name || '', + contractSourceType: row.contractSourceType || '', + startDate: formatDate(row.startDate), + endDate: formatDate(row.endDate), + validityEndDate: formatDate(row.validityEndDate), + contractScope: row.contractScope || '', + specificationType: row.specificationType || '', + specificationManualText: row.specificationManualText || '', + contractAmount: row.contractAmount ? row.contractAmount.toString() : '', + totalAmount: row.totalAmount ? row.totalAmount.toString() : '', + currency: row.currency || '', + registeredAt: formatDate(row.registeredAt), + signedAt: formatDate(row.signedAt), + linkedRfqOrItb: row.linkedRfqOrItb || '', + linkedPoNumber: row.linkedPoNumber || '', + linkedBidNumber: row.linkedBidNumber || '', + lastUpdatedAt: formatDate(row.lastUpdatedAt), + notes: row.notes || '', + vendorId: row.vendorId || 0, + registeredById: row.registeredById || 0, + lastUpdatedById: row.lastUpdatedById || 0, + vendorName: row.vendorName || '', + vendorCode: row.vendorCode || '', + managerName: row.managerName || '', + })), + pageCount, + total, + } + console.log(data, "data") + } catch (error) { + console.error('Error fetching vendor contract reviews:', error) + return { data: [], pageCount: 0, total: 0 } + } +} + +// ═══════════════════════════════════════════════════════════════ +// 조건검토 의견 관련 함수들 +// ═══════════════════════════════════════════════════════════════ + +// 협력업체 조건검토 의견 저장 +export async function saveVendorComment( + contractId: number, + vendorComment: string, + vendorId: number +) { + try { + // 계약 정보 조회 및 권한 확인 + const [contract] = await db + .select() + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + if (contract.vendorId !== vendorId) { + throw new Error('이 계약에 대한 접근 권한이 없습니다.') + } + + // generalContracts 테이블에 vendorComment 저장 + await db + .update(generalContracts) + .set({ + vendorComment: vendorComment, + lastUpdatedAt: new Date(), + lastUpdatedById: contract.lastUpdatedById, // 기존 수정자 유지 + }) + .where(eq(generalContracts.id, contractId)) + + revalidatePath(`/partners/general-contract-review/${contractId}`) + revalidatePath(`/general-contracts/detail/${contractId}`) + + return { success: true, message: '협력업체 의견이 저장되었습니다.' } + } catch (error) { + console.error('협력업체 의견 저장 오류:', error) + throw error + } +} + +// 협력업체 조건검토 의견 조회 +export async function getVendorComment(contractId: number, vendorId?: number) { + try { + const conditions = [eq(generalContracts.id, contractId)] + + if (vendorId) { + conditions.push(eq(generalContracts.vendorId, vendorId)) + } + + const [contract] = await db + .select({ + vendorComment: generalContracts.vendorComment, + }) + .from(generalContracts) + .where(and(...conditions)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + return { + success: true, + vendorComment: contract.vendorComment || '', + } + } catch (error) { + console.error('협력업체 의견 조회 오류:', error) + throw error } } + +// 당사 조건검토 의견 저장 +export async function saveShiComment( + contractId: number, + shiComment: string, + userId: number +) { + try { + // 계약 정보 조회 + const [contract] = await db + .select() + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + // generalContracts 테이블에 shiComment 저장 + await db + .update(generalContracts) + .set({ + shiComment: shiComment, + lastUpdatedAt: new Date(), + lastUpdatedById: userId, + }) + .where(eq(generalContracts.id, contractId)) + + revalidatePath(`/general-contracts/detail/${contractId}`) + revalidatePath(`/partners/general-contract-review/${contractId}`) + + return { success: true, message: '당사 의견이 저장되었습니다.' } + } catch (error) { + console.error('당사 의견 저장 오류:', error) + throw error + } +} + +// 당사 조건검토 의견 조회 +export async function getShiComment(contractId: number) { + try { + const [contract] = await db + .select({ + shiComment: generalContracts.shiComment, + }) + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + return { + success: true, + shiComment: contract.shiComment || '', + } + } catch (error) { + console.error('당사 의견 조회 오류:', error) + throw error + } +} + +// 조건검토 의견 모두 조회 (vendorComment + shiComment) +export async function getContractReviewComments(contractId: number, vendorId?: number) { + try { + const conditions = [eq(generalContracts.id, contractId)] + + if (vendorId) { + conditions.push(eq(generalContracts.vendorId, vendorId)) + } + + const [contract] = await db + .select({ + vendorComment: generalContracts.vendorComment, + shiComment: generalContracts.shiComment, + }) + .from(generalContracts) + .where(and(...conditions)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + return { + success: true, + vendorComment: contract.vendorComment || '', + shiComment: contract.shiComment || '', + } + } catch (error) { + console.error('조건검토 의견 조회 오류:', error) + throw error + } +} + +// ═══════════════════════════════════════════════════════════════ +// 조건검토요청 관련 함수들 +// ═══════════════════════════════════════════════════════════════ + +// 조건검토용 파일 업로드 +export async function uploadContractReviewFile(contractId: number, file: File, userId: string) { + try { + const userIdNumber = parseInt(userId) + if (isNaN(userIdNumber)) { + throw new Error('Invalid user ID') + } + + const saveResult = await saveDRMFile( + file, + decryptWithServerAction, + `general-contracts/${contractId}/review-documents`, + userId, + ) + + if (saveResult.success && saveResult.publicPath) { + return { + success: true, + message: '파일이 성공적으로 업로드되었습니다.', + filePath: saveResult.publicPath, + fileName: saveResult.fileName || file.name + } + } else { + return { + success: false, + error: saveResult.error || '파일 저장에 실패했습니다.' + } + } + } catch (error) { + console.error('Failed to upload contract review file:', error) + return { + success: false, + error: '파일 업로드에 실패했습니다.' + } + } +} + +// 조건검토요청 전송 (PDF 포함) +export async function sendContractReviewRequest( + contractSummary: any, + pdfBuffer: Uint8Array, + contractId: number, + userId: string +) { + try { + const userIdNumber = parseInt(userId) + if (isNaN(userIdNumber)) { + throw new Error('Invalid user ID') + } + + // 계약 정보 조회 + const [contract] = await db + .select() + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + // PDF 버퍼를 saveBuffer 함수로 저장 + const fileId = uuidv4() + const fileName = `contract_review_${fileId}.pdf` + + // PDF 버퍼를 Buffer로 변환 + let bufferData: Buffer + if (Buffer.isBuffer(pdfBuffer)) { + bufferData = pdfBuffer + } else if (pdfBuffer instanceof ArrayBuffer) { + bufferData = Buffer.from(pdfBuffer) + } else if (pdfBuffer instanceof Uint8Array) { + bufferData = Buffer.from(pdfBuffer) + } else { + bufferData = Buffer.from(pdfBuffer as any) + } + + // saveBuffer 함수를 사용해서 파일 저장 + const saveResult = await saveBuffer({ + buffer: bufferData, + fileName: fileName, + directory: "generalContracts", + originalName: `contract_review_${contractId}_${fileId}.pdf`, + userId: userId + }) + + if (!saveResult.success) { + throw new Error(saveResult.error || 'PDF 파일 저장에 실패했습니다.') + } + + const finalFileName = saveResult.fileName || fileName + const finalFilePath = saveResult.publicPath + ? saveResult.publicPath.replace('/api/files/', '') + : `/generalContracts/${fileName}` + + // generalContractAttachments 테이블에 계약서 초안 PDF 저장 + await db.insert(generalContractAttachments).values({ + contractId: contractId, + documentName: '계약서 초안', + fileName: finalFileName, + filePath: finalFilePath, + uploadedById: userIdNumber, + uploadedAt: new Date(), + }) + + // 계약 상태를 'Request to Review'로 변경 + await db + .update(generalContracts) + .set({ + status: 'Request to Review', + lastUpdatedAt: new Date(), + lastUpdatedById: userIdNumber, + }) + .where(eq(generalContracts.id, contractId)) + + // 협력업체 정보 조회 + const [vendor] = await db + .select() + .from(vendors) + .where(eq(vendors.id, contract.vendorId)) + .limit(1) + + // 협력업체 담당자에게 검토 요청 이메일 발송 + if (vendor?.vendorEmail) { + try { + await sendEmail({ + to: vendor.vendorEmail, + subject: `[SHI] 일반계약 조건검토 요청 - ${contract.contractNumber}`, + template: 'contract-review-request', + context: { + contractId: contractId, + contractNumber: contract.contractNumber, + contractName: contract.name, + loginUrl: `${process.env.NEXT_PUBLIC_URL}/partners/general-contract-review/${contractId}`, + language: 'ko', + }, + }) + } catch (emailError) { + console.error('이메일 발송 실패:', emailError) + // 이메일 발송 실패해도 계약 상태 변경은 유지 + } + } + + revalidatePath('/general-contracts') + revalidatePath(`/general-contracts/detail/${contractId}`) + + return { success: true, message: '조건검토요청이 성공적으로 전송되었습니다.' } + } catch (error: any) { + console.error('조건검토요청 전송 오류:', error) + return { + success: false, + error: error.message || '조건검토요청 전송에 실패했습니다.' + } + } +} + +// 조건검토요청 전송 (기존 함수 - 하위 호환성 유지) +export async function requestContractReview(contractId: number, userId: number) { + try { + // 계약 정보 조회 + const [contract] = await db + .select() + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + // 계약 상태를 'Request to Review'로 변경 + await db + .update(generalContracts) + .set({ + status: 'Request to Review', + lastUpdatedAt: new Date(), + lastUpdatedById: userId, + }) + .where(eq(generalContracts.id, contractId)) + + // 협력업체 정보 조회 + const [vendor] = await db + .select() + .from(vendors) + .where(eq(vendors.id, contract.vendorId)) + .limit(1) + + // 협력업체 담당자에게 검토 요청 이메일 발송 + if (vendor?.vendorEmail) { + try { + await sendEmail({ + to: vendor.vendorEmail, + subject: `[SHI] 일반계약 조건검토 요청 - ${contract.contractNumber}`, + template: 'contract-review-request', + context: { + contractId: contractId, + contractNumber: contract.contractNumber, + contractName: contract.name, + loginUrl: `${process.env.NEXT_PUBLIC_URL}/partners/general-contract-review/${contractId}`, + language: 'ko', + }, + }) + } catch (emailError) { + console.error('이메일 발송 실패:', emailError) + // 이메일 발송 실패해도 계약 상태 변경은 유지 + } + } + + revalidatePath('/general-contracts') + revalidatePath(`/general-contracts/detail/${contractId}`) + + return { success: true, message: '조건검토요청이 성공적으로 전송되었습니다.' } + } catch (error) { + console.error('조건검토요청 전송 오류:', error) + throw new Error('조건검토요청 전송에 실패했습니다.') + } +} + +// 협력업체용 계약 정보 조회 (검토용 최소 정보) +export async function getContractForVendorReview(contractId: number, vendorId?: number) { + try { + const contract = await db + .select({ + id: generalContracts.id, + contractNumber: generalContracts.contractNumber, + revision: generalContracts.revision, + name: generalContracts.name, + status: generalContracts.status, + type: generalContracts.type, + category: generalContracts.category, + vendorId: generalContracts.vendorId, + contractAmount: generalContracts.contractAmount, + currency: generalContracts.currency, + startDate: generalContracts.startDate, + endDate: generalContracts.endDate, + specificationType: generalContracts.specificationType, + specificationManualText: generalContracts.specificationManualText, + contractScope: generalContracts.contractScope, + notes: generalContracts.notes, + vendorComment: generalContracts.vendorComment, + shiComment: generalContracts.shiComment, + }) + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract.length) { + throw new Error('계약을 찾을 수 없습니다.') + } + + // 권한 확인: vendorId가 제공된 경우 해당 협력업체의 계약인지 확인 + if (vendorId && contract[0].vendorId !== vendorId) { + throw new Error('이 계약에 대한 접근 권한이 없습니다.') + } + + // 품목 정보 조회 + const contractItems = await db + .select() + .from(generalContractItems) + .where(eq(generalContractItems.contractId, contractId)) + + // 첨부파일 조회 + const attachments = await db + .select() + .from(generalContractAttachments) + .where(eq(generalContractAttachments.contractId, contractId)) + + // 협력업체 정보 조회 + const [vendor] = await db + .select() + .from(vendors) + .where(eq(vendors.id, contract[0].vendorId)) + .limit(1) + + return { + ...contract[0], + contractItems, + attachments, + vendor: vendor || null, + } + } catch (error) { + console.error('협력업체용 계약 정보 조회 오류:', error) + throw error + } +} + +// 협력업체 의견 회신 +export async function vendorReplyToContractReview( + contractId: number, + vendorComment: string, + vendorId: number +) { + try { + // 계약 정보 조회 및 권한 확인 + const [contract] = await db + .select() + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + if (contract.vendorId !== vendorId) { + throw new Error('이 계약에 대한 접근 권한이 없습니다.') + } + + // 계약 상태 확인 + if (contract.status !== 'Request to Review') { + throw new Error('조건검토요청 상태가 아닙니다.') + } + + // 계약 상태를 'Vendor Replied Review'로 변경하고 vendorComment 저장 + await db + .update(generalContracts) + .set({ + status: 'Vendor Replied Review', + vendorComment: vendorComment, + lastUpdatedAt: new Date(), + }) + .where(eq(generalContracts.id, contractId)) + + // 당사 구매 담당자에게 회신 알림 이메일 발송 + const [manager] = await db + .select() + .from(users) + .where(eq(users.id, contract.registeredById)) + .limit(1) + + if (manager?.email) { + try { + await sendEmail({ + to: manager.email, + subject: `[SHI] 협력업체 조건검토 회신 - ${contract.contractNumber}`, + template: 'vendor-review-reply', + context: { + contractId: contractId, + contractNumber: contract.contractNumber, + contractName: contract.name, + vendorName: contract.vendorName || '협력업체', + loginUrl: `${process.env.NEXT_PUBLIC_URL}/evcp/general-contracts/detail/${contractId}`, + language: 'ko', + }, + }) + } catch (emailError) { + console.error('이메일 발송 실패:', emailError) + } + } + + revalidatePath('/general-contracts') + revalidatePath(`/general-contracts/detail/${contractId}`) + + return { success: true, message: '의견이 성공적으로 회신되었습니다.' } + } catch (error) { + console.error('협력업체 의견 회신 오류:', error) + throw error + } +} + +// 협력업체 의견 임시 저장 +export async function saveVendorCommentDraft( + contractId: number, + vendorComment: string, + vendorId: number +) { + try { + // 계약 정보 조회 및 권한 확인 + const [contract] = await db + .select() + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + if (contract.vendorId !== vendorId) { + throw new Error('이 계약에 대한 접근 권한이 없습니다.') + } + + // 협력업체 의견을 임시 저장 (generalContracts 테이블의 vendorComment에 저장, 상태는 변경하지 않음) + await db + .update(generalContracts) + .set({ + vendorComment: vendorComment, + lastUpdatedAt: new Date(), + }) + .where(eq(generalContracts.id, contractId)) + + return { success: true, message: '의견이 임시 저장되었습니다.' } + } catch (error) { + console.error('협력업체 의견 임시 저장 오류:', error) + throw error + } +} + +// 당사 검토 확정 +export async function confirmContractReview( + contractId: number, + shiComment: string, + userId: number +) { + try { + // 계약 정보 조회 + const [contract] = await db + .select() + .from(generalContracts) + .where(eq(generalContracts.id, contractId)) + .limit(1) + + if (!contract) { + throw new Error('계약을 찾을 수 없습니다.') + } + + // 계약 상태 확인 + if (contract.status !== 'Vendor Replied Review') { + throw new Error('협력업체 회신 상태가 아닙니다.') + } + + // 계약 상태를 'SHI Confirmed Review'로 변경하고 shiComment 저장 + await db + .update(generalContracts) + .set({ + status: 'SHI Confirmed Review', + shiComment: shiComment, + lastUpdatedAt: new Date(), + lastUpdatedById: userId, + }) + .where(eq(generalContracts.id, contractId)) + + revalidatePath('/general-contracts') + revalidatePath(`/general-contracts/detail/${contractId}`) + + return { success: true, message: '검토가 확정되었습니다.' } + } catch (error) { + console.error('당사 검토 확정 오류:', error) + throw error + } +} \ No newline at end of file -- cgit v1.2.3