From b75b1cd920efd61923f7b2dbc4c49987b7b0c4e1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 20 Nov 2025 10:25:41 +0000 Subject: (최겸) 구매 입찰 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/detail/service.ts | 135 ++++----- .../table/bidding-detail-vendor-edit-dialog.tsx | 4 +- .../bidding-detail-vendor-toolbar-actions.tsx | 4 +- .../detail/table/bidding-invitation-dialog.tsx | 337 +-------------------- lib/bidding/list/biddings-table-columns.tsx | 4 +- .../list/biddings-table-toolbar-actions.tsx | 57 +--- lib/bidding/list/biddings-table.tsx | 1 + lib/bidding/list/biddings-transmission-dialog.tsx | 34 +++ lib/bidding/list/create-bidding-dialog.tsx | 4 +- lib/bidding/pre-quote/service.ts | 78 +++-- lib/bidding/selection/actions.ts | 156 +++++----- lib/bidding/selection/bidding-info-card.tsx | 4 +- .../selection/biddings-selection-columns.tsx | 46 +-- lib/bidding/selection/biddings-selection-table.tsx | 22 +- lib/bidding/service.ts | 84 ++++- lib/bidding/validation.ts | 2 +- .../vendor/partners-bidding-attachments-dialog.tsx | 9 +- lib/bidding/vendor/partners-bidding-detail.tsx | 81 ++--- .../vendor/partners-bidding-list-columns.tsx | 2 +- 19 files changed, 425 insertions(+), 639 deletions(-) (limited to 'lib') diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index 39bf0c46..d0f8070f 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -1505,6 +1505,7 @@ export async function getBiddingListForPartners(companyId: number): Promise 0) { - // 기존 스냅샷 조회 - const existingCompany = await tx - .select({ quotationSnapshots: biddingCompanies.quotationSnapshots }) - .from(biddingCompanies) - .where(eq(biddingCompanies.id, biddingCompanyId)) - .limit(1) - - const existingSnapshots = existingCompany[0]?.quotationSnapshots as any[] || [] - - // 새로운 스냅샷 생성 - const newSnapshot = { - id: Date.now().toString(), // 고유 ID - round: existingSnapshots.length + 1, // 차수 - submittedAt: new Date().toISOString(), - totalAmount: response.finalQuoteAmount, - currency: 'KRW', - isFinalSubmission: !!response.isFinalSubmission, - items: response.prItemQuotations.map(item => ({ - prItemId: item.prItemId, - bidUnitPrice: item.bidUnitPrice, - bidAmount: item.bidAmount, - proposedDeliveryDate: item.proposedDeliveryDate, - technicalSpecification: item.technicalSpecification - })) - } - - // 스냅샷 배열에 추가 - companyUpdateData.quotationSnapshots = [...existingSnapshots, newSnapshot] - } + // // 스냅샷은 임시저장/최종제출 관계없이 항상 생성 + // if (response.prItemQuotations && response.prItemQuotations.length > 0) { + // // 기존 스냅샷 조회 + // const existingCompany = await tx + // .select({ quotationSnapshots: biddingCompanies.quotationSnapshots }) + // .from(biddingCompanies) + // .where(eq(biddingCompanies.id, biddingCompanyId)) + // .limit(1) + + // const existingSnapshots = existingCompany[0]?.quotationSnapshots as any[] || [] + + // // 새로운 스냅샷 생성 + // const newSnapshot = { + // id: Date.now().toString(), // 고유 ID + // round: existingSnapshots.length + 1, // 차수 + // submittedAt: new Date().toISOString(), + // totalAmount: response.finalQuoteAmount, + // currency: 'KRW', + // isFinalSubmission: !!response.isFinalSubmission, + // items: response.prItemQuotations.map(item => ({ + // prItemId: item.prItemId, + // bidUnitPrice: item.bidUnitPrice, + // bidAmount: item.bidAmount, + // proposedDeliveryDate: item.proposedDeliveryDate, + // technicalSpecification: item.technicalSpecification + // })) + // } + + // // 스냅샷 배열에 추가 + // companyUpdateData.quotationSnapshots = [...existingSnapshots, newSnapshot] + // } } await tx @@ -2342,47 +2344,40 @@ export async function deleteBiddingDocument(documentId: number, biddingId: numbe } } -// 협력업체용 발주처 문서 조회 (캐시 적용) +// 협력업체용 발주처 문서 조회 (협력업체용 첨부파일만) export async function getBiddingDocumentsForPartners(biddingId: number) { - return unstable_cache( - async () => { - try { - const documents = await db - .select({ - id: biddingDocuments.id, - biddingId: biddingDocuments.biddingId, - companyId: biddingDocuments.companyId, - documentType: biddingDocuments.documentType, - fileName: biddingDocuments.fileName, - originalFileName: biddingDocuments.originalFileName, - fileSize: biddingDocuments.fileSize, - filePath: biddingDocuments.filePath, - title: biddingDocuments.title, - description: biddingDocuments.description, - uploadedAt: biddingDocuments.uploadedAt, - uploadedBy: biddingDocuments.uploadedBy - }) - .from(biddingDocuments) - .where( - and( - eq(biddingDocuments.biddingId, biddingId), - sql`${biddingDocuments.companyId} IS NULL`, // 발주처 문서만 - eq(biddingDocuments.isPublic, true) // 공개 문서만 - ) - ) - .orderBy(desc(biddingDocuments.uploadedAt)) + try { + const documents = await db + .select({ + id: biddingDocuments.id, + biddingId: biddingDocuments.biddingId, + companyId: biddingDocuments.companyId, + documentType: biddingDocuments.documentType, + fileName: biddingDocuments.fileName, + originalFileName: biddingDocuments.originalFileName, + fileSize: biddingDocuments.fileSize, + filePath: biddingDocuments.filePath, + title: biddingDocuments.title, + description: biddingDocuments.description, + uploadedAt: biddingDocuments.uploadedAt, + uploadedBy: biddingDocuments.uploadedBy + }) + .from(biddingDocuments) + .where( + and( + eq(biddingDocuments.biddingId, biddingId), + eq(biddingDocuments.documentType, 'company_proposal'), // 협력업체용 첨부파일만 + sql`${biddingDocuments.companyId} IS NULL`, // 발주처 문서만 + eq(biddingDocuments.isPublic, true) // 공개 문서만 + ) + ) + .orderBy(desc(biddingDocuments.uploadedAt)) - return documents - } catch (error) { - console.error('Failed to get bidding documents for partners:', error) - return [] - } - }, - [`bidding-documents-partners-${biddingId}`], - { - tags: [`bidding-${biddingId}`, 'bidding-documents'] - } - )() + return documents + } catch (error) { + console.error('Failed to get bidding documents for partners:', error) + return [] + } } // ================================================= diff --git a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx index 6e5481f4..5bc85fdb 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx @@ -22,7 +22,7 @@ interface BiddingDetailVendorEditDialogProps { open: boolean onOpenChange: (open: boolean) => void onSuccess: () => void - biddingAwardCount?: string // 낙찰수 정보 추가 + biddingAwardCount?: string // 낙찰업체 수 정보 추가 biddingStatus?: string // 입찰 상태 정보 추가 allVendors?: QuotationVendor[] // 전체 벤더 목록 추가 } @@ -55,7 +55,7 @@ export function BiddingDetailVendorEditDialog({ // vendor가 변경되면 폼 데이터 업데이트 React.useEffect(() => { if (vendor) { - // 낙찰수가 단수인 경우 발주비율을 100%로 자동 설정 + // 낙찰업체 수가 단수인 경우 발주비율을 100%로 자동 설정 const defaultAwardRatio = biddingAwardCount === 'single' ? 100 : (vendor.awardRatio || 0) setFormData({ awardRatio: defaultAwardRatio, diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx index 34ee690f..53fe05f9 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -286,14 +286,14 @@ export function BiddingDetailVendorToolbarActions({ bidding.status === 'bidding_disposal') && (
)} - + */}
diff --git a/lib/bidding/detail/table/bidding-invitation-dialog.tsx b/lib/bidding/detail/table/bidding-invitation-dialog.tsx index ffb1fcb3..582622d9 100644 --- a/lib/bidding/detail/table/bidding-invitation-dialog.tsx +++ b/lib/bidding/detail/table/bidding-invitation-dialog.tsx @@ -33,7 +33,6 @@ import { } from 'lucide-react' import { getExistingBasicContractsForBidding } from '../../pre-quote/service' import { getActiveContractTemplates } from '../../service' -import { getVendorContacts } from '@/lib/vendors/service' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' import { SelectTrigger } from '@/components/ui/select' @@ -269,47 +268,23 @@ export function BiddingInvitationDialog({ })); setSelectedContracts(initialSelected); - // 벤더 담당자 정보 병렬로 가져오기 - const vendorContactsPromises = selectedVendors.map(vendor => - getVendorContacts({ - page: 1, - perPage: 100, - flags: [], - sort: [], - filters: [], - joinOperator: 'and', - search: '', - contactName: '', - contactPosition: '', - contactEmail: '', - contactPhone: '' - }, vendor.vendorId) - .then(result => ({ - vendorId: vendor.vendorId, - contacts: (result.data || []).map(contact => ({ - id: contact.id, - contactName: contact.contactName, - contactEmail: contact.contactEmail, - contactPhone: contact.contactPhone, - contactPosition: contact.contactPosition, - contactDepartment: contact.contactDepartment - })) - })) - .catch(() => ({ - vendorId: vendor.vendorId, - contacts: [] - })) - ); - - const vendorContactsResults = await Promise.all(vendorContactsPromises); - const vendorContactsMap = new Map(vendorContactsResults.map(result => [result.vendorId, result.contacts])); + // 담당자 정보는 selectedVendors에 이미 포함되어 있음 // vendorData 초기화 (담당자 정보 포함) const initialVendorData: VendorWithContactInfo[] = selectedVendors.map(vendor => { const hasExistingContract = typedContracts.some((ec) => ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId ); - const vendorContacts = vendorContactsMap.get(vendor.vendorId) || []; + + // contacts 정보가 이미 selectedVendors에 포함되어 있음 + const vendorContacts = (vendor.contacts || []).map(contact => ({ + id: contact.id, + contactName: contact.contactName, + contactEmail: contact.contactEmail, + contactPhone: contact.contactNumber, + contactPosition: null, + contactDepartment: null + })); // 주 수신자 기본값: 벤더의 기본 이메일 (vendorEmail) const defaultEmail = vendor.vendorEmail || (vendorContacts.length > 0 ? vendorContacts[0].contactEmail : ''); @@ -569,296 +544,6 @@ export function BiddingInvitationDialog({ )} {/* 대상 업체 정보 - 테이블 형식 */} -
-
-
- - 초대 대상 업체 ({vendorData.length}) -
- - - 총 {totalRecipientCount}명 - -
- - {vendorData.length === 0 ? ( -
- 초대 가능한 업체가 없습니다. -
- ) : ( -
- - - - - - - - - - - - {vendorData.map((vendor, index) => { - const allContacts = vendor.contacts || []; - const allEmails = [ - // 벤더의 기본 이메일을 첫 번째로 표시 - ...(vendor.vendorEmail ? [{ - value: vendor.vendorEmail, - label: `${vendor.vendorEmail}`, - email: vendor.vendorEmail, - type: 'vendor' as const - }] : []), - // 담당자 이메일들 - ...allContacts.map(c => ({ - value: c.contactEmail, - label: `${c.contactName} ${c.contactPosition ? `(${c.contactPosition})` : ''}`, - email: c.contactEmail, - type: 'contact' as const - })), - // 커스텀 이메일들 - ...vendor.customEmails.map(c => ({ - value: c.email, - label: c.name || c.email, - email: c.email, - type: 'custom' as const - })) - ]; - - const ccEmails = allEmails.filter(e => e.value !== vendor.selectedMainEmail); - const selectedMainEmailInfo = allEmails.find(e => e.value === vendor.selectedMainEmail); - const isFormOpen = showCustomEmailForm[vendor.vendorId]; - - return ( - - - - - - - - - - {/* 인라인 수신자 추가 폼 */} - {isFormOpen && ( - - - - )} - - ); - })} - -
No.업체명주 수신자CC작업
-
-
- {index + 1} -
-
-
-
-
{vendor.vendorName}
-
- - {vendor.vendorCountry || vendor.vendorCode} - -
-
-
- - {!vendor.selectedMainEmail && ( - 필수 - )} - - - - - - -
- {ccEmails.map((email) => ( -
- toggleAdditionalEmail(vendor.vendorId, email.value)} - className="h-3 w-3" - /> - -
- ))} -
-
-
-
-
- - {vendor.customEmails.length > 0 && ( - - +{vendor.customEmails.length} - - )} -
-
-
-
-
- - 수신자 추가 - {vendor.vendorName} -
- -
- -
-
- - setCustomEmailInputs(prev => ({ - ...prev, - [vendor.vendorId]: { - ...prev[vendor.vendorId], - name: e.target.value - } - }))} - /> -
-
- - setCustomEmailInputs(prev => ({ - ...prev, - [vendor.vendorId]: { - ...prev[vendor.vendorId], - email: e.target.value - } - }))} - onKeyPress={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - addCustomEmail(vendor.vendorId); - } - }} - /> -
- - -
- - {/* 추가된 커스텀 이메일 목록 */} - {vendor.customEmails.length > 0 && ( -
-
추가된 수신자 목록
-
- {vendor.customEmails.map((custom) => ( -
-
- -
-
{custom.name}
-
{custom.email}
-
-
- -
- ))} -
-
- )} -
-
-
- )} -
diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx index 40c7f271..36abd03c 100644 --- a/lib/bidding/list/biddings-table-columns.tsx +++ b/lib/bidding/list/biddings-table-columns.tsx @@ -122,14 +122,14 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef // ░░░ 프로젝트명 ░░░ { accessorKey: "projectName", - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => (
{row.original.projectName || '-'}
), size: 150, - meta: { excelHeader: "프로젝트명" }, + meta: { excelHeader: "프로젝트 No." }, }, // ░░░ 입찰명 ░░░ { diff --git a/lib/bidding/list/biddings-table-toolbar-actions.tsx b/lib/bidding/list/biddings-table-toolbar-actions.tsx index 0cb87b11..3f65f559 100644 --- a/lib/bidding/list/biddings-table-toolbar-actions.tsx +++ b/lib/bidding/list/biddings-table-toolbar-actions.tsx @@ -80,26 +80,12 @@ export function BiddingsTableToolbarActions({ table }: BiddingsTableToolbarActio .getFilteredSelectedRowModel() .rows .map(row => row.original) - }, [table]) + }, [table.getFilteredSelectedRowModel().rows]) // 업체선정이 완료된 입찰만 전송 가능 - const canTransmit = selectedBiddings.length === 1 && selectedBiddings[0].status === 'vendor_selected' - - const handleExport = async () => { - try { - setIsExporting(true) - await exportTableToExcel(table, { - filename: "biddings", - excludeColumns: ["select", "actions"], - }) - toast.success("입찰 목록이 성공적으로 내보내졌습니다.") - } catch { - toast.error("내보내기 중 오류가 발생했습니다.") - } finally { - setIsExporting(false) - } - } - + const canTransmit = true + console.log(canTransmit, 'canTransmit') + console.log(selectedBiddings, 'selectedBiddings') return ( <> @@ -121,41 +107,6 @@ export function BiddingsTableToolbarActions({ table }: BiddingsTableToolbarActio