From daabc02e9ae54f216ada77aa826b349f37c3281a Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 27 Nov 2025 09:43:55 +0000 Subject: (최겸) 구매 입찰 피드백 반영(80%완) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/actions.ts | 3 +- lib/bidding/detail/service.ts | 7 +- lib/bidding/list/biddings-table-columns.tsx | 11 +- lib/bidding/pre-quote/service.ts | 87 ++++++--- lib/bidding/receive/biddings-receive-columns.tsx | 12 +- lib/bidding/receive/biddings-receive-table.tsx | 13 +- .../selection/biddings-selection-columns.tsx | 9 +- lib/bidding/service.ts | 87 +++++++-- .../vendor/components/pr-items-pricing-table.tsx | 140 +++++++------- lib/bidding/vendor/partners-bidding-detail.tsx | 205 ++++++++++++--------- .../vendor/partners-bidding-list-columns.tsx | 52 +++++- 11 files changed, 403 insertions(+), 223 deletions(-) (limited to 'lib/bidding') diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index 0bf2af57..02501b27 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -683,7 +683,8 @@ export async function openBiddingAction(biddingId: number) { } const now = new Date() - const isDeadlinePassed = bidding.submissionEndDate && now > bidding.submissionEndDate + const submissionEndDate = bidding.submissionEndDate ? new Date(bidding.submissionEndDate) : null + const isDeadlinePassed = submissionEndDate && now > submissionEndDate // 2. 개찰 가능 여부 확인 if (!isDeadlinePassed) { diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index c9aaa66c..0b68eaa7 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -36,7 +36,7 @@ export interface BiddingDetailData { } // getBiddingById 함수 임포트 (기존 함수 재사용) -import { getBiddingById } from '@/lib/bidding/service' +import { getBiddingById, updateBiddingProjectInfo } from '@/lib/bidding/service' // Promise.all을 사용하여 모든 데이터를 병렬로 조회 (캐시 적용) export async function getBiddingDetailData(biddingId: number): Promise { @@ -1375,6 +1375,9 @@ export async function updatePrItem(prItemId: number, input: Partial- - const now = new Date() - const isActive = now >= new Date(startDate) && now <= new Date(endDate) - const isPast = now > new Date(endDate) + const now = new Date().toString() + console.log(now, "now") + const startIso = new Date(startDate).toISOString() + const endIso = new Date(endDate).toISOString() + const isActive = new Date(now) >= new Date(startIso) && new Date(now) <= new Date(endIso) + console.log(isActive, "isActive") + const isPast = new Date(now) > new Date(endIso) + console.log(isPast, "isPast") return (
diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts index 1dd06b3c..0f938b24 100644 --- a/lib/bidding/pre-quote/service.ts +++ b/lib/bidding/pre-quote/service.ts @@ -840,37 +840,66 @@ export async function setPreQuoteParticipation( } // PR 아이템 조회 (입찰에 포함된 품목들) -export async function getPrItemsForBidding(biddingId: number) { +export async function getPrItemsForBidding(biddingId: number, companyId?: number) { try { - const prItems = await db - .select({ - id: prItemsForBidding.id, - biddingId: prItemsForBidding.biddingId, - itemNumber: prItemsForBidding.itemNumber, - projectId: prItemsForBidding.projectId, - projectInfo: prItemsForBidding.projectInfo, - itemInfo: prItemsForBidding.itemInfo, - shi: prItemsForBidding.shi, - materialGroupNumber: prItemsForBidding.materialGroupNumber, - materialGroupInfo: prItemsForBidding.materialGroupInfo, - materialNumber: prItemsForBidding.materialNumber, - materialInfo: prItemsForBidding.materialInfo, - requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate, - annualUnitPrice: prItemsForBidding.annualUnitPrice, - currency: prItemsForBidding.currency, - quantity: prItemsForBidding.quantity, - quantityUnit: prItemsForBidding.quantityUnit, - totalWeight: prItemsForBidding.totalWeight, - weightUnit: prItemsForBidding.weightUnit, - priceUnit: prItemsForBidding.priceUnit, - purchaseUnit: prItemsForBidding.purchaseUnit, - materialWeight: prItemsForBidding.materialWeight, - prNumber: prItemsForBidding.prNumber, - hasSpecDocument: prItemsForBidding.hasSpecDocument, - }) - .from(prItemsForBidding) - .where(eq(prItemsForBidding.biddingId, biddingId)) + const selectFields: any = { + id: prItemsForBidding.id, + biddingId: prItemsForBidding.biddingId, + itemNumber: prItemsForBidding.itemNumber, + projectId: prItemsForBidding.projectId, + projectInfo: prItemsForBidding.projectInfo, + itemInfo: prItemsForBidding.itemInfo, + shi: prItemsForBidding.shi, + materialGroupNumber: prItemsForBidding.materialGroupNumber, + materialGroupInfo: prItemsForBidding.materialGroupInfo, + materialNumber: prItemsForBidding.materialNumber, + materialInfo: prItemsForBidding.materialInfo, + requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate, + annualUnitPrice: prItemsForBidding.annualUnitPrice, + currency: prItemsForBidding.currency, + quantity: prItemsForBidding.quantity, + quantityUnit: prItemsForBidding.quantityUnit, + totalWeight: prItemsForBidding.totalWeight, + weightUnit: prItemsForBidding.weightUnit, + priceUnit: prItemsForBidding.priceUnit, + purchaseUnit: prItemsForBidding.purchaseUnit, + materialWeight: prItemsForBidding.materialWeight, + targetUnitPrice: prItemsForBidding.targetUnitPrice, + targetAmount: prItemsForBidding.targetAmount, + targetCurrency: prItemsForBidding.targetCurrency, + budgetAmount: prItemsForBidding.budgetAmount, + budgetCurrency: prItemsForBidding.budgetCurrency, + actualAmount: prItemsForBidding.actualAmount, + actualCurrency: prItemsForBidding.actualCurrency, + prNumber: prItemsForBidding.prNumber, + hasSpecDocument: prItemsForBidding.hasSpecDocument, + specification: prItemsForBidding.specification, + } + + if (companyId) { + selectFields.bidUnitPrice = companyPrItemBids.bidUnitPrice + selectFields.bidAmount = companyPrItemBids.bidAmount + selectFields.proposedDeliveryDate = companyPrItemBids.proposedDeliveryDate + selectFields.technicalSpecification = companyPrItemBids.technicalSpecification + } + + let query = db.select(selectFields).from(prItemsForBidding) + + if (companyId) { + query = query + .leftJoin(biddingCompanies, and( + eq(biddingCompanies.biddingId, biddingId), + eq(biddingCompanies.companyId, companyId) + )) + .leftJoin(companyPrItemBids, and( + eq(companyPrItemBids.prItemId, prItemsForBidding.id), + eq(companyPrItemBids.biddingCompanyId, biddingCompanies.id) + )) as any + } + + query = query.where(eq(prItemsForBidding.biddingId, biddingId)).orderBy(prItemsForBidding.id) as any + const prItems = await query return prItems } catch (error) { console.error('Failed to get PR items for bidding:', error) diff --git a/lib/bidding/receive/biddings-receive-columns.tsx b/lib/bidding/receive/biddings-receive-columns.tsx index ab2c0d02..4bde849c 100644 --- a/lib/bidding/receive/biddings-receive-columns.tsx +++ b/lib/bidding/receive/biddings-receive-columns.tsx @@ -196,13 +196,19 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co if (!startDate || !endDate) return - const now = new Date() - const isActive = now >= new Date(startDate) && now <= new Date(endDate) - const isPast = now > new Date(endDate) + const startObj = new Date(startDate) + const endObj = new Date(endDate) + + const isActive = now >= startObj && now <= endObj + const isPast = now > endObj + + // UI 표시용 KST 변환 + const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ') return (
- {new Date(startDate).toISOString().slice(0, 16).replace('T', ' ')} ~ {new Date(endDate).toISOString().slice(0, 16).replace('T', ' ')} + {formatKst(startObj)} ~ {formatKst(endObj)}
{isActive && ( 진행중 diff --git a/lib/bidding/receive/biddings-receive-table.tsx b/lib/bidding/receive/biddings-receive-table.tsx index 97d627ea..5bda921e 100644 --- a/lib/bidding/receive/biddings-receive-table.tsx +++ b/lib/bidding/receive/biddings-receive-table.tsx @@ -164,17 +164,6 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { setIsCompact(compact) }, []) - // const handleSpecMeetingDialogClose = React.useCallback(() => { - // setSpecMeetingDialogOpen(false) - // setRowAction(null) - // setSelectedBidding(null) - // }, []) - - // const handlePrDocumentsDialogClose = React.useCallback(() => { - // setPrDocumentsDialogOpen(false) - // setRowAction(null) - // setSelectedBidding(null) - // }, []) // 선택된 행 가져오기 const selectedRows = table.getFilteredSelectedRowModel().rows @@ -185,7 +174,7 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { if (!selectedBiddingForAction) return false const now = new Date() - const submissionEndDate = selectedBiddingForAction.submissionEndDate + const submissionEndDate = selectedBiddingForAction.submissionEndDate ? new Date(selectedBiddingForAction.submissionEndDate) : null // 1. 입찰 마감일이 지났으면 무조건 가능 if (submissionEndDate && now > submissionEndDate) return true diff --git a/lib/bidding/selection/biddings-selection-columns.tsx b/lib/bidding/selection/biddings-selection-columns.tsx index 9efa849b..355d5aaa 100644 --- a/lib/bidding/selection/biddings-selection-columns.tsx +++ b/lib/bidding/selection/biddings-selection-columns.tsx @@ -176,13 +176,18 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps): if (!startDate || !endDate) return - const now = new Date() - const isPast = now > new Date(endDate) + const startObj = new Date(startDate) + const endObj = new Date(endDate) + const isPast = now > endObj const isClosed = isPast + // UI 표시용 KST 변환 + const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ') + return (
- {formatDate(startDate, "KR")} ~ {formatDate(endDate, "KR")} + {formatKst(startObj)} ~ {formatKst(endObj)}
{isClosed && ( 마감 diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 489268c6..8fd1d368 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -797,6 +797,22 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { return null } } + + // 담당자 정보 준비 + let bidPicId = input.bidPicId ? parseInt(input.bidPicId.toString()) : null + let bidPicName = input.bidPicName || null + + if (!bidPicId && input.bidPicCode) { + try { + const userInfo = await findUserInfoByEKGRP(input.bidPicCode) + if (userInfo) { + bidPicId = userInfo.userId + bidPicName = userInfo.userName + } + } catch (e) { + console.error('Failed to find user info by EKGRP:', e) + } + } // 1. 입찰 생성 const [newBidding] = await tx @@ -849,8 +865,8 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { purchasingOrganization: input.purchasingOrganization, // 담당자 정보 (user FK) - bidPicId: input.bidPicId ? parseInt(input.bidPicId.toString()) : null, - bidPicName: input.bidPicName || null, + bidPicId, + bidPicName, bidPicCode: input.bidPicCode || null, supplyPicId: input.supplyPicId ? parseInt(input.supplyPicId.toString()) : null, supplyPicName: input.supplyPicName || null, @@ -1234,6 +1250,24 @@ export async function updateBidding(input: UpdateBiddingInput, userId: string) { // 담당자 정보 (user FK) if (input.bidPicId !== undefined) updateData.bidPicId = input.bidPicId if (input.bidPicName !== undefined) updateData.bidPicName = input.bidPicName + + // bidPicCode가 있으면 담당자 정보 자동 조회 및 업데이트 + if (input.bidPicCode !== undefined) { + updateData.bidPicCode = input.bidPicCode + // bidPicId가 명시적으로 제공되지 않았고 코드가 있는 경우 자동 조회 + if (!input.bidPicId && input.bidPicCode) { + try { + const userInfo = await findUserInfoByEKGRP(input.bidPicCode) + if (userInfo) { + updateData.bidPicId = userInfo.userId + updateData.bidPicName = userInfo.userName + } + } catch (e) { + console.error('Failed to find user info by EKGRP:', e) + } + } + } + if (input.supplyPicId !== undefined) updateData.supplyPicId = input.supplyPicId if (input.supplyPicName !== undefined) updateData.supplyPicName = input.supplyPicName @@ -1927,22 +1961,12 @@ export async function updateBiddingSchedule( try { const userName = await getUserNameById(userId) - // 날짜 문자열을 Date 객체로 수동 변환 + // 날짜 문자열을 Date 객체로 변환 (KST 기준) const parseDate = (dateStr?: string) => { if (!dateStr) return undefined - // 'YYYY-MM-DDTHH:mm' 또는 'YYYY-MM-DD HH:mm' 등을 허용 - // 잘못된 포맷이면 undefined 반환 - const m = dateStr.match( - /^(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})(?::(\d{2}))?$/ - ) - if (!m) return undefined - const year = parseInt(m[1], 10) - const month = parseInt(m[2], 10) - 1 // JS month는 0부터 - const day = parseInt(m[3], 10) - const hour = parseInt(m[4], 10) - const min = parseInt(m[5], 10) - const sec = m[6] ? parseInt(m[6], 10) : 0 - return new Date(Date.UTC(year, month, day, hour, min, sec)) + // 'YYYY-MM-DDTHH:mm' 형식을 가정하고 KST(+09:00) 오프셋을 붙여서 파싱 + // 초(:00)를 추가하여 ISO 8601 호환성 확보 + return new Date(`${dateStr}:00+09:00`) } return await db.transaction(async (tx) => { @@ -2178,6 +2202,34 @@ export async function removeBiddingItem(itemId: number) { } } +// 입찰의 첫 번째 PR 아이템 프로젝트 정보로 bidding 업데이트 +export async function updateBiddingProjectInfo(biddingId: number) { + try { + const firstItem = await db + .select({ + projectInfo: prItemsForBidding.projectInfo + }) + .from(prItemsForBidding) + .where(eq(prItemsForBidding.biddingId, biddingId)) + .orderBy(prItemsForBidding.id) + .limit(1) + + if (firstItem.length > 0 && firstItem[0].projectInfo) { + await db + .update(biddings) + .set({ + projectName: firstItem[0].projectInfo, + updatedAt: new Date() + }) + .where(eq(biddings.id, biddingId)) + + console.log(`Bidding ${biddingId} project info updated to: ${firstItem[0].projectInfo}`) + } + } catch (error) { + console.error('Failed to update bidding project info:', error) + } +} + // 입찰의 PR 아이템 금액 합산하여 bidding 업데이트 async function updateBiddingAmounts(biddingId: number) { try { @@ -2289,6 +2341,9 @@ export async function addPRItemForBidding( // PR 아이템 금액 합산하여 bidding 업데이트 await updateBiddingAmounts(biddingId) + // 프로젝트 정보 업데이트 + await updateBiddingProjectInfo(biddingId) + revalidatePath(`/evcp/bid/${biddingId}/info`) revalidatePath(`/evcp/bid/${biddingId}`) diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx index a0230478..7dd8384e 100644 --- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx +++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx @@ -42,7 +42,7 @@ interface PrItem { materialGroupInfo: string | null materialNumber: string | null materialInfo: string | null - requestedDeliveryDate: Date | null + requestedDeliveryDate: Date | string | null annualUnitPrice: string | null currency: string | null quantity: string | null @@ -54,6 +54,11 @@ interface PrItem { materialWeight: string | null prNumber: string | null hasSpecDocument: boolean | null + specification: string | null + bidUnitPrice?: string | number | null + bidAmount?: string | number | null + proposedDeliveryDate?: string | Date | null + technicalSpecification?: string | null } interface PrItemQuotation { @@ -189,6 +194,18 @@ export function PrItemsPricingTable({ if (existing) { return existing } + + // prItems 자체에 견적 정보가 있는 경우 활용 + if (item.bidUnitPrice !== undefined || item.bidAmount !== undefined) { + return { + prItemId: item.id, + bidUnitPrice: item.bidUnitPrice ? Number(item.bidUnitPrice) : 0, + bidAmount: item.bidAmount ? Number(item.bidAmount) : 0, + proposedDeliveryDate: item.proposedDeliveryDate ? (item.proposedDeliveryDate instanceof Date ? item.proposedDeliveryDate.toISOString().split('T')[0] : String(item.proposedDeliveryDate)) : '', + technicalSpecification: item.technicalSpecification || '' + } + } + return { prItemId: item.id, bidUnitPrice: 0, @@ -288,22 +305,22 @@ export function PrItemsPricingTable({ - 아이템번호 - PR번호 - 품목정보 - 자재내역 + 자재번호 + 자재명 + SHI 납품예정일 + 업체 납품예정일 수량 - 단위 + 구매단위 가격단위 - 중량 - 중량단위 구매단위 - SHI 납품요청일 + 총중량 + 중량단위 입찰단가 입찰금액 - 납품예정일 - {/* 기술사양 */} - SPEC + 업체 통화 + 자재내역상세 + 스팩 + P/R번호 @@ -318,35 +335,46 @@ export function PrItemsPricingTable({ return ( - - {item.itemNumber || '-'} - - {item.prNumber || '-'} -
- {item.itemInfo || '-'} -
+ {item.materialNumber || '-'}
{item.materialInfo || '-'}
+ + {item.requestedDeliveryDate ? + formatDate(new Date(item.requestedDeliveryDate), 'KR') : '-' + } + + + {readOnly ? ( + quotation.proposedDeliveryDate ? + formatDate(quotation.proposedDeliveryDate, 'KR') : '-' + ) : ( + updateQuotation( + item.id, + 'proposedDeliveryDate', + e.target.value + )} + className="w-40" + /> + )} + {item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'} {item.quantityUnit || '-'} {item.priceUnit || '-'} + {item.purchaseUnit || '-'} {item.totalWeight ? parseFloat(item.totalWeight).toLocaleString() : '-'} {item.weightUnit || '-'} - {item.purchaseUnit || '-'} - - {item.requestedDeliveryDate ? - formatDate(item.requestedDeliveryDate, 'KR') : '-' - } - {readOnly ? ( @@ -355,12 +383,23 @@ export function PrItemsPricingTable({ ) : ( updateQuotation( - item.id, - 'bidUnitPrice', - parseFloat(e.target.value) || 0 - )} + inputMode="decimal" + min={0} + pattern="^(0|[1-9][0-9]*)(\.[0-9]+)?$" + value={quotation.bidUnitPrice === 0 ? '' : quotation.bidUnitPrice} + onChange={(e) => { + let value = e.target.value + if (/^0[0-9]+/.test(value)) { + value = value.replace(/^0+/, '') + if (value === '') value = '0' + } + const numericValue = parseFloat(value) + updateQuotation( + item.id, + 'bidUnitPrice', + isNaN(numericValue) ? 0 : numericValue + ) + }} className="w-32 text-right" placeholder="단가" /> @@ -371,42 +410,12 @@ export function PrItemsPricingTable({ {formatCurrency(quotation.bidAmount)} + {currency} - {readOnly ? ( - quotation.proposedDeliveryDate ? - formatDate(quotation.proposedDeliveryDate, 'KR') : '-' - ) : ( - updateQuotation( - item.id, - 'proposedDeliveryDate', - e.target.value - )} - className="w-40" - /> - )} +
+ {item.specification || '-'} +
- {/* - {readOnly ? ( -
- {quotation.technicalSpecification || '-'} -
- ) : ( -