diff options
Diffstat (limited to 'lib/bidding/detail/service.ts')
| -rw-r--r-- | lib/bidding/detail/service.ts | 254 |
1 files changed, 87 insertions, 167 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index a0aa3378..68c55fb0 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -30,43 +30,97 @@ 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, + // 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) + )) + .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, + 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 // 예상액 @@ -103,66 +157,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,86 +275,12 @@ export async function getAllBiddingCompanies(biddingId: number) { } } -// prItemsForBidding 테이블에서 품목 정보 조회 (캐시 미적용, always fresh) -export async function getPRItemsForBidding(biddingId: number) { - try { - const items = await db - .select() - .from(prItemsForBidding) - .where(eq(prItemsForBidding.biddingId, biddingId)) - .orderBy(prItemsForBidding.id) +// prItemsForBidding 테이블에서 품목 정보 조회 (deprecated - import from pre-quote/service) +// export async function getPRItemsForBidding(biddingId: number) { ... } - return items - } catch (error) { - console.error('Failed to get PR items for bidding:', error) - return [] - } -} +// 견적 시스템에서 협력업체 정보를 가져오는 함수 (Deprecated - integrated into getBiddingDetailData) +// export async function getQuotationVendors(biddingId: number): Promise<QuotationVendor[]> { ... } -// 견적 시스템에서 협력업체 정보를 가져오는 함수 (캐시 적용) -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)) - - 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) { @@ -1693,7 +1613,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 +1702,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, |
