diff options
Diffstat (limited to 'lib/bidding/service.ts')
| -rw-r--r-- | lib/bidding/service.ts | 306 |
1 files changed, 82 insertions, 224 deletions
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 0064b66f..a658ee6a 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -391,6 +391,7 @@ export async function getBiddings(input: GetBiddingsSchema) { id: biddings.id, biddingNumber: biddings.biddingNumber, originalBiddingNumber: biddings.originalBiddingNumber, + projectCode: biddings.projectCode, projectName: biddings.projectName, title: biddings.title, @@ -2150,6 +2151,7 @@ export async function updateBiddingProjectInfo(biddingId: number) { try { const firstItem = await db .select({ + projectId: prItemsForBidding.projectId, projectInfo: prItemsForBidding.projectInfo }) .from(prItemsForBidding) @@ -2157,16 +2159,36 @@ export async function updateBiddingProjectInfo(biddingId: number) { .orderBy(prItemsForBidding.id) .limit(1) - if (firstItem.length > 0 && firstItem[0].projectInfo) { + if (firstItem.length > 0) { + let projectName = firstItem[0].projectInfo + let projectCode = null + + if (firstItem[0].projectId) { + const project = await db + .select({ + name: projects.name, + code: projects.code + }) + .from(projects) + .where(eq(projects.id, firstItem[0].projectId)) + .limit(1) + + if (project.length > 0) { + projectName = project[0].name + projectCode = project[0].code + } + } + await db .update(biddings) .set({ - projectName: firstItem[0].projectInfo, + projectName: projectName, + projectCode: projectCode, updatedAt: new Date() }) .where(eq(biddings.id, biddingId)) - console.log(`Bidding ${biddingId} project info updated to: ${firstItem[0].projectInfo}`) + console.log(`Bidding ${biddingId} project info updated to: ${projectName} (${projectCode})`) } } catch (error) { console.error('Failed to update bidding project info:', error) @@ -2556,227 +2578,6 @@ export async function updateBiddingConditions( } } -// 사전견적용 일반견적 생성 액션 -export async function createPreQuoteRfqAction(input: { - biddingId: number - rfqType: string - rfqTitle: string - dueDate: Date - picUserId: number - projectId?: number - remark?: string - items: Array<{ - itemCode: string - itemName: string - materialCode?: string - materialName?: string - quantity: number - uom: string - remark?: string - }> - biddingConditions?: { - paymentTerms?: string | null - taxConditions?: string | null - incoterms?: string | null - incotermsOption?: string | null - contractDeliveryDate?: string | null - shippingPort?: string | null - destinationPort?: string | null - isPriceAdjustmentApplicable?: boolean | null - sparePartOptions?: string | null - } - createdBy: number - updatedBy: number -}) { - try { - // 일반견적 생성 서버 액션 및 필요한 스키마 import - const { createGeneralRfqAction } = await import('@/lib/rfq-last/service') - const { rfqLastDetails, rfqLastVendorResponses, rfqLastVendorResponseHistory } = await import('@/db/schema') - - // 일반견적 생성 - const result = await createGeneralRfqAction({ - rfqType: input.rfqType, - rfqTitle: input.rfqTitle, - dueDate: input.dueDate, - picUserId: input.picUserId, - projectId: input.projectId, - remark: input.remark || '', - items: input.items.map(item => ({ - itemCode: item.itemCode, - itemName: item.itemName, - quantity: item.quantity, - uom: item.uom, - remark: item.remark, - materialCode: item.materialCode, - materialName: item.materialName, - })), - createdBy: input.createdBy, - updatedBy: input.updatedBy, - }) - - if (!result.success || !result.data) { - return { - success: false, - error: result.error || '사전견적용 일반견적 생성에 실패했습니다', - } - } - - const rfqId = result.data.id - const conditions = input.biddingConditions - - // 입찰 조건을 RFQ 조건으로 매핑 - const mapBiddingConditionsToRfqConditions = () => { - if (!conditions) { - return { - currency: 'KRW', - paymentTermsCode: undefined, - incotermsCode: undefined, - incotermsDetail: undefined, - deliveryDate: undefined, - taxCode: undefined, - placeOfShipping: undefined, - placeOfDestination: undefined, - materialPriceRelatedYn: false, - sparepartYn: false, - sparepartDescription: undefined, - } - } - - // contractDeliveryDate 문자열을 Date로 변환 (timestamp 타입용) - let deliveryDate: Date | undefined = undefined - if (conditions.contractDeliveryDate) { - try { - const date = new Date(conditions.contractDeliveryDate) - if (!isNaN(date.getTime())) { - deliveryDate = date - } - } catch (error) { - console.warn('Failed to parse contractDeliveryDate:', error) - } - } - - return { - currency: 'KRW', // 기본값 - paymentTermsCode: conditions.paymentTerms || undefined, - incotermsCode: conditions.incoterms || undefined, - incotermsDetail: conditions.incotermsOption || undefined, - deliveryDate: deliveryDate, // timestamp 타입 (rfqLastDetails용) - vendorDeliveryDate: deliveryDate, // date 타입 (rfqLastVendorResponses용) - taxCode: conditions.taxConditions || undefined, - placeOfShipping: conditions.shippingPort || undefined, - placeOfDestination: conditions.destinationPort || undefined, - materialPriceRelatedYn: conditions.isPriceAdjustmentApplicable ?? false, - sparepartYn: !!conditions.sparePartOptions, // sparePartOptions가 있으면 true - sparepartDescription: conditions.sparePartOptions || undefined, - } - } - - const rfqConditions = mapBiddingConditionsToRfqConditions() - - // 입찰에 참여한 업체 목록 조회 - const vendorsResult = await getBiddingVendors(input.biddingId) - if (!vendorsResult.success || !vendorsResult.data || vendorsResult.data.length === 0) { - return { - success: true, - message: '사전견적용 일반견적이 생성되었습니다. (참여 업체 없음)', - data: { - rfqCode: result.data.rfqCode, - rfqId: result.data.id, - }, - } - } - - // 각 업체에 대해 rfqLastDetails와 rfqLastVendorResponses 생성 - await db.transaction(async (tx) => { - for (const vendor of vendorsResult.data) { - if (!vendor.companyId) continue - - // 1. rfqLastDetails 생성 (구매자 제시 조건) - const [rfqDetail] = await tx - .insert(rfqLastDetails) - .values({ - rfqsLastId: rfqId, - vendorsId: vendor.companyId, - currency: rfqConditions.currency, - paymentTermsCode: rfqConditions.paymentTermsCode || null, - incotermsCode: rfqConditions.incotermsCode || null, - incotermsDetail: rfqConditions.incotermsDetail || null, - deliveryDate: rfqConditions.deliveryDate || null, - taxCode: rfqConditions.taxCode || null, - placeOfShipping: rfqConditions.placeOfShipping || null, - placeOfDestination: rfqConditions.placeOfDestination || null, - materialPriceRelatedYn: rfqConditions.materialPriceRelatedYn, - sparepartYn: rfqConditions.sparepartYn, - sparepartDescription: rfqConditions.sparepartDescription || null, - updatedBy: input.updatedBy, - createdBy: input.createdBy, - isLatest: true, - }) - .returning() - - // 2. rfqLastVendorResponses 생성 (초기 응답 레코드) - const [vendorResponse] = await tx - .insert(rfqLastVendorResponses) - .values({ - rfqsLastId: rfqId, - rfqLastDetailsId: rfqDetail.id, - vendorId: vendor.companyId, - status: '대기중', - responseVersion: 1, - isLatest: true, - participationStatus: '미응답', - currency: rfqConditions.currency, - // 구매자 제시 조건을 벤더 제안 조건의 초기값으로 복사 - vendorCurrency: rfqConditions.currency, - vendorPaymentTermsCode: rfqConditions.paymentTermsCode || null, - vendorIncotermsCode: rfqConditions.incotermsCode || null, - vendorIncotermsDetail: rfqConditions.incotermsDetail || null, - vendorDeliveryDate: rfqConditions.vendorDeliveryDate || null, - vendorTaxCode: rfqConditions.taxCode || null, - vendorPlaceOfShipping: rfqConditions.placeOfShipping || null, - vendorPlaceOfDestination: rfqConditions.placeOfDestination || null, - vendorMaterialPriceRelatedYn: rfqConditions.materialPriceRelatedYn, - vendorSparepartYn: rfqConditions.sparepartYn, - vendorSparepartDescription: rfqConditions.sparepartDescription || null, - createdBy: input.createdBy, - updatedBy: input.updatedBy, - }) - .returning() - - // 3. 이력 기록 - await tx - .insert(rfqLastVendorResponseHistory) - .values({ - vendorResponseId: vendorResponse.id, - action: '생성', - newStatus: '대기중', - changeDetails: { - action: '사전견적용 일반견적 생성', - biddingId: input.biddingId, - conditions: rfqConditions, - }, - performedBy: input.createdBy, - }) - } - }) - - return { - success: true, - message: `사전견적용 일반견적이 성공적으로 생성되었습니다. (${vendorsResult.data.length}개 업체 추가)`, - data: { - rfqCode: result.data.rfqCode, - rfqId: result.data.id, - }, - } - } catch (error) { - console.error('Failed to create pre-quote RFQ:', error) - return { - success: false, - error: error instanceof Error ? error.message : '사전견적용 일반견적 생성에 실패했습니다', - } - } -} - // 일반견적 RFQ 코드 미리보기 (rfq-last/service에서 재사용) export async function previewGeneralRfqCode(picUserId: number): Promise<string> { try { @@ -3404,8 +3205,65 @@ export async function getVendorContactsByVendorId(vendorId: number) { // bid-receive 페이지용 함수들 // ═══════════════════════════════════════════════════════════════ +// 입찰서 접수 기간 만료 체크 및 상태 업데이트 +export async function checkAndCloseExpiredBiddings() { + try { + const now = new Date() + + // 1. 기간이 만료되었는데 아직 진행중인 입찰 조회 + const expiredBiddings = await db + .select({ id: biddings.id }) + .from(biddings) + .where( + and( + or( + eq(biddings.status, 'bidding_opened') + ), + lte(biddings.submissionEndDate, now) + ) + ) + + if (expiredBiddings.length === 0) { + return + } + + const expiredBiddingIds = expiredBiddings.map(b => b.id) + + // 2. 입찰 상태를 '입찰마감(bidding_closed)'으로 변경 + await db + .update(biddings) + .set({ status: 'bidding_closed' }) + .where(inArray(biddings.id, expiredBiddingIds)) + + // 3. 최종 제출 버튼을 누르지 않은 벤더들의 가장 마지막 견적을 최종 제출로 처리 + // biddingCompanies 테이블에 이미 마지막 견적 정보가 저장되어 있다고 가정 + await db + .update(biddingCompanies) + .set({ + isFinalSubmission: true, + invitationStatus: 'bidding_submitted' // 상태도 최종 응찰로 변경 + }) + .where( + and( + inArray(biddingCompanies.biddingId, expiredBiddingIds), + eq(biddingCompanies.isFinalSubmission, false), + isNotNull(biddingCompanies.finalQuoteAmount) // 견적 금액이 있는 경우만 (참여한 경우) + ) + ) + + // 데이터 갱신을 위해 경로 재검증 + revalidatePath('/evcp/bid-receive') + + } catch (error) { + console.error('Error in checkAndCloseExpiredBiddings:', error) + } +} + // bid-receive: 입찰서접수및마감 페이지용 입찰 목록 조회 export async function getBiddingsForReceive(input: GetBiddingsSchema) { + // 조회 전 만료된 입찰 상태 업데이트 + await checkAndCloseExpiredBiddings() + try { const offset = (input.page - 1) * input.perPage |
