From 9cda8482660a87fd98c9ee43f507d75ff75b4e23 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 28 Nov 2025 03:12:57 +0000 Subject: (최겸) 구매 입찰 피드백 반영(90%) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/detail/service.ts | 76 +++++++++++++++++++++- lib/bidding/list/biddings-table-columns.tsx | 20 ++---- lib/bidding/list/edit-bidding-sheet.tsx | 14 +++- lib/bidding/receive/biddings-receive-columns.tsx | 76 +++++++++++++++------- lib/bidding/receive/biddings-receive-table.tsx | 53 +++++++++++++-- .../selection/biddings-selection-columns.tsx | 10 +-- lib/bidding/service.ts | 1 - .../vendor/partners-bidding-list-columns.tsx | 11 +++- 8 files changed, 204 insertions(+), 57 deletions(-) (limited to 'lib/bidding') diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index 0b68eaa7..e425959c 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 } from 'drizzle-orm' +import { eq, and, sql, desc, ne, asc } from 'drizzle-orm' import { revalidatePath, revalidateTag } from 'next/cache' import { unstable_cache } from "@/lib/unstable-cache"; import { sendEmail } from '@/lib/mail/sendEmail' @@ -207,6 +207,80 @@ export async function getBiddingCompaniesData(biddingId: number) { } } +// 입찰 접수 화면용: 모든 초대된 협력사 조회 (필터링 없음, contact 정보 포함) +export async function getAllBiddingCompanies(biddingId: number) { + try { + // 1. 기본 협력사 정보 조회 + const companies = await db + .select({ + id: biddingCompanies.id, + biddingId: biddingCompanies.biddingId, + companyId: biddingCompanies.companyId, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + invitationStatus: biddingCompanies.invitationStatus, + invitedAt: biddingCompanies.invitedAt, + respondedAt: biddingCompanies.respondedAt, + preQuoteAmount: biddingCompanies.preQuoteAmount, + preQuoteSubmittedAt: biddingCompanies.preQuoteSubmittedAt, + isPreQuoteSelected: biddingCompanies.isPreQuoteSelected, + finalQuoteAmount: biddingCompanies.finalQuoteAmount, + finalQuoteSubmittedAt: biddingCompanies.finalQuoteSubmittedAt, + isWinner: biddingCompanies.isWinner, + notes: biddingCompanies.notes, + createdAt: biddingCompanies.createdAt, + updatedAt: biddingCompanies.updatedAt + }) + .from(biddingCompanies) + .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) + .where(eq(biddingCompanies.biddingId, biddingId)) + .orderBy(biddingCompanies.invitedAt) + + // 2. 각 협력사의 첫 번째 contact 정보 조회 + const companiesWithContacts = await Promise.all( + companies.map(async (company) => { + if (!company.companyId) { + return { + ...company, + contactPerson: null, + contactEmail: null, + contactPhone: null + } + } + + // biddingCompaniesContacts에서 첫 번째 contact 조회 + const [firstContact] = await db + .select({ + contactName: biddingCompaniesContacts.contactName, + contactEmail: biddingCompaniesContacts.contactEmail, + contactNumber: biddingCompaniesContacts.contactNumber, + }) + .from(biddingCompaniesContacts) + .where( + and( + eq(biddingCompaniesContacts.biddingId, biddingId), + eq(biddingCompaniesContacts.vendorId, company.companyId) + ) + ) + .orderBy(asc(biddingCompaniesContacts.id)) + .limit(1) + + return { + ...company, + contactPerson: firstContact?.contactName || null, + contactEmail: firstContact?.contactEmail || null, + contactPhone: firstContact?.contactNumber || null + } + }) + ) + + return companiesWithContacts + } catch (error) { + console.error('Failed to get all bidding companies:', error) + return [] + } +} + // prItemsForBidding 테이블에서 품목 정보 조회 (캐시 미적용, always fresh) export async function getPRItemsForBidding(biddingId: number) { try { diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx index 907115b1..9b8c19c5 100644 --- a/lib/bidding/list/biddings-table-columns.tsx +++ b/lib/bidding/list/biddings-table-columns.tsx @@ -256,23 +256,17 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef if (!startDate || !endDate) return - - const now = new Date().toString() - console.log(now, "now") - const startIso = new Date(startDate).toISOString() - const endIso = new Date(endDate).toISOString() + const startObj = new Date(startDate) + const endObj = new Date(endDate) + + // UI 표시용 KST 변환 + const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ') - 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 (
-
- {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/list/edit-bidding-sheet.tsx b/lib/bidding/list/edit-bidding-sheet.tsx index ed3d3f41..23f76f4a 100644 --- a/lib/bidding/list/edit-bidding-sheet.tsx +++ b/lib/bidding/list/edit-bidding-sheet.tsx @@ -367,7 +367,12 @@ export function EditBiddingSheet({ 계약 시작일 - + @@ -381,7 +386,12 @@ export function EditBiddingSheet({ 계약 종료일 - + diff --git a/lib/bidding/receive/biddings-receive-columns.tsx b/lib/bidding/receive/biddings-receive-columns.tsx index 4bde849c..9650574a 100644 --- a/lib/bidding/receive/biddings-receive-columns.tsx +++ b/lib/bidding/receive/biddings-receive-columns.tsx @@ -58,6 +58,7 @@ type BiddingReceiveItem = { interface GetColumnsProps { setRowAction: React.Dispatch | null>> + onParticipantClick?: (biddingId: number, participantType: 'expected' | 'participated' | 'declined' | 'pending') => void } // 상태별 배지 색상 @@ -89,7 +90,7 @@ const formatCurrency = (amount: string | number | null, currency = 'KRW') => { }).format(numAmount) } -export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { +export function getBiddingsReceiveColumns({ setRowAction, onParticipantClick }: GetColumnsProps): ColumnDef[] { return [ // ░░░ 선택 ░░░ @@ -195,24 +196,17 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co if (!startDate || !endDate) return - - const now = new Date() 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 (
-
+
{formatKst(startObj)} ~ {formatKst(endObj)}
- {isActive && ( - 진행중 - )}
) }, @@ -251,10 +245,18 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co id: "participantExpected", header: ({ column }) => , cell: ({ row }) => ( -
- - {row.original.participantExpected} -
+ ), size: 100, meta: { excelHeader: "참여예정협력사" }, @@ -265,10 +267,18 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co id: "participantParticipated", header: ({ column }) => , cell: ({ row }) => ( -
- - {row.original.participantParticipated} -
+ ), size: 100, meta: { excelHeader: "참여협력사" }, @@ -279,10 +289,18 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co id: "participantDeclined", header: ({ column }) => , cell: ({ row }) => ( -
- - {row.original.participantDeclined} -
+ ), size: 100, meta: { excelHeader: "포기협력사" }, @@ -293,10 +311,18 @@ export function getBiddingsReceiveColumns({ setRowAction }: GetColumnsProps): Co id: "participantPending", header: ({ column }) => , cell: ({ row }) => ( -
- - {row.original.participantPending} -
+ ), size: 100, meta: { excelHeader: "미제출협력사" }, diff --git a/lib/bidding/receive/biddings-receive-table.tsx b/lib/bidding/receive/biddings-receive-table.tsx index 5bda921e..2b141d5e 100644 --- a/lib/bidding/receive/biddings-receive-table.tsx +++ b/lib/bidding/receive/biddings-receive-table.tsx @@ -22,7 +22,9 @@ import { contractTypeLabels, } from "@/db/schema" // import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs" -import { openBiddingAction, earlyOpenBiddingAction } from "@/lib/bidding/actions" +import { openBiddingAction } from "@/lib/bidding/actions" +import { BiddingParticipantsDialog } from "@/components/bidding/receive/bidding-participants-dialog" +import { getAllBiddingCompanies } from "@/lib/bidding/detail/service" type BiddingReceiveItem = { id: number @@ -69,17 +71,49 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { const [isCompact, setIsCompact] = React.useState(false) // const [specMeetingDialogOpen, setSpecMeetingDialogOpen] = React.useState(false) // const [prDocumentsDialogOpen, setPrDocumentsDialogOpen] = React.useState(false) - // const [selectedBidding, setSelectedBidding] = React.useState(null) + const [selectedBidding, setSelectedBidding] = React.useState(null) const [rowAction, setRowAction] = React.useState | null>(null) const [isOpeningBidding, setIsOpeningBidding] = React.useState(false) + // 협력사 다이얼로그 관련 상태 + const [participantsDialogOpen, setParticipantsDialogOpen] = React.useState(false) + const [selectedParticipantType, setSelectedParticipantType] = React.useState<'expected' | 'participated' | 'declined' | 'pending' | null>(null) + const [selectedBiddingId, setSelectedBiddingId] = React.useState(null) + const [participantCompanies, setParticipantCompanies] = React.useState([]) + const [isLoadingParticipants, setIsLoadingParticipants] = React.useState(false) + const router = useRouter() const { data: session } = useSession() + // 협력사 클릭 핸들러 + const handleParticipantClick = React.useCallback(async (biddingId: number, participantType: 'expected' | 'participated' | 'declined' | 'pending') => { + setSelectedBiddingId(biddingId) + setSelectedParticipantType(participantType) + setIsLoadingParticipants(true) + setParticipantsDialogOpen(true) + + try { + // 협력사 데이터 로드 (모든 초대된 협력사) + const companies = await getAllBiddingCompanies(biddingId) + + console.log('Loaded companies:', companies) + + // 필터링 없이 모든 데이터 그대로 표시 + // invitationStatus가 그대로 다이얼로그에 표시됨 + setParticipantCompanies(companies) + } catch (error) { + console.error('Failed to load participant companies:', error) + toast.error('협력사 목록을 불러오는데 실패했습니다.') + setParticipantCompanies([]) + } finally { + setIsLoadingParticipants(false) + } + }, []) + const columns = React.useMemo( - () => getBiddingsReceiveColumns({ setRowAction }), - [setRowAction] + () => getBiddingsReceiveColumns({ setRowAction, onParticipantClick: handleParticipantClick }), + [setRowAction, handleParticipantClick] ) // rowAction 변경 감지하여 해당 다이얼로그 열기 @@ -96,7 +130,7 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { break } } - }, [rowAction]) + }, [rowAction, router]) const filterFields: DataTableFilterField[] = [ { @@ -248,6 +282,15 @@ export function BiddingsReceiveTable({ promises }: BiddingsReceiveTableProps) { onOpenChange={handlePrDocumentsDialogClose} bidding={selectedBidding} /> */} + + {/* 참여 협력사 다이얼로그 */} + ) } diff --git a/lib/bidding/selection/biddings-selection-columns.tsx b/lib/bidding/selection/biddings-selection-columns.tsx index 355d5aaa..87c489e3 100644 --- a/lib/bidding/selection/biddings-selection-columns.tsx +++ b/lib/bidding/selection/biddings-selection-columns.tsx @@ -175,23 +175,17 @@ export function getBiddingsSelectionColumns({ setRowAction }: GetColumnsProps): if (!startDate || !endDate) return - - const now = new Date() 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 (
-
+
{formatKst(startObj)} ~ {formatKst(endObj)}
- {isClosed && ( - 마감 - )}
) }, diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 8fd1d368..1ae23e81 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -3635,7 +3635,6 @@ export async function getBiddingsForSelection(input: GetBiddingsSchema) { // 'bidding_opened', 'bidding_closed', 'evaluation_of_bidding', 'vendor_selected' 상태만 조회 basicConditions.push( or( - eq(biddings.status, 'bidding_opened'), eq(biddings.status, 'bidding_closed'), eq(biddings.status, 'evaluation_of_bidding'), eq(biddings.status, 'vendor_selected') diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx index 64b4bebf..a122e87b 100644 --- a/lib/bidding/vendor/partners-bidding-list-columns.tsx +++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx @@ -348,11 +348,18 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL if (!startDate || !endDate) { return
-
} + + const startObj = new Date(startDate) + const endObj = new Date(endDate) + + // 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', ' ')}
+
{formatKst(startObj)}
~
-
{new Date(endDate).toISOString().slice(0, 16).replace('T', ' ')}
+
{formatKst(endObj)}
) }, -- cgit v1.2.3