From 10cb50753ccf318024c4394282f9e8d968dcd1a5 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 17 Sep 2025 10:40:12 +0000 Subject: (최겸) 구매 입찰 오류 수정 및 선적지,하역지 연동,TO Cont, TO PO 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/actions.ts | 172 +++++++++----- lib/bidding/detail/service.ts | 69 ++++-- .../detail/table/bidding-detail-vendor-columns.tsx | 2 +- .../bidding-detail-vendor-toolbar-actions.tsx | 8 + .../detail/table/bidding-invitation-dialog.tsx | 98 ++++---- lib/bidding/list/biddings-page-header.tsx | 4 +- lib/bidding/list/biddings-table-columns.tsx | 21 +- .../list/biddings-table-toolbar-actions.tsx | 6 +- lib/bidding/list/biddings-table.tsx | 17 +- lib/bidding/list/biddings-transmission-dialog.tsx | 14 +- lib/bidding/list/create-bidding-dialog.tsx | 249 +++++++++++++++++--- lib/bidding/list/edit-bidding-sheet.tsx | 37 ++- lib/bidding/pre-quote/service.ts | 16 +- .../table/bidding-pre-quote-vendor-columns.tsx | 2 +- lib/bidding/service.ts | 255 +++++++++------------ lib/bidding/validation.ts | 10 +- .../vendor/components/pr-items-pricing-table.tsx | 5 +- .../vendor/components/simple-file-upload.tsx | 3 +- .../vendor/partners-bidding-attachments-dialog.tsx | 1 + .../vendor/partners-bidding-attendance-dialog.tsx | 19 +- lib/bidding/vendor/partners-bidding-detail.tsx | 68 +----- .../vendor/partners-bidding-list-columns.tsx | 55 ++--- lib/bidding/vendor/partners-bidding-list.tsx | 132 +++-------- .../partners-bidding-participation-dialog.tsx | 248 -------------------- lib/bidding/vendor/partners-bidding-pre-quote.tsx | 178 +++++++++++--- .../vendor/partners-bidding-toolbar-actions.tsx | 16 +- 26 files changed, 832 insertions(+), 873 deletions(-) delete mode 100644 lib/bidding/vendor/partners-bidding-participation-dialog.tsx (limited to 'lib') diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index 9aabd469..65ff3138 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -1,26 +1,24 @@ "use server" import db from "@/db/db" -import { eq, and } from "drizzle-orm" +import { eq, and, sql } from "drizzle-orm" import { biddings, biddingCompanies, prItemsForBidding, vendors, generalContracts, - generalContractItems + generalContractItems, + biddingConditions } from "@/db/schema" import { createPurchaseOrder } from "@/lib/soap/ecc/send/create-po" import { getCurrentSAPDate } from "@/lib/soap/utils" +import { generateContractNumber } from "@/lib/general-contracts/service" -// TO Contract 서버 액션 +// TO Contract export async function transmitToContract(biddingId: number, userId: number) { - console.log('=== transmitToContract STARTED ===') - console.log('biddingId:', biddingId, 'userId:', userId) - try { // 1. 입찰 정보 조회 (단순 쿼리) - console.log('Querying bidding...') const bidding = await db.select() .from(biddings) .where(eq(biddings.id, biddingId)) @@ -31,14 +29,19 @@ export async function transmitToContract(biddingId: number, userId: number) { } const biddingData = bidding[0] - console.log('biddingData', biddingData) - // 2. 낙찰된 업체들 조회 (별도 쿼리) - console.log('Querying bidding companies...') - let winnerCompaniesData = [] + // 2. 입찰 조건 정보 조회 + const biddingConditionData = await db.select() + .from(biddingConditions) + .where(eq(biddingConditions.biddingId, biddingId)) + .limit(1) + + const biddingCondition = biddingConditionData.length > 0 ? biddingConditionData[0] : null + + // 3. 낙찰된 업체들 조회 (별도 쿼리) + let winnerCompaniesData: { companyId: number; finalQuoteAmount: string | null; vendorCode: string | null; vendorName: string | null; }[] = [] try { // 2.1 biddingCompanies만 먼저 조회 (join 제거) - console.log('Step 1: Querying biddingCompanies only...') const biddingCompaniesRaw = await db.select() .from(biddingCompanies) .where( @@ -48,12 +51,9 @@ export async function transmitToContract(biddingId: number, userId: number) { ) ) - console.log('biddingCompaniesRaw:', biddingCompaniesRaw) // 2.2 각 company에 대한 vendor 정보 개별 조회 for (const bc of biddingCompaniesRaw) { - console.log('Processing companyId:', bc.companyId) - try { const vendorData = await db.select() .from(vendors) @@ -61,13 +61,11 @@ export async function transmitToContract(biddingId: number, userId: number) { .limit(1) const vendor = vendorData.length > 0 ? vendorData[0] : null - console.log('Vendor data for', bc.companyId, ':', vendor) - winnerCompaniesData.push({ companyId: bc.companyId, finalQuoteAmount: bc.finalQuoteAmount, vendorCode: vendor?.vendorCode || null, - vendorName: vendor?.vendorName || null, + vendorName: vendor?.vendorName || null as string | null, }) } catch (vendorError) { console.error('Vendor query error for', bc.companyId, ':', vendorError) @@ -75,22 +73,18 @@ export async function transmitToContract(biddingId: number, userId: number) { winnerCompaniesData.push({ companyId: bc.companyId, finalQuoteAmount: bc.finalQuoteAmount, - vendorCode: null, - vendorName: null, + vendorCode: null as string | null, + vendorName: null as string | null, }) } } - console.log('winnerCompaniesData type:', typeof winnerCompaniesData) - console.log('winnerCompaniesData length:', winnerCompaniesData?.length) - console.log('winnerCompaniesData:', winnerCompaniesData) } catch (queryError) { console.error('Query error:', queryError) throw new Error(`biddingCompanies 쿼리 실패: ${queryError}`) } // 상태 검증 - console.log('biddingData.status', biddingData.status) if (biddingData.status !== 'vendor_selected') { throw new Error("업체 선정이 완료되지 않은 입찰입니다.") } @@ -100,32 +94,64 @@ export async function transmitToContract(biddingId: number, userId: number) { throw new Error("낙찰된 업체가 없습니다.") } - console.log('Processing', winnerCompaniesData.length, 'winner companies') for (const winnerCompany of winnerCompaniesData) { - // 계약 번호 자동 생성 (현재 시간 기반) - const contractNumber = `CONTRACT-BID-${Date.now()}-${winnerCompany.companyId}` - console.log('contractNumber', contractNumber) + // 계약 번호 자동 생성 (실제 규칙에 맞게) + const contractNumber = await generateContractNumber(userId.toString(), biddingData.contractType) + console.log('Generated contractNumber:', contractNumber) // general-contract 생성 const contractResult = await db.insert(generalContracts).values({ contractNumber, revision: 0, contractSourceType: 'bid', // 입찰에서 생성됨 status: 'Draft', - category: biddingData.contractType as any, // 단가계약, 일반계약, 매각계약 + category: biddingData.contractType || 'general', name: biddingData.title, - selectionMethod: '입찰', vendorId: winnerCompany.companyId, linkedBidNumber: biddingData.biddingNumber, - contractAmount: winnerCompany.finalQuoteAmount || undefined, + contractAmount: winnerCompany.finalQuoteAmount || null, + contractStartDate: biddingData.contractStartDate || null, + contractEndDate: biddingData.contractEndDate || null, currency: biddingData.currency || 'KRW', - registeredById: userId, // TODO: 현재 사용자 ID로 변경 필요 - lastUpdatedById: userId, // TODO: 현재 사용자 ID로 변경 필요 + // 계약 조건 정보 추가 + paymentTerm: biddingCondition?.paymentTerms || null, + taxType: biddingCondition?.taxConditions || 'V0', + deliveryTerm: biddingCondition?.incoterms || 'FOB', + shippingLocation: biddingCondition?.shippingPort || null, + dischargeLocation: biddingCondition?.destinationPort || null, + registeredById: userId, + lastUpdatedById: userId, }).returning({ id: generalContracts.id }) console.log('contractResult', contractResult) const contractId = contractResult[0].id - // 3. PR 아이템들로 general-contract-items 생성 (일단 생략) - console.log('Skipping PR items creation for now') + // 4. PR 아이템들로 general-contract-items 생성 + const prItems = await db.select() + .from(prItemsForBidding) + .where(eq(prItemsForBidding.biddingId, biddingId)) + + if (prItems.length > 0) { + console.log(`Creating ${prItems.length} contract items for contract ${contractId}`) + for (const prItem of prItems) { + await db.insert(generalContractItems).values({ + contractId, + project: prItem.projectInfo || '', + itemCode: prItem.itemNumber || '', + itemInfo: prItem.itemInfo || '', + specification: prItem.materialDescription || '', + quantity: prItem.quantity || null, + quantityUnit: prItem.quantityUnit || '', + contractUnitPrice: prItem.annualUnitPrice || null, + contractAmount: prItem.annualUnitPrice && prItem.quantity + ? (prItem.annualUnitPrice * prItem.quantity) + : null, + contractCurrency: biddingData.currency || 'KRW', + contractDeliveryDate: prItem.requestedDeliveryDate || null, + }) + } + console.log(`Created ${prItems.length} contract items`) + } else { + console.log('No PR items found for this bidding') + } } return { success: true, message: `${winnerCompaniesData.length}개의 계약서가 생성되었습니다.` } @@ -136,59 +162,80 @@ export async function transmitToContract(biddingId: number, userId: number) { } } -// TO PO 서버 액션 +// TO PO export async function transmitToPO(biddingId: number) { try { - // 1. 입찰 정보 및 낙찰 업체 조회 - const bidding = await db.query.biddings.findFirst({ - where: eq(biddings.id, biddingId), - with: { - biddingCompanies: { - where: eq(biddingCompanies.isWinner, true), // 낙찰된 업체만 - with: { - vendor: true - } - }, - prItemsForBidding: true - } - }) + // 1. 입찰 정보 조회 + const biddingData = await db.select() + .from(biddings) + .where(eq(biddings.id, biddingId)) + .limit(1) - if (!bidding) { + if (!biddingData || biddingData.length === 0) { throw new Error("입찰 정보를 찾을 수 없습니다.") } + const bidding = biddingData[0] + if (bidding.status !== 'vendor_selected') { throw new Error("업체 선정이 완료되지 않은 입찰입니다.") } - const winnerCompanies = bidding.biddingCompanies.filter(bc => bc.isWinner) + // 2. 입찰 조건 정보 조회 + const biddingConditionData = await db.select() + .from(biddingConditions) + .where(eq(biddingConditions.biddingId, biddingId)) + .limit(1) + + const biddingCondition = biddingConditionData.length > 0 ? biddingConditionData[0] : null - if (winnerCompanies.length === 0) { + // 3. 낙찰된 업체들 조회 + const winnerCompaniesRaw = await db.select({ + companyId: biddingCompanies.companyId, + finalQuoteAmount: biddingCompanies.finalQuoteAmount, + vendorCode: vendors.vendorCode, + vendorName: vendors.vendorName + }) + .from(biddingCompanies) + .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) + .where( + and( + eq(biddingCompanies.biddingId, biddingId), + eq(biddingCompanies.isWinner, true) + ) + ) + + if (winnerCompaniesRaw.length === 0) { throw new Error("낙찰된 업체가 없습니다.") } - // 2. PO 데이터 구성 + // 4. PR 아이템 조회 + const prItems = await db.select() + .from(prItemsForBidding) + .where(eq(prItemsForBidding.biddingId, biddingId)) + + // 5. PO 데이터 구성 (bidding condition 정보 사용) const poData = { - T_Bidding_HEADER: winnerCompanies.map((company, index) => ({ + T_Bidding_HEADER: winnerCompaniesRaw.map((company, index) => ({ ANFNR: bidding.biddingNumber, - LIFNR: company.vendor?.vendorCode || `VENDOR${company.companyId}`, + LIFNR: company.vendorCode || `VENDOR${company.companyId}`, ZPROC_IND: 'A', // 구매 처리 상태 ANGNR: bidding.biddingNumber, WAERS: bidding.currency || 'KRW', - ZTERM: '0001', // 기본 지급조건 - INCO1: 'FOB', - INCO2: 'Seoul, Korea', - MWSKZ: 'V0', // 세금 코드 + ZTERM: biddingCondition?.paymentTerms || '0001', // 지급조건 + INCO1: biddingCondition?.incoterms || 'FOB', // Incoterms + INCO2: biddingCondition?.destinationPort || biddingCondition?.shippingPort || 'Seoul, Korea', + MWSKZ: biddingCondition?.taxConditions || 'V0', // 세금 코드 LANDS: 'KR', ZRCV_DT: getCurrentSAPDate(), ZATTEN_IND: 'Y', IHRAN: getCurrentSAPDate(), TEXT: `PO from Bidding: ${bidding.title}`, })), - T_Bidding_ITEM: bidding.prItemsForBidding?.map((item, index) => ({ + T_Bidding_ITEM: prItems.map((item, index) => ({ ANFNR: bidding.biddingNumber, ANFPS: (index + 1).toString().padStart(5, '0'), - LIFNR: winnerCompanies[0]?.vendor?.vendorCode || `VENDOR${winnerCompanies[0]?.companyId}`, + LIFNR: winnerCompaniesRaw[0]?.vendorCode || `VENDOR${winnerCompaniesRaw[0]?.companyId}`, NETPR: item.annualUnitPrice?.toString() || '0', PEINH: '1', BPRME: item.quantityUnit || 'EA', @@ -199,7 +246,7 @@ export async function transmitToPO(biddingId: number) { ? ((item.annualUnitPrice * item.quantity) * 1.1).toString() // 10% 부가세 가정 : '0', LFDAT: item.requestedDeliveryDate?.toISOString().split('T')[0] || getCurrentSAPDate(), - })) || [], + })), T_PR_RETURN: [{ ANFNR: bidding.biddingNumber, ANFPS: '00001', @@ -211,6 +258,7 @@ export async function transmitToPO(biddingId: number) { } // 3. SAP으로 PO 전송 + console.log('SAP으로 PO 전송할 poData', poData) const result = await createPurchaseOrder(poData) if (!result.success) { diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index b00a4f4f..a603834c 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -969,7 +969,7 @@ export async function markAsDisposal(biddingId: number, userId: string) { // 입찰 등록 (사전견적에서 선정된 업체들에게 본입찰 초대 발송) export async function registerBidding(biddingId: number, userId: string) { try { - // 사전견적에서 선정된 업체들 조회 + // 사전견적에서 선정된 업체들 + 본입찰에서 개별적으로 추가한 업체들 조회 const selectedCompanies = await db .select({ companyId: biddingCompanies.companyId, @@ -1118,18 +1118,18 @@ export async function createRebidding(biddingId: number, userId: string) { return { success: false, error: '재입찰 업데이트에 실패했습니다.' } } - // 참여 업체들의 상태를 대기로 변경 - await db - .update(biddingCompanies) - .set({ - isBiddingParticipated: null, // 대기 상태로 변경 - invitationStatus: 'sent', - updatedAt: new Date() - }) - .where(and( - eq(biddingCompanies.biddingId, biddingId), - eq(biddingCompanies.isBiddingParticipated, true) - )) + // // 참여 업체들의 상태를 대기로 변경 + // await db + // .update(biddingCompanies) + // .set({ + // isBiddingParticipated: null, // 대기 상태로 변경 + // invitationStatus: 'sent', + // updatedAt: new Date() + // }) + // .where(and( + // eq(biddingCompanies.biddingId, biddingId), + // eq(biddingCompanies.isBiddingParticipated, true) + // )) // 재입찰 안내 메일 발송 for (const company of participantCompanies) { @@ -1686,6 +1686,7 @@ export interface PartnersBiddingListItem { isAttendingMeeting: boolean | null isPreQuoteSelected: boolean | null isPreQuoteParticipated: boolean | null + isBiddingParticipated: boolean | null preQuoteDeadline: Date | null isBiddingInvited: boolean | null notes: string | null @@ -1702,7 +1703,9 @@ export interface PartnersBiddingListItem { title: string contractType: string biddingType: string - contractPeriod: string | null + preQuoteDate: Date | null + contractStartDate: Date | null + contractEndDate: Date | null submissionStartDate: Date | null submissionEndDate: Date | null status: string @@ -1733,6 +1736,7 @@ export async function getBiddingListForPartners(companyId: number): Promise 0) { + const biddingId = result[0].biddingId + revalidateTag(`bidding-${biddingId}`) + revalidateTag('quotation-vendors') + revalidatePath(`/partners/bid/${biddingId}`) + } + + return { + success: true, + message: `사양설명회 참여상태가 ${participated ? '참여' : '불참'}로 업데이트되었습니다.`, + } + } catch (error) { + console.error('Failed to update specification meeting participation:', error) + return { success: false, error: '사양설명회 참여상태 업데이트에 실패했습니다.' } + } +} \ No newline at end of file diff --git a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx index cbdf79c2..3b42cc88 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-columns.tsx @@ -142,7 +142,7 @@ export function getBiddingDetailVendorColumns({ status === 'rejected' ? 'destructive' : 'outline' const label = status === 'selected' ? '선정' : - status === 'submitted' ? '제출' : + status === 'submitted' ? '견적 제출' : status === 'rejected' ? '거절' : '대기' return {label} 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 893fb185..eec44bb1 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -68,6 +68,14 @@ export function BiddingDetailVendorToolbarActions({ } const handleRegister = () => { + if (bidding.status !== 'set_target_price') { + toast({ + title: '오류', + description: '내정가 산정이 완료되어야 입찰 등록을 할 수 있습니다.', + variant: 'destructive', + }) + return + } // 본입찰 초대 다이얼로그 열기 setIsBiddingInvitationDialogOpen(true) } diff --git a/lib/bidding/detail/table/bidding-invitation-dialog.tsx b/lib/bidding/detail/table/bidding-invitation-dialog.tsx index 031231a1..48b235f9 100644 --- a/lib/bidding/detail/table/bidding-invitation-dialog.tsx +++ b/lib/bidding/detail/table/bidding-invitation-dialog.tsx @@ -159,7 +159,7 @@ export function BiddingInvitationDialog({ try { const [contractsResult, templatesData] = await Promise.all([ getSelectedVendorsForBidding(biddingId), - getActiveContractTemplates() + getActiveContractTemplates(), ]); // 기존 계약 조회 (사전견적에서 보낸 기본계약 확인) - 서버 액션 사용 @@ -184,7 +184,6 @@ export function BiddingInvitationDialog({ checked: false })); setSelectedContracts(initialSelected); - } catch (error) { console.error('초기 데이터 로드 실패:', error); toast({ @@ -318,36 +317,34 @@ export function BiddingInvitationDialog({ const handleSendInvitation = () => { const selectedContractTemplates = selectedContracts.filter(c => c.checked); - if (selectedContractTemplates.length === 0) { - toast({ - title: '알림', - description: '발송할 기본계약서를 선택해주세요.', - variant: 'default', - }) - return - } - startTransition(async () => { try { - // 선택된 템플릿에 따라 PDF 생성 - setIsGeneratingPdfs(true) - setPdfGenerationProgress(0) + let generatedPdfs: Array<{ + key: string + buffer: number[] + fileName: string + }> = [] const generatedPdfsMap = new Map() - let generatedCount = 0; - for (const vendor of selectedVendors) { - // 사전견적에서 이미 기본계약을 보낸 벤더인지 확인 - const hasExistingContract = existingContracts.some((ec: any) => - ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId - ); - - if (hasExistingContract) { - console.log(`벤더 ${vendor.vendorName}는 사전견적에서 이미 기본계약을 받았으므로 건너뜁니다.`); - generatedCount++; - setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100); - continue; - } + // 선택된 템플릿이 있는 경우에만 PDF 생성 + if (selectedContractTemplates.length > 0) { + setIsGeneratingPdfs(true) + setPdfGenerationProgress(0) + + let generatedCount = 0; + for (const vendor of selectedVendors) { + // 사전견적에서 이미 기본계약을 보낸 벤더인지 확인 + const hasExistingContract = existingContracts.some((ec: any) => + ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId + ); + + if (hasExistingContract) { + console.log(`벤더 ${vendor.vendorName}는 사전견적에서 이미 기본계약을 받았으므로 건너뜁니다.`); + generatedCount++; + setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100); + continue; + } for (const contract of selectedContractTemplates) { setCurrentGeneratingContract(`${vendor.vendorName} - ${contract.templateName}`); @@ -374,7 +371,16 @@ export function BiddingInvitationDialog({ setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100); } - setIsGeneratingPdfs(false); + setIsGeneratingPdfs(false); + + const pdfsArray = Array.from(generatedPdfsMap.entries()).map(([key, data]) => ({ + key, + buffer: data.buffer, + fileName: data.fileName, + })); + + generatedPdfs = pdfsArray; + } const vendorData = selectedVendors.map(vendor => { const hasExistingContract = existingContracts.some((ec: any) => @@ -400,15 +406,9 @@ export function BiddingInvitationDialog({ }; }); - const pdfsArray = Array.from(generatedPdfsMap.entries()).map(([key, data]) => ({ - key, - buffer: data.buffer, - fileName: data.fileName, - })); - await onSend({ vendors: vendorData, - generatedPdfs: pdfsArray, + generatedPdfs: generatedPdfs, message: additionalMessage }); @@ -428,7 +428,7 @@ export function BiddingInvitationDialog({ return ( - + @@ -448,7 +448,7 @@ export function BiddingInvitationDialog({ 기존 계약 정보 사전견적에서 이미 기본계약을 받은 업체가 있습니다. - 해당 업체들은 계약서 재생성을 건너뜁니다. + 해당 업체들은 계약서 재생성을 건너뜁니다. (본입찰 초대는 정상 진행됩니다) )} @@ -494,18 +494,21 @@ export function BiddingInvitationDialog({

- 기존 계약 존재 (건너뜀) ({vendorsWithExistingContracts.length}개) + 기존 계약 존재 (계약서 재생성 건너뜀) ({vendorsWithExistingContracts.length}개)

{vendorsWithExistingContracts.map((vendor) => ( -
+
- {vendor.vendorName} + {vendor.vendorName} {vendor.vendorCode} - - 계약 존재 + + 계약 존재 (재생성 건너뜀) + + + 본입찰 초대
))} @@ -522,7 +525,7 @@ export function BiddingInvitationDialog({ - 기본계약 선택 + 기본계약 선택 (선택사항) @@ -657,7 +660,7 @@ export function BiddingInvitationDialog({
- {/* {(selectedContractCount > 0) && ( -
-

- {selectedVendors.length}개 업체에 {selectedContractCount}개의 기본계약서를 발송합니다. -

-
- )} */}
diff --git a/lib/bidding/list/biddings-page-header.tsx b/lib/bidding/list/biddings-page-header.tsx index 7fa9a39c..64e588c3 100644 --- a/lib/bidding/list/biddings-page-header.tsx +++ b/lib/bidding/list/biddings-page-header.tsx @@ -19,13 +19,13 @@ export function BiddingsPageHeader() { {/* 우측: 액션 버튼들 */}
- + */}
@@ -1463,7 +1623,10 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
세부내역 관리

- PR 아이템 또는 수기 아이템을 추가하여 입찰 세부내역을 관리하세요 + 최소 하나의 품목을 입력해야 합니다 +

+

+ 수량/단위 또는 중량/중량단위를 선택해서 입력하세요

@@ -1978,7 +2146,14 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions )} )} - {activeTab === "details" && "세부내역 아이템을 관리하세요 (선택사항)"} + {activeTab === "details" && ( + + 최소 하나의 품목을 입력하세요 + {!tabValidation.details.isValid && ( + • 필수 항목이 누락되었습니다 + )} + + )} {activeTab === "manager" && "담당자 정보를 확인하고 입찰을 생성하세요"} diff --git a/lib/bidding/list/edit-bidding-sheet.tsx b/lib/bidding/list/edit-bidding-sheet.tsx index c76ec2a2..dc24d0cf 100644 --- a/lib/bidding/list/edit-bidding-sheet.tsx +++ b/lib/bidding/list/edit-bidding-sheet.tsx @@ -82,7 +82,8 @@ export function EditBiddingSheet({ contractType: "general", biddingType: "equipment", awardCount: "single", - contractPeriod: "", + contractStartDate: "", + contractEndDate: "", preQuoteDate: "", biddingRegistrationDate: "", @@ -126,7 +127,8 @@ export function EditBiddingSheet({ contractType: bidding.contractType || "general", biddingType: bidding.biddingType || "equipment", awardCount: bidding.awardCount || "single", - contractPeriod: bidding.contractPeriod || "", + contractStartDate: formatDate(bidding.contractStartDate, "kr"), + contractEndDate: formatDate(bidding.contractEndDate, "kr"), preQuoteDate: formatDate(bidding.preQuoteDate, "kr"), biddingRegistrationDate: formatDate(bidding.biddingRegistrationDate, "kr"), @@ -356,6 +358,37 @@ export function EditBiddingSheet({ /> + {/* 계약 기간 */} +
+ ( + + 계약 시작일 + + + + + + )} + /> + + ( + + 계약 종료일 + + + + + + )} + /> +
+ (
{row.original.proposedDestinationPort || '-'} diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 55146c4b..89e4f80f 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -13,9 +13,6 @@ import { biddingConditions, users, basicContractTemplates, - paymentTerms, - incoterms, - vendors, vendorsWithTypesView, biddingCompanies } from '@/db/schema' @@ -30,10 +27,11 @@ import { ilike, gte, lte, - SQL, like + SQL, + like, + notInArray } from 'drizzle-orm' import { revalidatePath } from 'next/cache' -import { BiddingListItem } from '@/db/schema' import { filterColumns } from '@/lib/filter-columns' import { CreateBiddingSchema, GetBiddingsSchema, UpdateBiddingSchema } from './validation' import { saveFile } from '../file-stroage' @@ -167,17 +165,17 @@ export async function getBiddings(input: GetBiddingsSchema) { } if (input.submissionDateFrom) { - basicConditions.push(gte(biddingListView.submissionStartDate, input.submissionDateFrom)) + basicConditions.push(gte(biddingListView.submissionStartDate, new Date(input.submissionDateFrom))) } if (input.submissionDateTo) { - basicConditions.push(lte(biddingListView.submissionEndDate, input.submissionDateTo)) + basicConditions.push(lte(biddingListView.submissionEndDate, new Date(input.submissionDateTo))) } if (input.createdAtFrom) { - basicConditions.push(gte(biddingListView.createdAt, input.createdAtFrom)) + basicConditions.push(gte(biddingListView.createdAt, new Date(input.createdAtFrom))) } if (input.createdAtTo) { - basicConditions.push(lte(biddingListView.createdAt, input.createdAtTo)) + basicConditions.push(lte(biddingListView.createdAt, new Date(input.createdAtTo))) } // 가격 범위 필터 @@ -367,118 +365,129 @@ export async function getBiddingMonthlyStats(year: number = new Date().getFullYe } export interface CreateBiddingInput extends CreateBiddingSchema { - // 사양설명회 정보 (선택사항) - specificationMeeting?: { - meetingDate: string - meetingTime: string - location: string - address: string - contactPerson: string - contactPhone: string - contactEmail: string - agenda: string - materials: string - notes: string - isRequired: boolean - meetingFiles: File[] - } | null - - // PR 아이템들 (선택사항) - prItems?: Array<{ - id: string - prNumber: string - itemCode: string - itemInfo: string - quantity: string - quantityUnit: string - totalWeight: string - weightUnit: string - materialDescription: string - hasSpecDocument: boolean - requestedDeliveryDate: string - specFiles: File[] - isRepresentative: boolean - }> - - // 입찰 조건 (선택사항) - biddingConditions?: { - paymentTerms: string - taxConditions: string - incoterms: string - contractDeliveryDate: string - shippingPort: string - destinationPort: string - isPriceAdjustmentApplicable: boolean - sparePartOptions: string - } + // 사양설명회 정보 (선택사항) + specificationMeeting?: { + meetingDate: string + meetingTime: string + location: string + address: string + contactPerson: string + contactPhone: string + contactEmail: string + agenda: string + materials: string + notes: string + isRequired: boolean + meetingFiles: File[] + } | null + + // PR 아이템들 (선택사항) + prItems?: Array<{ + id: string + prNumber: string + itemCode: string + itemInfo: string + quantity: string + quantityUnit: string + totalWeight: string + weightUnit: string + materialDescription: string + hasSpecDocument: boolean + requestedDeliveryDate: string + specFiles: File[] + isRepresentative: boolean + }> + + // 입찰 조건 (선택사항) + biddingConditions?: { + paymentTerms: string + taxConditions: string + incoterms: string + contractDeliveryDate: string + shippingPort: string + destinationPort: string + isPriceAdjustmentApplicable: boolean + sparePartOptions: string } + // 계약 기간 정보 + contractStartDate?: string + contractEndDate?: string +} + export interface UpdateBiddingInput extends UpdateBiddingSchema { id: number } // 자동 입찰번호 생성 -async function generateBiddingNumber(biddingType: string, tx?: any, maxRetries: number = 5): Promise { - const year = new Date().getFullYear() - const typePrefix = { - 'equipment': 'EQ', - 'construction': 'CT', - 'service': 'SV', - 'lease': 'LS', - 'steel_stock': 'SS', - 'piping': 'PP', - 'transport': 'TP', - 'waste': 'WS', - 'sale': 'SL' - }[biddingType] || 'GN' - - const dbInstance = tx || db - const prefix = `${year}${typePrefix}` +async function generateBiddingNumber( + userId?: string, + tx?: any, + maxRetries: number = 5 +): Promise { + // user 테이블의 user.userCode가 있으면 발주담당자 코드로 사용 + // userId가 주어졌을 때 user.userCode를 조회, 없으면 '000' 사용 + let purchaseManagerCode = '000'; + if (userId) { + const user = await db + .select({ userCode: users.userCode }) + .from(users) + .where(eq(users.id, parseInt(userId))) + .limit(1); + if (user[0]?.userCode && user[0].userCode.length >= 3) { + purchaseManagerCode = user[0].userCode.substring(0, 3).toUpperCase(); + } + } + const managerCode = (purchaseManagerCode && purchaseManagerCode.length >= 3) + ? purchaseManagerCode.substring(0, 3).toUpperCase() + : '000'; + + const dbInstance = tx || db; + const prefix = `B${managerCode}`; for (let attempt = 0; attempt < maxRetries; attempt++) { - // 현재 최대 시퀀스 번호 조회 + // 현재 최대 일련번호 조회 const result = await dbInstance .select({ maxNumber: sql`MAX(${biddings.biddingNumber})` }) .from(biddings) - .where(like(biddings.biddingNumber, `${prefix}%`)) + .where(like(biddings.biddingNumber, `${prefix}%`)); - let sequence = 1 + let sequence = 1; if (result[0]?.maxNumber) { - const lastSequence = parseInt(result[0].maxNumber.slice(-4)) + const lastSequence = parseInt(result[0].maxNumber.slice(-5)); if (!isNaN(lastSequence)) { - sequence = lastSequence + 1 + sequence = lastSequence + 1; } } - const biddingNumber = `${prefix}${sequence.toString().padStart(4, '0')}` + const biddingNumber = `${prefix}${sequence.toString().padStart(5, '0')}`; // 중복 확인 const existing = await dbInstance .select({ id: biddings.id }) .from(biddings) .where(eq(biddings.biddingNumber, biddingNumber)) - .limit(1) + .limit(1); if (existing.length === 0) { - return biddingNumber + return biddingNumber; } - // 중복이 발견되면 잠시 대기 후 재시도 - await new Promise(resolve => setTimeout(resolve, 10 + Math.random() * 20)) + // 중복이 발견되면 잠시 대기 후 재시도 (동시성 문제 방지) + await new Promise(resolve => setTimeout(resolve, 10 + Math.random() * 20)); } - throw new Error(`Failed to generate unique bidding number after ${maxRetries} attempts`) + throw new Error(`Failed to generate unique bidding number after ${maxRetries} attempts`); } - // 입찰 생성 export async function createBidding(input: CreateBiddingInput, userId: string) { try { const userName = await getUserNameById(userId) return await db.transaction(async (tx) => { // 자동 입찰번호 생성 - const biddingNumber = await generateBiddingNumber(input.biddingType) + const biddingNumber = await generateBiddingNumber(userId) // 프로젝트 정보 조회 let projectName = input.projectName @@ -541,7 +550,8 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { contractType: input.contractType, biddingType: input.biddingType, awardCount: input.awardCount, - contractPeriod: input.contractPeriod, + contractStartDate: input.contractStartDate ? parseDate(input.contractStartDate) : null, + contractEndDate: input.contractEndDate ? parseDate(input.contractEndDate) : null, // 자동 등록일 설정 biddingRegistrationDate: new Date(), @@ -640,7 +650,7 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { paymentTerms: input.biddingConditions.paymentTerms, taxConditions: input.biddingConditions.taxConditions, incoterms: input.biddingConditions.incoterms, - contractDeliveryDate: input.biddingConditions.contractDeliveryDate ? new Date(input.biddingConditions.contractDeliveryDate) : null, + contractDeliveryDate: input.biddingConditions.contractDeliveryDate || null, shippingPort: input.biddingConditions.shippingPort, destinationPort: input.biddingConditions.destinationPort, isPriceAdjustmentApplicable: input.biddingConditions.isPriceAdjustmentApplicable, @@ -703,7 +713,6 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { isPublic: false, isRequired: false, uploadedBy: userName, - displayOrder: fileIndex + 1, }) } else { console.error(`Failed to save spec file: ${file.name}`, saveResult.error) @@ -796,10 +805,9 @@ export async function updateBidding(input: UpdateBiddingInput, userId: string) { if (input.contractType !== undefined) updateData.contractType = input.contractType if (input.biddingType !== undefined) updateData.biddingType = input.biddingType if (input.awardCount !== undefined) updateData.awardCount = input.awardCount - if (input.contractPeriod !== undefined) updateData.contractPeriod = input.contractPeriod + if (input.contractStartDate !== undefined) updateData.contractStartDate = parseDate(input.contractStartDate) + if (input.contractEndDate !== undefined) updateData.contractEndDate = parseDate(input.contractEndDate) - if (input.preQuoteDate !== undefined) updateData.preQuoteDate = parseDate(input.preQuoteDate) - if (input.biddingRegistrationDate !== undefined) updateData.biddingRegistrationDate = parseDate(input.biddingRegistrationDate) if (input.submissionStartDate !== undefined) updateData.submissionStartDate = parseDate(input.submissionStartDate) if (input.submissionEndDate !== undefined) updateData.submissionEndDate = parseDate(input.submissionEndDate) if (input.evaluationDate !== undefined) updateData.evaluationDate = parseDate(input.evaluationDate) @@ -1039,15 +1047,15 @@ export async function getSpecificationMeetingDetailsAction( biddingId: meetingData.biddingId, meetingDate: meetingData.meetingDate?.toISOString() || '', meetingTime: meetingData.meetingTime, - location: meetingData.location, + location: meetingData.location || '', address: meetingData.address, - contactPerson: meetingData.contactPerson, + contactPerson: meetingData.contactPerson || '', contactPhone: meetingData.contactPhone, contactEmail: meetingData.contactEmail, agenda: meetingData.agenda, materials: meetingData.materials, notes: meetingData.notes, - isRequired: meetingData.isRequired, + isRequired: meetingData.isRequired || false, createdAt: meetingData.createdAt?.toISOString() || '', updatedAt: meetingData.updatedAt?.toISOString() || '', documents: documents.map(doc => ({ @@ -1183,7 +1191,7 @@ export async function getPRDetailsAction( createdAt: doc.createdAt?.toISOString() || '', updatedAt: doc.updatedAt?.toISOString() || '', })), - items: itemsWithDocs + items: itemsWithDocs as any } return { @@ -1200,8 +1208,6 @@ export async function getPRDetailsAction( } } - - /** * 입찰 기본 정보 조회 서버 액션 (선택사항) */ @@ -1241,7 +1247,7 @@ export async function getBiddingBasicInfoAction( return { success: true, - data: bidding + data: bidding as any } } catch (error) { @@ -1306,7 +1312,7 @@ export async function updateBiddingConditions( paymentTerms: updates.paymentTerms, taxConditions: updates.taxConditions, incoterms: updates.incoterms, - contractDeliveryDate: updates.contractDeliveryDate ? new Date(updates.contractDeliveryDate) : null, + contractDeliveryDate: updates.contractDeliveryDate || null, shippingPort: updates.shippingPort, destinationPort: updates.destinationPort, isPriceAdjustmentApplicable: updates.isPriceAdjustmentApplicable, @@ -1324,7 +1330,7 @@ export async function updateBiddingConditions( // 새로 생성 await tx.insert(biddingConditions).values({ biddingId, - ...updateData, + ...updateData as any, }) } @@ -1346,59 +1352,6 @@ export async function updateBiddingConditions( } // 활성 템플릿 조회 서버 액션 -// 입찰 조건 옵션 관련 서버 액션들 -export async function getActivePaymentTerms() { - try { - const result = await db - .select({ - code: paymentTerms.code, - description: paymentTerms.description, - isActive: paymentTerms.isActive, - createdAt: paymentTerms.createdAt, - }) - .from(paymentTerms) - .where(eq(paymentTerms.isActive, true)) - .orderBy(paymentTerms.createdAt) - - return { - success: true, - data: result - } - } catch (error) { - console.error('Error fetching active payment terms:', error) - return { - success: false, - error: '지급조건 조회 중 오류가 발생했습니다.' - } - } -} - -export async function getActiveIncoterms() { - try { - const result = await db - .select({ - code: incoterms.code, - description: incoterms.description, - isActive: incoterms.isActive, - createdAt: incoterms.createdAt, - }) - .from(incoterms) - .where(eq(incoterms.isActive, true)) - .orderBy(incoterms.createdAt) - - return { - success: true, - data: result - } - } catch (error) { - console.error('Error fetching active incoterms:', error) - return { - success: false, - error: '운송조건 조회 중 오류가 발생했습니다.' - } - } -} - export async function getActiveContractTemplates() { try { // 활성 상태의 템플릿들 조회 @@ -1461,7 +1414,7 @@ export async function searchVendorsForBidding(searchTerm: string = "", biddingId and( whereCondition, // 이미 참여중인 벤더 제외 - excludedIds.length > 0 ? sql`${vendorsWithTypesView.id} NOT IN (${excludedIds})` : undefined, + excludedIds.length > 0 ? notInArray(vendorsWithTypesView.id, excludedIds) : undefined, // ACTIVE 상태인 벤더만 검색 // eq(vendorsWithTypesView.status, "ACTIVE"), ) diff --git a/lib/bidding/validation.ts b/lib/bidding/validation.ts index ab330596..2011cd27 100644 --- a/lib/bidding/validation.ts +++ b/lib/bidding/validation.ts @@ -1,4 +1,4 @@ -import { BiddingListView, biddings, type Bidding } from "@/db/schema" +import { BiddingListView, biddings } from "@/db/schema" import { createSearchParamsCache, parseAsArrayOf, @@ -74,7 +74,8 @@ export const createBiddingSchema = z.object({ awardCount: z.enum(biddings.awardCount.enumValues, { required_error: "낙찰수를 선택해주세요" }), - contractPeriod: z.string().min(1, "계약기간은 필수입니다"), + contractStartDate: z.string().optional(), + contractEndDate: z.string().optional(), // ✅ 일정 (제출기간 필수) submissionStartDate: z.string().min(1, "제출시작일시는 필수입니다"), @@ -110,7 +111,7 @@ export const createBiddingSchema = z.object({ incoterms: z.string().min(1, "운송조건은 필수입니다"), contractDeliveryDate: z.string().min(1, "계약납품일은 필수입니다"), shippingPort: z.string().min(1, "선적지는 필수입니다"), - destinationPort: z.string().min(1, "도착지는 필수입니다"), + destinationPort: z.string().min(1, "하역지는 필수입니다"), isPriceAdjustmentApplicable: z.boolean().default(false), sparePartOptions: z.string().optional(), }).optional(), @@ -141,7 +142,8 @@ export const createBiddingSchema = z.object({ contractType: z.enum(biddings.contractType.enumValues).optional(), biddingType: z.enum(biddings.biddingType.enumValues).optional(), awardCount: z.enum(biddings.awardCount.enumValues).optional(), - contractPeriod: z.string().optional(), + contractStartDate: z.string().optional(), + contractEndDate: z.string().optional(), submissionStartDate: z.string().optional(), submissionEndDate: z.string().optional(), diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx index 13804251..483bce5c 100644 --- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx +++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx @@ -4,8 +4,7 @@ import * as React from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { Button } from '@/components/ui/button' -import { Textarea } from '@/components/ui/textarea' + import { Badge } from '@/components/ui/badge' import { Table, @@ -17,7 +16,7 @@ import { } from '@/components/ui/table' import { Package, - FileText, + Download, Calculator } from 'lucide-react' diff --git a/lib/bidding/vendor/components/simple-file-upload.tsx b/lib/bidding/vendor/components/simple-file-upload.tsx index 58b60bdf..1344a491 100644 --- a/lib/bidding/vendor/components/simple-file-upload.tsx +++ b/lib/bidding/vendor/components/simple-file-upload.tsx @@ -5,7 +5,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { Badge } from '@/components/ui/badge' import { Table, TableBody, @@ -15,7 +14,6 @@ import { TableRow, } from '@/components/ui/table' import { - Upload, FileText, Download, Trash2 @@ -171,6 +169,7 @@ export function SimpleFileUpload({ throw new Error('파일 정보가 없습니다.') } } catch (error) { + console.error('파일 다운로드 실패:', error) toast({ title: '다운로드 실패', description: '파일 다운로드에 실패했습니다.', diff --git a/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx b/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx index 951923ca..f5206c71 100644 --- a/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx +++ b/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx @@ -121,6 +121,7 @@ export function PartnersBiddingAttachmentsDialog({ throw new Error('파일 정보가 없습니다.') } } catch (error) { + console.error('파일 다운로드 실패:', error) toast({ title: '다운로드 실패', description: '파일 다운로드에 실패했습니다.', diff --git a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx index 6276e433..d0ef97f1 100644 --- a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx +++ b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx @@ -13,28 +13,24 @@ import { import { Label } from '@/components/ui/label' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Input } from '@/components/ui/input' -import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { ScrollArea } from '@/components/ui/scroll-area' import { Calendar, Users, - MapPin, Clock, - FileText, CheckCircle, XCircle, Download, - User, - Phone } from 'lucide-react' import { formatDate } from '@/lib/utils' import { updatePartnerAttendance, getSpecificationMeetingForPartners } from '../detail/service' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' +import { useRouter } from 'next/navigation' -interface PartnersBiddingAttendanceDialogProps { +interface PartnersSpecificationMeetingDialogProps { biddingDetail: { id: number biddingNumber: string @@ -48,22 +44,20 @@ interface PartnersBiddingAttendanceDialogProps { isAttending: boolean | null open: boolean onOpenChange: (open: boolean) => void - onSuccess: () => void } -export function PartnersBiddingAttendanceDialog({ +export function PartnersSpecificationMeetingDialog({ biddingDetail, biddingCompanyId, isAttending, open, onOpenChange, - onSuccess, -}: PartnersBiddingAttendanceDialogProps) { +}: PartnersSpecificationMeetingDialogProps) { const { toast } = useToast() const [isPending, startTransition] = useTransition() const [isLoading, setIsLoading] = React.useState(false) const [meetingData, setMeetingData] = React.useState(null) - + const router = useRouter() // 폼 상태 const [attendance, setAttendance] = React.useState('') const [attendeeCount, setAttendeeCount] = React.useState('') @@ -93,6 +87,7 @@ export function PartnersBiddingAttendanceDialog({ }) } } catch (error) { + console.error('사양설명회 정보를 불러오는데 실패했습니다.', error) toast({ title: '오류', description: '사양설명회 정보를 불러오는데 실패했습니다.', @@ -178,7 +173,7 @@ export function PartnersBiddingAttendanceDialog({ }) } - onSuccess() + router.refresh() onOpenChange(false) } else { toast({ diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx index 89ca426b..d134bc3b 100644 --- a/lib/bidding/vendor/partners-bidding-detail.tsx +++ b/lib/bidding/vendor/partners-bidding-detail.tsx @@ -5,30 +5,24 @@ import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { Textarea } from '@/components/ui/textarea' -import { Checkbox } from '@/components/ui/checkbox' import { ArrowLeft, - Calendar, - Building2, - Package, User, - DollarSign, - FileText, Users, Send, CheckCircle, XCircle, - Save + Save, + FileText, + Building2, + Package } from 'lucide-react' import { formatDate } from '@/lib/utils' import { getBiddingDetailsForPartners, submitPartnerResponse, - updatePartnerAttendance, updatePartnerBiddingParticipation, saveBiddingDraft } from '../detail/service' @@ -61,7 +55,8 @@ interface BiddingDetail { contractType: string biddingType: string awardCount: string | null - contractPeriod: string | null + contractStartDate: Date | null + contractEndDate: Date | null preQuoteDate: Date | null biddingRegistrationDate: Date | null submissionStartDate: Date | null @@ -180,10 +175,10 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD setTotalQuotationAmount(total) // 응찰 확정 시에만 사전견적 금액을 finalQuoteAmount로 설정 - if (total > 0 && result.isBiddingParticipated === true) { + if (totalQuotationAmount > 0 && result.isBiddingParticipated === true) { setResponseData(prev => ({ ...prev, - finalQuoteAmount: total.toString() + finalQuoteAmount: totalQuotationAmount.toString() })) } } catch (error) { @@ -455,13 +450,6 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD }) } - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('ko-KR', { - style: 'currency', - currency: biddingDetail?.currency || 'KRW', - }).format(amount) - } - if (isLoading) { return (
@@ -497,9 +485,11 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD

{biddingDetail.title}

- + {biddingDetail.biddingNumber} - {biddingDetail.revision && biddingDetail.revision > 0 && ` Rev.${biddingDetail.revision}`} + + + Rev. {biddingDetail.revision ?? 0} - {/* 품목별 견적 섹션 */} - {/*
- - setResponseData({...responseData, finalQuoteAmount: e.target.value})} - placeholder="총 견적금액을 입력하세요" - /> -
*/} - - {/*
- - setResponseData({...responseData, proposedContractDeliveryDate: e.target.value})} - /> -
*/} - {/* 품목별 상세 견적 테이블 */} {prItems.length > 0 ? ( )} - - {/* 기타 사항 */} - {/*
- -