summaryrefslogtreecommitdiff
path: root/lib/bidding/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/service.ts')
-rw-r--r--lib/bidding/service.ts306
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