diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2025-11-10 11:25:19 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2025-11-10 11:25:19 +0900 |
| commit | a5501ad1d1cb836d2b2f84e9b0f06049e22c901e (patch) | |
| tree | 667ed8c5d6ec35b109190e9f976d66ae54def4ce /lib/general-contracts/service.ts | |
| parent | b0fe980376fcf1a19ff4b90851ca8b01f378fdc0 (diff) | |
| parent | f8a38907911d940cb2e8e6c9aa49488d05b2b578 (diff) | |
Merge remote-tracking branch 'origin/dujinkim' into master_homemaster
Diffstat (limited to 'lib/general-contracts/service.ts')
| -rw-r--r-- | lib/general-contracts/service.ts | 1102 |
1 files changed, 950 insertions, 152 deletions
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<string, unknown>) { 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<string, unknown>) { 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<string, u // 업데이트할 데이터 정리
// 클라이언트에서 전송된 formData를 그대로 사용합니다.
const {
+ contractScope,
specificationType,
specificationManualText,
unitPriceType,
@@ -475,6 +460,8 @@ export async function updateContractBasicInfo(id: number, data: Record<string, u interlockingSystem,
mandatoryDocuments,
contractTerminationConditions,
+ externalYardEntry,
+ contractAmountReason,
} = data
// 계약금액 자동 집계 로직
@@ -507,6 +494,7 @@ export async function updateContractBasicInfo(id: number, data: Record<string, u // 업데이트할 데이터 객체 생성
const updateData: Record<string, unknown> = {
+ contractScope,
specificationType,
specificationManualText,
unitPriceType,
@@ -531,6 +519,8 @@ export async function updateContractBasicInfo(id: number, data: Record<string, u interlockingSystem,
mandatoryDocuments, // JSON 필드
contractTerminationConditions, // JSON 필드
+ externalYardEntry,
+ contractAmountReason: convertEmptyStringToNull(contractAmountReason),
contractAmount: calculatedContractAmount || 0,
lastUpdatedAt: new Date(),
lastUpdatedById: userId,
@@ -543,6 +533,12 @@ export async function updateContractBasicInfo(id: number, data: Record<string, u .where(eq(generalContracts.id, id))
.returning()
+ // 계약명 I/F 로직 (39번 화면으로의 I/F)
+ // TODO: 39번 화면의 정확한 API 엔드포인트나 함수명 확인 필요
+ // if (data.name) {
+ // await syncContractNameToScreen39(id, data.name as string)
+ // }
+
revalidatePath('/general-contracts')
revalidatePath(`/general-contracts/detail/${id}`)
return updatedContract
@@ -556,8 +552,28 @@ export async function updateContractBasicInfo(id: number, data: Record<string, u export async function getContractItems(contractId: number) {
try {
const items = await db
- .select()
+ .select({
+ id: generalContractItems.id,
+ contractId: generalContractItems.contractId,
+ projectId: generalContractItems.projectId,
+ itemCode: generalContractItems.itemCode,
+ itemInfo: generalContractItems.itemInfo,
+ specification: generalContractItems.specification,
+ quantity: generalContractItems.quantity,
+ quantityUnit: generalContractItems.quantityUnit,
+ totalWeight: generalContractItems.totalWeight,
+ weightUnit: generalContractItems.weightUnit,
+ contractDeliveryDate: generalContractItems.contractDeliveryDate,
+ contractUnitPrice: generalContractItems.contractUnitPrice,
+ contractAmount: generalContractItems.contractAmount,
+ contractCurrency: generalContractItems.contractCurrency,
+ createdAt: generalContractItems.createdAt,
+ updatedAt: generalContractItems.updatedAt,
+ projectName: projects.name,
+ projectCode: projects.code,
+ })
.from(generalContractItems)
+ .leftJoin(projects, eq(generalContractItems.projectId, projects.id))
.where(eq(generalContractItems.contractId, contractId))
.orderBy(asc(generalContractItems.id))
@@ -575,6 +591,7 @@ export async function createContractItem(contractId: number, itemData: Record<st .insert(generalContractItems)
.values({
contractId,
+ projectId: itemData.projectId ? (itemData.projectId as number) : null,
itemCode: itemData.itemCode as string,
itemInfo: itemData.itemInfo as string,
specification: itemData.specification as string,
@@ -604,6 +621,7 @@ export async function updateContractItem(itemId: number, itemData: Record<string const [updatedItem] = await db
.update(generalContractItems)
.set({
+ projectId: itemData.projectId ? (itemData.projectId as number) : null,
itemCode: itemData.itemCode as string,
itemInfo: itemData.itemInfo as string,
specification: itemData.specification as string,
@@ -673,6 +691,7 @@ export async function updateContractItems(contractId: number, items: Record<stri .values(
items.map((item: Record<string, unknown>) => ({
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<string, unknown>, 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<string, unknown>, 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<string, unknown>) {
try {
// 숫자 필드에서 빈 문자열을 null로 변환
@@ -990,7 +929,7 @@ export async function updateContract(id: number, data: Record<string, unknown>) .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<string, unknown> || 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<string, unknown>,
- 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<string, unknown> || 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<string, unknown>,
- 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<string, unknown> = {}
+ 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<unknown>[] = [
+ 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 |
