From 93b6b8868d409c7f6c9d9222b93750848caaedde Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 5 Dec 2025 03:28:04 +0000 Subject: (최겸) 구매 입찰 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bidding/create/bidding-create-dialog.tsx | 51 ++++++++---- .../bidding/manage/bidding-basic-info-editor.tsx | 2 - .../bidding/manage/bidding-companies-editor.tsx | 17 +++- .../manage/bidding-detail-vendor-create-dialog.tsx | 15 +++- .../bidding/manage/create-pre-quote-rfq-dialog.tsx | 90 ++++++++-------------- lib/bidding/actions.ts | 2 +- lib/bidding/approval-actions.ts | 16 ++-- lib/bidding/detail/service.ts | 14 +++- lib/bidding/handlers.ts | 21 ++++- .../manage/import-bidding-items-from-excel.ts | 4 +- lib/bidding/pre-quote/service.ts | 10 ++- lib/bidding/service.ts | 61 ++++++++++++--- lib/bidding/validation.ts | 2 - .../vendor/partners-bidding-attendance-dialog.tsx | 1 - lib/bidding/vendor/partners-bidding-detail.tsx | 7 +- lib/bidding/vendor/partners-bidding-list.tsx | 1 - 16 files changed, 196 insertions(+), 118 deletions(-) diff --git a/components/bidding/create/bidding-create-dialog.tsx b/components/bidding/create/bidding-create-dialog.tsx index b3972e11..af33f1f6 100644 --- a/components/bidding/create/bidding-create-dialog.tsx +++ b/components/bidding/create/bidding-create-dialog.tsx @@ -63,7 +63,7 @@ import { PurchaseGroupCodeSelector } from '@/components/common/selectors/purchas import { ProcurementManagerSelector } from '@/components/common/selectors/procurement-manager' import type { PurchaseGroupCodeWithUser } from '@/components/common/selectors/purchase-group-code/purchase-group-code-service' import type { ProcurementManagerWithUser } from '@/components/common/selectors/procurement-manager/procurement-manager-service' -import { createBidding } from '@/lib/bidding/service' +import { createBidding, getUserDetails } from '@/lib/bidding/service' import { useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' @@ -97,13 +97,6 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp sparePartOptions: '', }) - // 구매요청자 정보 (현재 사용자) - // React.useEffect(() => { - // // 실제로는 현재 로그인한 사용자의 정보를 가져와야 함 - // // 임시로 기본값 설정 - // form.setValue('requesterName', '김두진') // 실제로는 API에서 가져와야 함 - // }, [form]) - const [shiAttachmentFiles, setShiAttachmentFiles] = React.useState([]) const [vendorAttachmentFiles, setVendorAttachmentFiles] = React.useState([]) @@ -164,13 +157,41 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp React.useEffect(() => { if (isOpen) { - if (userId && session?.user?.name) { - // 현재 사용자의 정보를 임시로 입찰담당자로 설정 - form.setValue('bidPicName', session.user.name) - form.setValue('bidPicId', userId) - // userCode는 현재 세션에 없으므로 이름으로 설정 (실제로는 API에서 가져와야 함) - // form.setValue('bidPicCode', session.user.name) + const initUser = async () => { + if (userId) { + try { + const user = await getUserDetails(userId) + if (user) { + // 현재 사용자의 정보를 입찰담당자로 설정 + form.setValue('bidPicName', user.name) + form.setValue('bidPicId', user.id) + form.setValue('bidPicCode', user.userCode || '') + + // 담당자 selector 상태 업데이트 + setSelectedBidPic({ + PURCHASE_GROUP_CODE: user.userCode || '', + DISPLAY_NAME: user.name, + EMPLOYEE_NUMBER: user.employeeNumber || '', + user: { + id: user.id, + name: user.name, + email: '', + employeeNumber: user.employeeNumber + } + } as any) + } + } catch (error) { + console.error('Failed to fetch user details:', error) + // 실패 시 세션 정보로 폴백 + if (session?.user?.name) { + form.setValue('bidPicName', session.user.name) + form.setValue('bidPicId', userId) + } + } + } } + initUser() + loadPaymentTerms() loadIncoterms() loadShippingPlaces() @@ -181,7 +202,7 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp form.setValue('biddingConditions.taxConditions', 'V1') } } - }, [isOpen, loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces, form]) + }, [isOpen, userId, session, form, loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces]) // SHI용 파일 첨부 핸들러 const handleShiFileUpload = (event: React.ChangeEvent) => { diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx index 27a2c097..13c58311 100644 --- a/components/bidding/manage/bidding-basic-info-editor.tsx +++ b/components/bidding/manage/bidding-basic-info-editor.tsx @@ -88,7 +88,6 @@ interface BiddingBasicInfo { contractEndDate?: string submissionStartDate?: string submissionEndDate?: string - evaluationDate?: string hasSpecificationMeeting?: boolean hasPrDocument?: boolean currency?: string @@ -252,7 +251,6 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB contractEndDate: formatDate(bidding.contractEndDate), submissionStartDate: formatDateTime(bidding.submissionStartDate), submissionEndDate: formatDateTime(bidding.submissionEndDate), - evaluationDate: formatDateTime(bidding.evaluationDate), hasSpecificationMeeting: bidding.hasSpecificationMeeting || false, hasPrDocument: bidding.hasPrDocument || false, currency: bidding.currency || 'KRW', diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx index 4c3e6bbc..9bfea90e 100644 --- a/components/bidding/manage/bidding-companies-editor.tsx +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -566,7 +566,22 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC {vendor.vendorName} {vendor.vendorCode} - {vendor.businessSize || '-'} + + {(() => { + switch (vendor.businessSize) { + case 'A': + return '대기업'; + case 'B': + return '중견기업'; + case 'C': + return '중소기업'; + case 'D': + return '소기업'; + default: + return '-'; + } + })()} + {vendor.companyId && vendorFirstContacts.has(vendor.companyId) ? vendorFirstContacts.get(vendor.companyId)!.contactName diff --git a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx index 0dd9f0eb..489f104d 100644 --- a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx +++ b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx @@ -408,7 +408,20 @@ export function BiddingDetailVendorCreateDialog({ 연동제 적용요건 문의 - 기업규모: {businessSizeMap[item.vendor.id] || '미정'} + 기업규모: {(() => { + switch (businessSizeMap[item.vendor.id]) { + case 'A': + return '대기업'; + case 'B': + return '중견기업'; + case 'C': + return '중소기업'; + case 'D': + return '소기업'; + default: + return '-'; + } + })()} diff --git a/components/bidding/manage/create-pre-quote-rfq-dialog.tsx b/components/bidding/manage/create-pre-quote-rfq-dialog.tsx index 1ab7a40f..b0cecc25 100644 --- a/components/bidding/manage/create-pre-quote-rfq-dialog.tsx +++ b/components/bidding/manage/create-pre-quote-rfq-dialog.tsx @@ -26,13 +26,6 @@ import { FormMessage, FormDescription, } from "@/components/ui/form" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { @@ -41,20 +34,15 @@ import { PopoverTrigger, } from "@/components/ui/popover" import { Calendar } from "@/components/ui/calendar" -import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { toast } from "sonner" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { createPreQuoteRfqAction } from "@/lib/bidding/pre-quote/service" -import { previewGeneralRfqCode } from "@/lib/rfq-last/service" -import { MaterialGroupSelectorDialogSingle } from "@/components/common/material/material-group-selector-dialog-single" -import { MaterialSearchItem } from "@/lib/material/material-group-service" -import { MaterialSelectorDialogSingle } from "@/components/common/selectors/material/material-selector-dialog-single" -import { MaterialSearchItem as SAPMaterialSearchItem } from "@/components/common/selectors/material/material-service" import { PurchaseGroupCodeSelector } from "@/components/common/selectors/purchase-group-code/purchase-group-code-selector" import type { PurchaseGroupCodeWithUser } from "@/components/common/selectors/purchase-group-code" import { getBiddingById } from "@/lib/bidding/service" +import { getProjectIdByCodeAndName } from "@/lib/bidding/manage/project-utils" // 아이템 스키마 const itemSchema = z.object({ @@ -64,6 +52,8 @@ const itemSchema = z.object({ materialName: z.string().optional(), quantity: z.number().min(1, "수량은 1 이상이어야 합니다"), uom: z.string().min(1, "단위를 입력해주세요"), + totalWeight: z.union([z.number(), z.string(), z.null()]).optional(), // 중량 추가 + weightUnit: z.string().optional().nullable(), // 중량단위 추가 remark: z.string().optional(), }) @@ -125,8 +115,6 @@ export function CreatePreQuoteRfqDialog({ onSuccess }: CreatePreQuoteRfqDialogProps) { const [isLoading, setIsLoading] = React.useState(false) - const [previewCode, setPreviewCode] = React.useState("") - const [isLoadingPreview, setIsLoadingPreview] = React.useState(false) const [selectedBidPic, setSelectedBidPic] = React.useState(undefined) const { data: session } = useSession() @@ -143,6 +131,8 @@ export function CreatePreQuoteRfqDialog({ materialName: item.materialInfo || "", quantity: item.quantity ? parseFloat(item.quantity) : 1, uom: item.quantityUnit || item.weightUnit || "EA", + totalWeight: item.totalWeight ? parseFloat(item.totalWeight) : null, + weightUnit: item.weightUnit || null, remark: "", })) }, [biddingItems]) @@ -164,6 +154,8 @@ export function CreatePreQuoteRfqDialog({ materialName: "", quantity: 1, uom: "", + totalWeight: null, + weightUnit: null, remark: "", }, ], @@ -231,6 +223,14 @@ export function CreatePreQuoteRfqDialog({ const pName = bidding.projectName || ""; setProjectInfo(pCode && pName ? `${pCode} - ${pName}` : pCode || pName || ""); + // 프로젝트 ID 조회 + if (pCode && pName) { + const fetchedProjectId = await getProjectIdByCodeAndName(pCode, pName) + if (fetchedProjectId) { + form.setValue("projectId", fetchedProjectId) + } + } + // 폼 값 설정 form.setValue("rfqTitle", rfqTitle); form.setValue("rfqType", "pre_bidding"); // 기본값 설정 @@ -264,36 +264,15 @@ export function CreatePreQuoteRfqDialog({ materialName: "", quantity: 1, uom: "", + totalWeight: null, + weightUnit: null, remark: "", }, ], }) - setPreviewCode("") } }, [open, initialItems, form, selectedBidPic, biddingId]) - // 견적담당자 선택 시 RFQ 코드 미리보기 생성 - React.useEffect(() => { - if (!selectedBidPic?.user?.id) { - setPreviewCode("") - return - } - - // 즉시 실행 함수 패턴 사용 - (async () => { - setIsLoadingPreview(true) - try { - const code = await previewGeneralRfqCode(selectedBidPic.user!.id) - setPreviewCode(code) - } catch (error) { - console.error("코드 미리보기 오류:", error) - setPreviewCode("") - } finally { - setIsLoadingPreview(false) - } - })() - }, [selectedBidPic]) - // 견적 종류 변경 const handleRfqTypeChange = (value: string) => { form.setValue("rfqType", value) @@ -315,12 +294,13 @@ export function CreatePreQuoteRfqDialog({ materialName: "", quantity: 1, uom: "", + totalWeight: null, + weightUnit: null, remark: "", }, ], }) setSelectedBidPic(undefined) - setPreviewCode("") onOpenChange(false) } @@ -350,15 +330,17 @@ export function CreatePreQuoteRfqDialog({ biddingNumber: data.biddingNumber, // 추가 contractStartDate: data.contractStartDate, // 추가 contractEndDate: data.contractEndDate, // 추가 - items: data.items as Array<{ - itemCode: string; - itemName: string; - materialCode?: string; - materialName?: string; - quantity: number; - uom: string; - remark?: string; - }>, + items: data.items.map(item => ({ + itemCode: item.itemCode || "", + itemName: item.itemName || "", + materialCode: item.materialCode, + materialName: item.materialName, + quantity: item.quantity, + uom: item.uom, + totalWeight: item.totalWeight, + weightUnit: item.weightUnit, + remark: item.remark, + })), biddingConditions: biddingConditions || undefined, createdBy: userId, updatedBy: userId, @@ -590,17 +572,7 @@ export function CreatePreQuoteRfqDialog({ )} /> - {/* RFQ 코드 미리보기 */} - {previewCode && ( -
- - 예상 RFQ 코드: {previewCode} - - {isLoadingPreview && ( - - )} -
- )} + {/* 계약기간 */}
diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index cc246ee7..6bedbab5 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -652,7 +652,7 @@ export async function cancelDisposalAction( } // 사용자 이름 조회 헬퍼 함수 -async function getUserNameById(userId: string): Promise { +export async function getUserNameById(userId: string): Promise { try { const user = await db .select({ name: users.name }) diff --git a/lib/bidding/approval-actions.ts b/lib/bidding/approval-actions.ts index 0fb16439..b4f6f297 100644 --- a/lib/bidding/approval-actions.ts +++ b/lib/bidding/approval-actions.ts @@ -266,12 +266,14 @@ export async function requestBiddingInvitationWithApproval(data: { const { default: db } = await import('@/db/db'); const { biddings, biddingCompanies, prItemsForBidding } = await import('@/db/schema'); const { eq } = await import('drizzle-orm'); - + const { getUserNameById } = await import('@/lib/bidding/actions'); + const userName = await getUserNameById(data.currentUser.id.toString()); + await db .update(biddings) .set({ status: 'approval_pending', // 결재 진행중 상태 - // updatedBy: String(data.currentUser.id), // 기존 등록자 유지를 위해 주석 처리 + updatedBy: userName, updatedAt: new Date() }) .where(eq(biddings.id, data.biddingId)); @@ -465,6 +467,7 @@ export async function requestBiddingClosureWithApproval(data: { const { default: db } = await import('@/db/db'); const { biddings } = await import('@/db/schema'); const { eq } = await import('drizzle-orm'); + const { getUserNameById } = await import('@/lib/bidding/actions'); // 유찰상태인지 확인 const biddingResult = await db @@ -487,12 +490,12 @@ export async function requestBiddingClosureWithApproval(data: { // 3. 입찰 상태를 결재 진행중으로 변경 debugLog('[BiddingClosureApproval] 입찰 상태 변경 시작'); - + const userName = await getUserNameById(data.currentUser.id.toString()); await db .update(biddings) .set({ status: 'approval_pending', // 폐찰 결재 진행중 상태 - // updatedBy: Number(data.currentUser.id), // 기존 등록자 유지를 위해 주석 처리 + updatedBy: userName, updatedAt: new Date() }) .where(eq(biddings.id, data.biddingId)); @@ -693,12 +696,13 @@ export async function requestBiddingAwardWithApproval(data: { const { default: db } = await import('@/db/db'); const { biddings } = await import('@/db/schema'); const { eq } = await import('drizzle-orm'); - + const { getUserNameById } = await import('@/lib/bidding/actions'); + const userName = await getUserNameById(data.currentUser.id.toString()); await db .update(biddings) .set({ status: 'approval_pending', // 낙찰 결재 진행중 상태 - // updatedBy: Number(data.currentUser.id), // 기존 등록자 유지를 위해 주석 처리 + updatedBy: userName, updatedAt: new Date() }) .where(eq(biddings.id, data.biddingId)); diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index 68c55fb0..17ea8f28 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -1288,10 +1288,14 @@ export async function getAwardedCompanies(biddingId: number) { companyId: biddingCompanies.companyId, companyName: vendors.vendorName, finalQuoteAmount: biddingCompanies.finalQuoteAmount, - awardRatio: biddingCompanies.awardRatio + awardRatio: biddingCompanies.awardRatio, + vendorCode: vendors.vendorCode, + companySize: vendors.businessSize, + targetPrice: biddings.targetPrice }) .from(biddingCompanies) .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) + .leftJoin(biddings, eq(biddingCompanies.biddingId, biddings.id)) .where(and( eq(biddingCompanies.biddingId, biddingId), eq(biddingCompanies.isWinner, true) @@ -1301,7 +1305,10 @@ export async function getAwardedCompanies(biddingId: number) { companyId: company.companyId, companyName: company.companyName, finalQuoteAmount: parseFloat(company.finalQuoteAmount?.toString() || '0'), - awardRatio: parseFloat(company.awardRatio?.toString() || '0') + awardRatio: parseFloat(company.awardRatio?.toString() || '0'), + vendorCode: company.vendorCode, + companySize: company.companySize, + targetPrice: company.targetPrice ? parseFloat(company.targetPrice.toString()) : 0 })) } catch (error) { console.error('Failed to get awarded companies:', error) @@ -1330,7 +1337,7 @@ async function updateBiddingAmounts(biddingId: number) { .set({ targetPrice: totalTargetAmount.toString(), budget: totalBudgetAmount.toString(), - finalBidPrice: totalActualAmount.toString(), + actualPrice: totalActualAmount.toString(), updatedAt: new Date() }) .where(eq(biddings.id, biddingId)) @@ -1745,7 +1752,6 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId: biddingRegistrationDate: biddings.biddingRegistrationDate, submissionStartDate: biddings.submissionStartDate, submissionEndDate: biddings.submissionEndDate, - evaluationDate: biddings.evaluationDate, // 가격 정보 currency: biddings.currency, diff --git a/lib/bidding/handlers.ts b/lib/bidding/handlers.ts index d56a083a..03a85bb6 100644 --- a/lib/bidding/handlers.ts +++ b/lib/bidding/handlers.ts @@ -422,12 +422,13 @@ export async function requestBiddingClosureInternal(payload: { const { default: db } = await import('@/db/db'); const { biddings } = await import('@/db/schema'); const { eq } = await import('drizzle-orm'); - + const { getUserNameById } = await import('@/lib/bidding/actions'); + const userName = await getUserNameById(payload.currentUserId.toString()); await db .update(biddings) .set({ status: 'bid_closure', - updatedBy: payload.currentUserId.toString(), + updatedBy: userName, updatedAt: new Date(), remarks: payload.description, // 폐찰 사유를 remarks에 저장 }) @@ -614,6 +615,15 @@ export async function mapBiddingAwardToTemplateVariables(payload: { biddingId: number; selectionReason: string; requestedAt: Date; + awardedCompanies?: Array<{ + companyId: number; + companyName: string | null; + finalQuoteAmount: number; + awardRatio: number; + vendorCode?: string | null; + companySize?: string | null; + targetPrice?: number | null; + }>; }): Promise> { const { biddingId, selectionReason, requestedAt } = payload; @@ -649,8 +659,11 @@ export async function mapBiddingAwardToTemplateVariables(payload: { const bidding = biddingInfo[0]; // 2. 낙찰된 업체 정보 조회 - const { getAwardedCompanies } = await import('@/lib/bidding/detail/service'); - const awardedCompanies = await getAwardedCompanies(biddingId); + let awardedCompanies = payload.awardedCompanies; + if (!awardedCompanies) { + const { getAwardedCompanies } = await import('@/lib/bidding/detail/service'); + awardedCompanies = await getAwardedCompanies(biddingId); + } // 3. 입찰 대상 자재 정보 조회 const biddingItemsInfo = await db diff --git a/lib/bidding/manage/import-bidding-items-from-excel.ts b/lib/bidding/manage/import-bidding-items-from-excel.ts index 2e0dfe33..fe5b17a9 100644 --- a/lib/bidding/manage/import-bidding-items-from-excel.ts +++ b/lib/bidding/manage/import-bidding-items-from-excel.ts @@ -1,6 +1,7 @@ import ExcelJS from "exceljs" import { PRItemInfo } from "@/components/bidding/manage/bidding-items-editor" import { getProjectIdByCodeAndName } from "./project-utils" +import { decryptWithServerAction } from "@/components/drm/drmUtils" export interface ImportBiddingItemsResult { success: boolean @@ -19,7 +20,8 @@ export async function importBiddingItemsFromExcel( try { const workbook = new ExcelJS.Workbook() - const arrayBuffer = await file.arrayBuffer() + // DRM 해제 후 ArrayBuffer 획득 (DRM 서버 미연결 시 원본 반환) + const arrayBuffer = await decryptWithServerAction(file) await workbook.xlsx.load(arrayBuffer) const worksheet = workbook.worksheets[0] diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts index e1152abe..6fef228c 100644 --- a/lib/bidding/pre-quote/service.ts +++ b/lib/bidding/pre-quote/service.ts @@ -859,8 +859,8 @@ export async function getSelectedVendorsForBidding(biddingId: number) { interface CreatePreQuoteRfqInput { rfqType: string; rfqTitle: string; - dueDate: Date; - picUserId: number; + dueDate?: Date; + picUserId: number | string | undefined; projectId?: number; remark?: string; biddingNumber?: string; @@ -875,6 +875,8 @@ interface CreatePreQuoteRfqInput { remark?: string; materialCode?: string; materialName?: string; + totalWeight?: number | string | null; // 중량 추가 + weightUnit?: string | null; // 중량단위 추가 }>; biddingConditions?: { paymentTerms?: string | null @@ -976,6 +978,10 @@ export async function createPreQuoteRfqAction(input: CreatePreQuoteRfqInput) { quantity: item.quantity, // 수량 uom: item.uom, // 단위 + // 중량 정보 + grossWeight: item.totalWeight ? (typeof item.totalWeight === 'string' ? parseFloat(item.totalWeight) : item.totalWeight) : null, + gwUom: item.weightUnit || null, + majorYn: index === 0, // 첫 번째 아이템을 주요 아이템으로 설정 remark: item.remark || null, // 비고 })); diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 77a0b1b4..76cd31f7 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -61,6 +61,27 @@ export async function getUserCodeByEmail(email: string): Promise } } +// 사용자 ID로 상세 정보 조회 (이름, 코드 등) +export async function getUserDetails(userId: number) { + try { + const user = await db + .select({ + id: users.id, + name: users.name, + userCode: users.userCode, + employeeNumber: users.employeeNumber + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1) + + return user[0] || null + } catch (error) { + console.error('Failed to get user details:', error) + return null + } +} + // userId를 user.name으로 변환하는 유틸리티 함수 async function getUserNameById(userId: string): Promise { try { @@ -421,9 +442,10 @@ export async function getBiddings(input: GetBiddingsSchema) { // 메타 정보 remarks: biddings.remarks, updatedAt: biddings.updatedAt, - updatedBy: biddings.updatedBy, + updatedBy: users.name, }) .from(biddings) + .leftJoin(users, sql`${biddings.updatedBy} = ${users.id}::varchar`) .where(finalWhere) .orderBy(...orderByColumns) .limit(input.perPage) @@ -874,7 +896,6 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { biddingRegistrationDate: new Date(), submissionStartDate: parseDate(input.submissionStartDate), submissionEndDate: parseDate(input.submissionEndDate), - evaluationDate: parseDate(input.evaluationDate), hasSpecificationMeeting: input.hasSpecificationMeeting || false, hasPrDocument: input.hasPrDocument || false, @@ -913,6 +934,7 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { await tx.insert(biddingNoticeTemplate).values({ biddingId, title: input.title + ' 입찰공고', + type: input.noticeType || 'standard', content: input.content || standardContent, isTemplate: false, }) @@ -1723,7 +1745,6 @@ export async function updateBiddingBasicInfo( contractEndDate?: string submissionStartDate?: string submissionEndDate?: string - evaluationDate?: string hasSpecificationMeeting?: boolean hasPrDocument?: boolean currency?: string @@ -1781,9 +1802,23 @@ export async function updateBiddingBasicInfo( // 정의된 필드들만 업데이트 if (updates.title !== undefined) updateData.title = updates.title if (updates.description !== undefined) updateData.description = updates.description - if (updates.content !== undefined) updateData.content = updates.content + // content는 bidding 테이블에 컬럼이 없음, notice content는 별도로 저장해야 함 + // if (updates.content !== undefined) updateData.content = updates.content if (updates.noticeType !== undefined) updateData.noticeType = updates.noticeType if (updates.contractType !== undefined) updateData.contractType = updates.contractType + + // 입찰공고 내용 저장 + if (updates.content !== undefined) { + try { + await saveBiddingNotice(biddingId, { + title: (updates.title || '') + ' 입찰공고', // 제목이 없으면 기존 제목을 가져오거나 해야하는데, 여기서는 업데이트된 제목 사용 + content: updates.content + }) + } catch (e) { + console.error('Failed to save bidding notice content:', e) + // 공고 저장 실패는 전체 업데이트 실패로 처리하지 않음 (로그만 남김) + } + } if (updates.biddingType !== undefined) updateData.biddingType = updates.biddingType if (updates.biddingTypeCustom !== undefined) updateData.biddingTypeCustom = updates.biddingTypeCustom if (updates.awardCount !== undefined) updateData.awardCount = updates.awardCount @@ -1795,7 +1830,6 @@ export async function updateBiddingBasicInfo( if (updates.contractEndDate !== undefined) updateData.contractEndDate = parseDate(updates.contractEndDate) if (updates.submissionStartDate !== undefined) updateData.submissionStartDate = parseDate(updates.submissionStartDate) if (updates.submissionEndDate !== undefined) updateData.submissionEndDate = parseDate(updates.submissionEndDate) - if (updates.evaluationDate !== undefined) updateData.evaluationDate = parseDate(updates.evaluationDate) if (updates.hasSpecificationMeeting !== undefined) updateData.hasSpecificationMeeting = updates.hasSpecificationMeeting if (updates.hasPrDocument !== undefined) updateData.hasPrDocument = updates.hasPrDocument if (updates.currency !== undefined) updateData.currency = updates.currency @@ -2889,7 +2923,7 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u let currentRound = match ? parseInt(match[1]) : 1 if (currentRound >= 3) { - // -03 이상이면 새로운 번호 생성 + // -03 이상이면 재입찰이며, 새로운 번호 생성 newBiddingNumber = await generateBiddingNumber(existingBidding.contractType, userId, tx) // 새로 생성한 입찰번호를 원입찰번호로 셋팅 originalBiddingNumber = newBiddingNumber.split('-')[0] @@ -2913,13 +2947,15 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u // 기본 정보 복제 projectName: existingBidding.projectName, + projectCode: existingBidding.projectCode, // 프로젝트 코드 복제 itemName: existingBidding.itemName, title: existingBidding.title, description: existingBidding.description, // 계약 정보 복제 contractType: existingBidding.contractType, - biddingType: existingBidding.biddingType, + noticeType: existingBidding.noticeType, // 공고타입 복제 + biddingType: existingBidding.biddingType, // 구매유형 복제 awardCount: existingBidding.awardCount, contractStartDate: existingBidding.contractStartDate, contractEndDate: existingBidding.contractEndDate, @@ -2929,7 +2965,6 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u biddingRegistrationDate: new Date(), submissionStartDate: null, submissionEndDate: null, - evaluationDate: null, // 사양설명회 hasSpecificationMeeting: existingBidding.hasSpecificationMeeting, @@ -2939,6 +2974,7 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u budget: existingBidding.budget, targetPrice: existingBidding.targetPrice, targetPriceCalculationCriteria: existingBidding.targetPriceCalculationCriteria, + actualPrice: existingBidding.actualPrice, finalBidPrice: null, // 최종입찰가는 초기화 // PR 정보 복제 @@ -3194,8 +3230,6 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u .from(biddingDocuments) .where(and( eq(biddingDocuments.biddingId, biddingId), - // PR 아이템에 연결된 첨부파일은 제외 (SHI용과 협력업체용만 복제) - isNull(biddingDocuments.prItemId), // SHI용(evaluation_doc) 또는 협력업체용(company_proposal) 문서만 복제 or( eq(biddingDocuments.documentType, 'evaluation_doc'), @@ -3266,6 +3300,8 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u } revalidatePath('/bid-receive') + revalidatePath('/evcp/bid-receive') + revalidatePath('/evcp/bid') revalidatePath(`/bid-receive/${biddingId}`) // 기존 입찰 페이지도 갱신 revalidatePath(`/bid-receive/${newBidding.id}`) @@ -3825,7 +3861,7 @@ export async function getBiddingsForFailure(input: GetBiddingsSchema) { // 유찰 정보 (업데이트 일시를 유찰일로 사용) disposalDate: biddings.updatedAt, // 유찰일 disposalUpdatedAt: biddings.updatedAt, // 폐찰수정일 - disposalUpdatedBy: biddings.updatedBy, // 폐찰수정자 + disposalUpdatedBy: users.name, // 폐찰수정자 // 폐찰 정보 closureReason: biddings.description, // 폐찰사유 @@ -3840,9 +3876,10 @@ export async function getBiddingsForFailure(input: GetBiddingsSchema) { createdBy: biddings.createdBy, createdAt: biddings.createdAt, updatedAt: biddings.updatedAt, - updatedBy: biddings.updatedBy, + updatedBy: users.name, }) .from(biddings) + .leftJoin(users, sql`${biddings.updatedBy} = ${users.id}::varchar`) .leftJoin(biddingDocuments, and( eq(biddingDocuments.biddingId, biddings.id), eq(biddingDocuments.documentType, 'evaluation_doc'), // 폐찰 문서 diff --git a/lib/bidding/validation.ts b/lib/bidding/validation.ts index 73c2fe21..3254ae7e 100644 --- a/lib/bidding/validation.ts +++ b/lib/bidding/validation.ts @@ -99,7 +99,6 @@ export const createBiddingSchema = z.object({ submissionEndDate: z.string().optional(), - evaluationDate: z.string().optional(), // 회의 및 문서 hasSpecificationMeeting: z.boolean().default(false), @@ -220,7 +219,6 @@ export const createBiddingSchema = z.object({ submissionStartDate: z.string().optional(), submissionEndDate: z.string().optional(), - evaluationDate: z.string().optional(), hasSpecificationMeeting: z.boolean().optional(), hasPrDocument: z.boolean().optional(), diff --git a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx index d0ef97f1..8d6cb82d 100644 --- a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx +++ b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx @@ -37,7 +37,6 @@ interface PartnersSpecificationMeetingDialogProps { title: string preQuoteDate: string | null biddingRegistrationDate: string | null - evaluationDate: string | null hasSpecificationMeeting?: boolean // 사양설명회 여부 추가 } | null biddingCompanyId: number diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx index bf76de62..087648ab 100644 --- a/lib/bidding/vendor/partners-bidding-detail.tsx +++ b/lib/bidding/vendor/partners-bidding-detail.tsx @@ -75,7 +75,6 @@ interface BiddingDetail { biddingRegistrationDate: Date | string | null submissionStartDate: Date | string | null submissionEndDate: Date | string | null - evaluationDate: Date | string | null currency: string budget: number | null targetPrice: number | null @@ -927,11 +926,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD })()}
)} - {biddingDetail.evaluationDate && ( -
- 평가일: {format(new Date(biddingDetail.evaluationDate), "yyyy-MM-dd HH:mm")} -
- )} + diff --git a/lib/bidding/vendor/partners-bidding-list.tsx b/lib/bidding/vendor/partners-bidding-list.tsx index 0f68ed68..f1cb0bdc 100644 --- a/lib/bidding/vendor/partners-bidding-list.tsx +++ b/lib/bidding/vendor/partners-bidding-list.tsx @@ -181,7 +181,6 @@ export function PartnersBiddingList({ promises }: PartnersBiddingListProps) { title: rowAction.row.original.title, preQuoteDate: null, biddingRegistrationDate: rowAction.row.original.submissionStartDate?.toISOString() || null, - evaluationDate: null, hasSpecificationMeeting: rowAction.row.original.hasSpecificationMeeting || false, } : null} biddingCompanyId={rowAction?.row.original?.biddingCompanyId || 0} -- cgit v1.2.3