summaryrefslogtreecommitdiff
path: root/lib/bidding/detail/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/detail/service.ts')
-rw-r--r--lib/bidding/detail/service.ts508
1 files changed, 260 insertions, 248 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts
index f52ecb1e..eec3f253 100644
--- a/lib/bidding/detail/service.ts
+++ b/lib/bidding/detail/service.ts
@@ -3,7 +3,7 @@
import db from '@/db/db'
import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, priceAdjustmentForms, users, vendorContacts } from '@/db/schema'
import { specificationMeetings, biddingCompaniesContacts } from '@/db/schema/bidding'
-import { eq, and, sql, desc, ne, asc } from 'drizzle-orm'
+import { eq, and, sql, desc, ne, asc, inArray } from 'drizzle-orm'
import { revalidatePath, revalidateTag } from 'next/cache'
import { unstable_cache } from "@/lib/unstable-cache";
import { sendEmail } from '@/lib/mail/sendEmail'
@@ -30,43 +30,113 @@ async function getUserNameById(userId: string): Promise<string> {
// 데이터 조회 함수들
export interface BiddingDetailData {
bidding: Awaited<ReturnType<typeof getBiddingById>>
- quotationDetails: QuotationDetails | null
+ quotationDetails: null
quotationVendors: QuotationVendor[]
- prItems: Awaited<ReturnType<typeof getPRItemsForBidding>>
+ prItems: Awaited<ReturnType<typeof getPrItemsForBidding>>
}
// getBiddingById 함수 임포트 (기존 함수 재사용)
import { getBiddingById, updateBiddingProjectInfo } from '@/lib/bidding/service'
+import { getPrItemsForBidding } from '../pre-quote/service'
-// Promise.all을 사용하여 모든 데이터를 병렬로 조회 (캐시 적용)
+// Bidding Detail Data 조회 (캐시 제거, 로직 단순화)
export async function getBiddingDetailData(biddingId: number): Promise<BiddingDetailData> {
- return unstable_cache(
- async () => {
- const [
- bidding,
- quotationDetails,
- quotationVendors,
- prItems
- ] = await Promise.all([
- getBiddingById(biddingId),
- getQuotationDetails(biddingId),
- getQuotationVendors(biddingId),
- getPRItemsForBidding(biddingId)
- ])
+ try {
+ // 1. 입찰 정보 조회
+ const bidding = await getBiddingById(biddingId)
- return {
- bidding,
- quotationDetails,
- quotationVendors,
- prItems
+ // 2. 입찰 품목 조회 (pre-quote service 함수 재사용)
+ const prItems = await getPrItemsForBidding(biddingId)
+
+ // 3. 본입찰 제출 업체 조회 (bidding_submitted 상태)
+ const vendorsData = await db
+ .select({
+ id: biddingCompanies.id,
+ biddingId: biddingCompanies.biddingId,
+ vendorId: biddingCompanies.companyId,
+ vendorName: vendors.vendorName,
+ vendorCode: vendors.vendorCode,
+ vendorEmail: vendors.email,
+ quotationAmount: biddingCompanies.finalQuoteAmount,
+ currency: sql<string>`'KRW'`,
+ submissionDate: biddingCompanies.finalQuoteSubmittedAt,
+ isWinner: biddingCompanies.isWinner,
+ awardRatio: biddingCompanies.awardRatio,
+ isBiddingParticipated: biddingCompanies.isBiddingParticipated,
+ invitationStatus: biddingCompanies.invitationStatus,
+ // 연동제 관련 필드
+ isPriceAdjustmentApplicableQuestion: biddingCompanies.isPriceAdjustmentApplicableQuestion,
+ priceAdjustmentResponse: companyConditionResponses.priceAdjustmentResponse, // 벤더가 응답한 연동제 적용 여부
+ shiPriceAdjustmentApplied: biddingCompanies.shiPriceAdjustmentApplied,
+ priceAdjustmentNote: biddingCompanies.priceAdjustmentNote,
+ hasChemicalSubstance: biddingCompanies.hasChemicalSubstance,
+ // Contact info from biddingCompaniesContacts
+ contactPerson: biddingCompaniesContacts.contactName,
+ contactEmail: biddingCompaniesContacts.contactEmail,
+ contactPhone: biddingCompaniesContacts.contactNumber,
+ })
+ .from(biddingCompanies)
+ .innerJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
+ .leftJoin(biddingCompaniesContacts, and(
+ eq(biddingCompaniesContacts.biddingId, biddingId),
+ eq(biddingCompaniesContacts.vendorId, biddingCompanies.companyId)
+ ))
+ .leftJoin(companyConditionResponses, and(
+ eq(companyConditionResponses.biddingCompanyId, biddingCompanies.id),
+ eq(companyConditionResponses.isPreQuote, false) // 본입찰 데이터만
+ ))
+ .where(and(
+ eq(biddingCompanies.biddingId, biddingId),
+ eq(biddingCompanies.isBiddingParticipated, true)
+ ))
+ .orderBy(desc(biddingCompanies.finalQuoteAmount))
+
+ // 중복 제거 (업체당 여러 담당자가 있을 경우 첫 번째만 사용하거나 처리)
+ // 여기서는 간단히 메모리에서 중복 제거 (biddingCompanyId 기준)
+ const uniqueVendors = vendorsData.reduce((acc, curr) => {
+ if (!acc.find(v => v.id === curr.id)) {
+ acc.push({
+ id: curr.id,
+ biddingId: curr.biddingId,
+ vendorId: curr.vendorId,
+ vendorName: curr.vendorName || `Vendor ${curr.vendorId}`,
+ vendorCode: curr.vendorCode || '',
+ vendorEmail: curr.vendorEmail || '',
+ contactPerson: curr.contactPerson || '',
+ contactEmail: curr.contactEmail || '',
+ contactPhone: curr.contactPhone || '',
+ quotationAmount: Number(curr.quotationAmount) || 0,
+ currency: curr.currency,
+ submissionDate: curr.submissionDate ? (curr.submissionDate instanceof Date ? curr.submissionDate.toISOString().split('T')[0] : String(curr.submissionDate).split('T')[0]) : '',
+ isWinner: curr.isWinner,
+ awardRatio: curr.awardRatio ? Number(curr.awardRatio) : null,
+ isBiddingParticipated: curr.isBiddingParticipated,
+ invitationStatus: curr.invitationStatus,
+ // 연동제 관련 필드
+ isPriceAdjustmentApplicableQuestion: curr.isPriceAdjustmentApplicableQuestion,
+ priceAdjustmentResponse: curr.priceAdjustmentResponse, // 벤더가 응답한 연동제 적용 여부
+ shiPriceAdjustmentApplied: curr.shiPriceAdjustmentApplied,
+ priceAdjustmentNote: curr.priceAdjustmentNote,
+ hasChemicalSubstance: curr.hasChemicalSubstance,
+ documents: [],
+ })
}
- },
- [`bidding-detail-data-${biddingId}`],
- {
- tags: [`bidding-${biddingId}`, 'bidding-detail', 'quotation-vendors', 'pr-items']
+ return acc
+ }, [] as QuotationVendor[])
+
+ return {
+ bidding,
+ quotationDetails: null,
+ quotationVendors: uniqueVendors,
+ prItems
}
- )()
+ } catch (error) {
+ console.error('Failed to get bidding detail data:', error)
+ throw error
+ }
}
+
+// QuotationDetails Interface (Keeping it for type safety if needed elsewhere, or remove if safe)
export interface QuotationDetails {
biddingId: number
estimatedPrice: number // 예상액
@@ -94,6 +164,12 @@ export interface QuotationVendor {
awardRatio: number | null // 발주비율
isBiddingParticipated: boolean | null // 본입찰 참여여부
invitationStatus: 'pending' | 'pre_quote_sent' | 'pre_quote_accepted' | 'pre_quote_declined' | 'pre_quote_submitted' | 'bidding_sent' | 'bidding_accepted' | 'bidding_declined' | 'bidding_cancelled' | 'bidding_submitted'
+ // 연동제 관련 필드
+ isPriceAdjustmentApplicableQuestion: boolean | null // SHI가 요청한 연동제 요청 여부
+ priceAdjustmentResponse: boolean | null // 벤더가 응답한 연동제 적용 여부 (companyConditionResponses.priceAdjustmentResponse)
+ shiPriceAdjustmentApplied: boolean | null // SHI 연동제 적용여부
+ priceAdjustmentNote: string | null // 연동제 Note
+ hasChemicalSubstance: boolean | null // 화학물질여부
documents: Array<{
id: number
fileName: string
@@ -103,66 +179,6 @@ export interface QuotationVendor {
}>
}
-// 견적 시스템에서 내정가 및 관련 정보를 가져오는 함수 (캐시 적용)
-export async function getQuotationDetails(biddingId: number): Promise<QuotationDetails | null> {
- return unstable_cache(
- async () => {
- try {
- // bidding_companies 테이블에서 견적 데이터를 집계
- const quotationStats = await db
- .select({
- biddingId: biddingCompanies.biddingId,
- estimatedPrice: sql<number>`AVG(${biddingCompanies.finalQuoteAmount})`.as('estimated_price'),
- lowestQuote: sql<number>`MIN(${biddingCompanies.finalQuoteAmount})`.as('lowest_quote'),
- averageQuote: sql<number>`AVG(${biddingCompanies.finalQuoteAmount})`.as('average_quote'),
- targetPrice: sql<number>`AVG(${biddings.targetPrice})`.as('target_price'),
- quotationCount: sql<number>`COUNT(*)`.as('quotation_count'),
- lastUpdated: sql<string>`MAX(${biddingCompanies.updatedAt})`.as('last_updated')
- })
- .from(biddingCompanies)
- .leftJoin(biddings, eq(biddingCompanies.biddingId, biddings.id))
- .where(and(
- eq(biddingCompanies.biddingId, biddingId),
- sql`${biddingCompanies.finalQuoteAmount} IS NOT NULL`
- ))
- .groupBy(biddingCompanies.biddingId)
- .limit(1)
-
- if (quotationStats.length === 0) {
- return {
- biddingId,
- estimatedPrice: 0,
- lowestQuote: 0,
- averageQuote: 0,
- targetPrice: 0,
- quotationCount: 0,
- lastUpdated: new Date().toISOString()
- }
- }
-
- const stat = quotationStats[0]
-
- return {
- biddingId,
- estimatedPrice: Number(stat.estimatedPrice) || 0,
- lowestQuote: Number(stat.lowestQuote) || 0,
- averageQuote: Number(stat.averageQuote) || 0,
- targetPrice: Number(stat.targetPrice) || 0,
- quotationCount: Number(stat.quotationCount) || 0,
- lastUpdated: stat.lastUpdated || new Date().toISOString()
- }
- } catch (error) {
- console.error('Failed to get quotation details:', error)
- return null
- }
- },
- [`quotation-details-${biddingId}`],
- {
- tags: [`bidding-${biddingId}`, 'quotation-details']
- }
- )()
-}
-
// bidding_companies 테이블을 메인으로 vendors 테이블을 조인하여 협력업체 정보 조회
export async function getBiddingCompaniesData(biddingId: number) {
try {
@@ -281,7 +297,7 @@ export async function getAllBiddingCompanies(biddingId: number) {
}
}
-// prItemsForBidding 테이블에서 품목 정보 조회 (캐시 미적용, always fresh)
+// prItemsForBidding 테이블에서 품목 정보 조회 (deprecated - import from pre-quote/service)
export async function getPRItemsForBidding(biddingId: number) {
try {
const items = await db
@@ -297,70 +313,9 @@ export async function getPRItemsForBidding(biddingId: number) {
}
}
-// 견적 시스템에서 협력업체 정보를 가져오는 함수 (캐시 적용)
-export async function getQuotationVendors(biddingId: number): Promise<QuotationVendor[]> {
- return unstable_cache(
- async () => {
- try {
- // bidding_companies 테이블을 메인으로 vendors를 조인하여 협력업체 정보 조회
- const vendorsData = await db
- .select({
- id: biddingCompanies.id,
- biddingId: biddingCompanies.biddingId,
- vendorId: biddingCompanies.companyId,
- vendorName: vendors.vendorName,
- vendorCode: vendors.vendorCode,
- vendorEmail: vendors.email, // 벤더의 기본 이메일
- contactPerson: biddingCompanies.contactPerson,
- contactEmail: biddingCompanies.contactEmail,
- contactPhone: biddingCompanies.contactPhone,
- quotationAmount: biddingCompanies.finalQuoteAmount,
- currency: sql<string>`'KRW'`,
- submissionDate: biddingCompanies.finalQuoteSubmittedAt,
- isWinner: biddingCompanies.isWinner,
- // awardRatio: sql<number>`CASE WHEN ${biddingCompanies.isWinner} THEN 100 ELSE 0 END`,
- awardRatio: biddingCompanies.awardRatio,
- isBiddingParticipated: biddingCompanies.isBiddingParticipated,
- invitationStatus: biddingCompanies.invitationStatus,
- })
- .from(biddingCompanies)
- .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
- .where(and(
- eq(biddingCompanies.biddingId, biddingId),
- eq(biddingCompanies.isPreQuoteSelected, true) // 본입찰 선정된 업체만 조회
- ))
- .orderBy(desc(biddingCompanies.finalQuoteAmount))
+// 견적 시스템에서 협력업체 정보를 가져오는 함수 (Deprecated - integrated into getBiddingDetailData)
+// export async function getQuotationVendors(biddingId: number): Promise<QuotationVendor[]> { ... }
- return vendorsData.map(vendor => ({
- id: vendor.id,
- biddingId: vendor.biddingId,
- vendorId: vendor.vendorId,
- vendorName: vendor.vendorName || `Vendor ${vendor.vendorId}`,
- vendorCode: vendor.vendorCode || '',
- vendorEmail: vendor.vendorEmail || '', // 벤더의 기본 이메일
- contactPerson: vendor.contactPerson || '',
- contactEmail: vendor.contactEmail || '',
- contactPhone: vendor.contactPhone || '',
- quotationAmount: Number(vendor.quotationAmount) || 0,
- currency: vendor.currency,
- submissionDate: vendor.submissionDate ? (vendor.submissionDate instanceof Date ? vendor.submissionDate.toISOString().split('T')[0] : String(vendor.submissionDate).split('T')[0]) : '',
- isWinner: vendor.isWinner,
- awardRatio: vendor.awardRatio ? Number(vendor.awardRatio) : null,
- isBiddingParticipated: vendor.isBiddingParticipated,
- invitationStatus: vendor.invitationStatus,
- documents: [], // 빈 배열로 초기화
- }))
- } catch (error) {
- console.error('Failed to get quotation vendors:', error)
- return []
- }
- },
- [`quotation-vendors-${biddingId}`],
- {
- tags: [`bidding-${biddingId}`, 'quotation-vendors']
- }
- )()
-}
// 사전견적 데이터 조회 (내정가 산정용)
export async function getPreQuoteData(biddingId: number) {
@@ -898,11 +853,59 @@ export async function registerBidding(biddingId: number, userId: string) {
await db.transaction(async (tx) => {
debugLog('registerBidding: Transaction started')
- // 1. 입찰 상태를 오픈으로 변경
+
+ // 0. 입찰서 제출기간 계산 (입력값 절대 기준)
+ const { submissionStartOffset, submissionDurationDays, submissionStartDate, submissionEndDate } = bidding
+
+ let calculatedStartDate = bidding.submissionStartDate
+ let calculatedEndDate = bidding.submissionEndDate
+
+ if (submissionStartOffset !== null && submissionDurationDays !== null) {
+ // DB에 저장된 시간을 숫자 그대로 가져옴 (예: 10:00 저장 → 10 반환)
+ const startTime = submissionStartDate
+ ? { hours: submissionStartDate.getUTCHours(), minutes: submissionStartDate.getUTCMinutes() }
+ : { hours: 9, minutes: 0 }
+ const endTime = submissionEndDate
+ ? { hours: submissionEndDate.getUTCHours(), minutes: submissionEndDate.getUTCMinutes() }
+ : { hours: 18, minutes: 0 }
+
+ // 서버의 오늘 날짜(년/월/일)를 그대로 사용해 00:00 UTC 시점 생성
+ const now = new Date()
+ const baseDate = new Date(Date.UTC(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ 0, 0, 0
+ ))
+
+ // 시작일 = baseDate + offset일 + 입력 시간(숫자 그대로)
+ const tempStartDate = new Date(baseDate)
+ tempStartDate.setUTCDate(tempStartDate.getUTCDate() + submissionStartOffset)
+ tempStartDate.setUTCHours(startTime.hours, startTime.minutes, 0, 0)
+
+ // 마감일 = 시작일 날짜만 기준 + duration일 + 입력 마감 시간
+ const tempEndDate = new Date(tempStartDate)
+ tempEndDate.setUTCHours(0, 0, 0, 0)
+ tempEndDate.setUTCDate(tempEndDate.getUTCDate() + submissionDurationDays)
+ tempEndDate.setUTCHours(endTime.hours, endTime.minutes, 0, 0)
+
+ calculatedStartDate = tempStartDate
+ calculatedEndDate = tempEndDate
+
+ debugLog('registerBidding: Submission dates calculated (Input Value Based)', {
+ baseDate: baseDate.toISOString(),
+ calculatedStartDate: calculatedStartDate.toISOString(),
+ calculatedEndDate: calculatedEndDate.toISOString(),
+ })
+ }
+
+ // 1. 입찰 상태를 오픈으로 변경 + 제출기간 업데이트
await tx
.update(biddings)
.set({
status: 'bidding_opened',
+ submissionStartDate: calculatedStartDate,
+ submissionEndDate: calculatedEndDate,
updatedBy: userName,
updatedAt: new Date()
})
@@ -1368,10 +1371,14 @@ export async function getAwardedCompanies(biddingId: number) {
companyId: biddingCompanies.companyId,
companyName: vendors.vendorName,
finalQuoteAmount: biddingCompanies.finalQuoteAmount,
- awardRatio: biddingCompanies.awardRatio
+ awardRatio: biddingCompanies.awardRatio,
+ vendorCode: vendors.vendorCode,
+ companySize: vendors.businessSize,
+ targetPrice: biddings.targetPrice
})
.from(biddingCompanies)
.leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
+ .leftJoin(biddings, eq(biddingCompanies.biddingId, biddings.id))
.where(and(
eq(biddingCompanies.biddingId, biddingId),
eq(biddingCompanies.isWinner, true)
@@ -1381,7 +1388,10 @@ export async function getAwardedCompanies(biddingId: number) {
companyId: company.companyId,
companyName: company.companyName,
finalQuoteAmount: parseFloat(company.finalQuoteAmount?.toString() || '0'),
- awardRatio: parseFloat(company.awardRatio?.toString() || '0')
+ awardRatio: parseFloat(company.awardRatio?.toString() || '0'),
+ vendorCode: company.vendorCode,
+ companySize: company.companySize,
+ targetPrice: company.targetPrice ? parseFloat(company.targetPrice.toString()) : 0
}))
} catch (error) {
console.error('Failed to get awarded companies:', error)
@@ -1410,7 +1420,7 @@ async function updateBiddingAmounts(biddingId: number) {
.set({
targetPrice: totalTargetAmount.toString(),
budget: totalBudgetAmount.toString(),
- finalBidPrice: totalActualAmount.toString(),
+ actualPrice: totalActualAmount.toString(),
updatedAt: new Date()
})
.where(eq(biddings.id, biddingId))
@@ -1693,7 +1703,7 @@ export interface PartnersBiddingListItem {
biddingNumber: string
originalBiddingNumber: string | null // 원입찰번호
revision: number | null
- projectName: string
+ projectName: string | null
itemName: string
title: string
contractType: string
@@ -1782,9 +1792,9 @@ export async function getBiddingListForPartners(companyId: number): Promise<Part
// 계산된 필드 추가
const resultWithCalculatedFields = result.map(item => ({
...item,
- respondedAt: item.respondedAt ? (item.respondedAt instanceof Date ? item.respondedAt.toISOString() : item.respondedAt.toString()) : null,
+ respondedAt: item.respondedAt ? (item.respondedAt instanceof Date ? item.respondedAt.toISOString() : String(item.respondedAt)) : null,
finalQuoteAmount: item.finalQuoteAmount ? Number(item.finalQuoteAmount) : null, // string을 number로 변환
- finalQuoteSubmittedAt: item.finalQuoteSubmittedAt ? (item.finalQuoteSubmittedAt instanceof Date ? item.finalQuoteSubmittedAt.toISOString() : item.finalQuoteSubmittedAt.toString()) : null,
+ finalQuoteSubmittedAt: item.finalQuoteSubmittedAt ? (item.finalQuoteSubmittedAt instanceof Date ? item.finalQuoteSubmittedAt.toISOString() : String(item.finalQuoteSubmittedAt)) : null,
responseDeadline: item.submissionStartDate
? new Date(item.submissionStartDate.getTime() - 3 * 24 * 60 * 60 * 1000) // 3일 전
: null,
@@ -1825,7 +1835,6 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId:
biddingRegistrationDate: biddings.biddingRegistrationDate,
submissionStartDate: biddings.submissionStartDate,
submissionEndDate: biddings.submissionEndDate,
- evaluationDate: biddings.evaluationDate,
// 가격 정보
currency: biddings.currency,
@@ -2596,101 +2605,72 @@ export async function getBiddingDocumentsForPartners(biddingId: number) {
// 입찰가 비교 분석 함수들
// =================================================
-// 벤더별 입찰가 정보 조회 (캐시 적용)
+// 벤더별 입찰가 정보 조회 (최적화 및 간소화됨)
export async function getVendorPricesForBidding(biddingId: number) {
- return unstable_cache(
- async () => {
- try {
- // 각 회사의 입찰가 정보를 조회 - 본입찰 참여 업체들
- const vendorPrices = await db
- .select({
- companyId: biddingCompanies.companyId,
- companyName: vendors.vendorName,
- biddingCompanyId: biddingCompanies.id,
- currency: sql<string>`'KRW'`, // 기본값 KRW
- finalQuoteAmount: biddingCompanies.finalQuoteAmount,
- isBiddingParticipated: biddingCompanies.isBiddingParticipated,
- })
- .from(biddingCompanies)
- .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
- .where(and(
- eq(biddingCompanies.biddingId, biddingId),
- eq(biddingCompanies.isBiddingParticipated, true), // 본입찰 참여 업체만
- sql`${biddingCompanies.finalQuoteAmount} IS NOT NULL` // 입찰가를 제출한 업체만
- ))
+ try {
+ // 1. 본입찰 참여 업체들 조회
+ const participatingVendors = await db
+ .select({
+ companyId: biddingCompanies.companyId,
+ companyName: vendors.vendorName,
+ biddingCompanyId: biddingCompanies.id,
+ currency: sql<string>`'KRW'`, // 기본값 KRW
+ finalQuoteAmount: biddingCompanies.finalQuoteAmount,
+ isBiddingParticipated: biddingCompanies.isBiddingParticipated,
+ })
+ .from(biddingCompanies)
+ .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id))
+ .where(and(
+ eq(biddingCompanies.biddingId, biddingId),
+ eq(biddingCompanies.isBiddingParticipated, true) // 본입찰 참여 업체만
+ ))
- console.log(`Found ${vendorPrices.length} vendors for bidding ${biddingId}`)
+ if (participatingVendors.length === 0) {
+ return []
+ }
- const result: any[] = []
+ const biddingCompanyIds = participatingVendors.map(v => v.biddingCompanyId)
- for (const vendor of vendorPrices) {
- try {
- // 해당 회사의 품목별 입찰가 조회 (본입찰 데이터)
- const itemPrices = await db
- .select({
- prItemId: companyPrItemBids.prItemId,
- itemName: prItemsForBidding.itemInfo, // itemInfo 사용
- itemNumber: prItemsForBidding.itemNumber, // itemNumber도 포함
- quantity: prItemsForBidding.quantity,
- quantityUnit: prItemsForBidding.quantityUnit,
- weight: prItemsForBidding.totalWeight, // totalWeight 사용
- weightUnit: prItemsForBidding.weightUnit,
- unitPrice: companyPrItemBids.bidUnitPrice,
- amount: companyPrItemBids.bidAmount,
- proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate,
- })
- .from(companyPrItemBids)
- .leftJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id))
- .where(and(
- eq(companyPrItemBids.biddingCompanyId, vendor.biddingCompanyId),
- eq(companyPrItemBids.isPreQuote, false) // 본입찰 데이터만
- ))
- .orderBy(prItemsForBidding.id)
-
- console.log(`Vendor ${vendor.companyName}: Found ${itemPrices.length} item prices`)
-
- // 총 금액은 biddingCompanies.finalQuoteAmount 사용
- const totalAmount = parseFloat(vendor.finalQuoteAmount || '0')
-
- result.push({
- companyId: vendor.companyId,
- companyName: vendor.companyName || `Vendor ${vendor.companyId}`,
- biddingCompanyId: vendor.biddingCompanyId,
- totalAmount,
- currency: vendor.currency,
- itemPrices: itemPrices.map(item => ({
- prItemId: item.prItemId,
- itemName: item.itemName || item.itemNumber || `Item ${item.prItemId}`,
- quantity: parseFloat(item.quantity || '0'),
- quantityUnit: item.quantityUnit || 'ea',
- weight: item.weight ? parseFloat(item.weight) : null,
- weightUnit: item.weightUnit,
- unitPrice: parseFloat(item.unitPrice || '0'),
- amount: parseFloat(item.amount || '0'),
- proposedDeliveryDate: item.proposedDeliveryDate ?
- (typeof item.proposedDeliveryDate === 'string'
- ? item.proposedDeliveryDate
- : item.proposedDeliveryDate.toISOString().split('T')[0])
- : null,
- }))
- })
- } catch (vendorError) {
- console.error(`Error processing vendor ${vendor.companyId}:`, vendorError)
- // 벤더 처리 중 에러가 발생해도 다른 벤더들은 계속 처리
- }
- }
+ // 2. 해당 업체들의 입찰 품목 조회 (한 번의 쿼리로 최적화)
+ // 필요한 필드만 조회: prItemId, bidUnitPrice, bidAmount
+ const allItemBids = await db
+ .select({
+ biddingCompanyId: companyPrItemBids.biddingCompanyId,
+ prItemId: companyPrItemBids.prItemId,
+ bidUnitPrice: companyPrItemBids.bidUnitPrice,
+ bidAmount: companyPrItemBids.bidAmount,
+ })
+ .from(companyPrItemBids)
+ .where(and(
+ inArray(companyPrItemBids.biddingCompanyId, biddingCompanyIds),
+ eq(companyPrItemBids.isPreQuote, false) // 본입찰 데이터만
+ ))
- return result
- } catch (error) {
- console.error('Failed to get vendor prices for bidding:', error)
- return []
+ // 3. 업체별로 데이터 매핑
+ const result = participatingVendors.map(vendor => {
+ const vendorItems = allItemBids.filter(item => item.biddingCompanyId === vendor.biddingCompanyId)
+
+ const totalAmount = parseFloat(vendor.finalQuoteAmount || '0')
+
+ return {
+ companyId: vendor.companyId,
+ companyName: vendor.companyName || `Vendor ${vendor.companyId}`,
+ biddingCompanyId: vendor.biddingCompanyId,
+ totalAmount,
+ currency: vendor.currency,
+ itemPrices: vendorItems.map(item => ({
+ prItemId: item.prItemId,
+ unitPrice: parseFloat(item.bidUnitPrice || '0'),
+ amount: parseFloat(item.bidAmount || '0'),
+ }))
}
- },
- [`bidding-vendor-prices-${biddingId}`],
- {
- tags: [`bidding-${biddingId}`, 'quotation-vendors', 'pr-items']
- }
- )()
+ })
+
+ return result
+ } catch (error) {
+ console.error('Failed to get vendor prices for bidding:', error)
+ return []
+ }
}
// 사양설명회 참여 여부 업데이트
@@ -2720,3 +2700,35 @@ export async function setSpecificationMeetingParticipation(biddingCompanyId: num
return { success: false, error: '사양설명회 참여상태 업데이트에 실패했습니다.' }
}
}
+
+// 연동제 정보 업데이트
+export async function updatePriceAdjustmentInfo(params: {
+ biddingCompanyId: number
+ shiPriceAdjustmentApplied: boolean | null
+ priceAdjustmentNote: string | null
+ hasChemicalSubstance: boolean | null
+}): Promise<{ success: boolean; error?: string }> {
+ try {
+ const result = await db.update(biddingCompanies)
+ .set({
+ shiPriceAdjustmentApplied: params.shiPriceAdjustmentApplied,
+ priceAdjustmentNote: params.priceAdjustmentNote,
+ hasChemicalSubstance: params.hasChemicalSubstance,
+ updatedAt: new Date(),
+ })
+ .where(eq(biddingCompanies.id, params.biddingCompanyId))
+ .returning({ biddingId: biddingCompanies.biddingId })
+
+ if (result.length > 0) {
+ const biddingId = result[0].biddingId
+ revalidateTag(`bidding-${biddingId}`)
+ revalidateTag('quotation-vendors')
+ revalidatePath(`/evcp/bid/${biddingId}`)
+ }
+
+ return { success: true }
+ } catch (error) {
+ console.error('Failed to update price adjustment info:', error)
+ return { success: false, error: '연동제 정보 업데이트에 실패했습니다.' }
+ }
+}