diff options
Diffstat (limited to 'lib/bidding/detail')
| -rw-r--r-- | lib/bidding/detail/service.ts | 65 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-header.tsx | 101 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx | 41 |
3 files changed, 63 insertions, 144 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index d9bcb255..e22331bb 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -1,7 +1,7 @@ 'use server' import db from '@/db/db' -import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, BiddingListItem, biddingConditions, priceAdjustmentForms } from '@/db/schema' +import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, BiddingListItem, biddingConditions, priceAdjustmentForms, users } from '@/db/schema' import { specificationMeetings } from '@/db/schema/bidding' import { eq, and, sql, desc, ne } from 'drizzle-orm' import { revalidatePath, revalidateTag } from 'next/cache' @@ -9,6 +9,22 @@ import { unstable_cache } from "@/lib/unstable-cache"; import { sendEmail } from '@/lib/mail/sendEmail' import { saveFile } from '@/lib/file-stroage' +// userId를 user.name으로 변환하는 유틸리티 함수 +async function getUserNameById(userId: string): Promise<string> { + try { + const user = await db + .select({ name: users.name }) + .from(users) + .where(eq(users.id, parseInt(userId))) + .limit(1) + + return user[0]?.name || userId // user.name이 없으면 userId를 그대로 반환 + } catch (error) { + console.error('Failed to get user name:', error) + return userId // 에러 시 userId를 그대로 반환 + } +} + // 데이터 조회 함수들 export interface BiddingDetailData { bidding: Awaited<ReturnType<typeof getBiddingById>> @@ -266,6 +282,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV awardRatio: vendor.awardRatio ? Number(vendor.awardRatio) : null, isBiddingParticipated: vendor.isBiddingParticipated, status: vendor.status as 'pending' | 'submitted' | 'selected' | 'rejected', + documents: [], // 빈 배열로 초기화 })) } catch (error) { console.error('Failed to get quotation vendors:', error) @@ -664,22 +681,20 @@ export async function createBiddingDetailVendor( // 협력업체 정보 저장 - biddingCompanies와 companyConditionResponses 테이블에 레코드 생성 export async function createQuotationVendor(input: any, userId: string) { try { + const userName = await getUserNameById(userId) const result = await db.transaction(async (tx) => { // 1. biddingCompanies에 레코드 생성 const biddingCompanyResult = await tx.insert(biddingCompanies).values({ biddingId: input.biddingId, companyId: input.vendorId, - quotationAmount: input.quotationAmount, - currency: input.currency, - status: input.status, - awardRatio: input.awardRatio, + finalQuoteAmount: input.quotationAmount?.toString(), + awardRatio: input.awardRatio?.toString(), isWinner: false, contactPerson: input.contactPerson, contactEmail: input.contactEmail, contactPhone: input.contactPhone, - submissionDate: new Date(), - createdBy: userId, - updatedBy: userId, + finalQuoteSubmittedAt: new Date(), + // 스키마에 createdBy, updatedBy 필드가 없으므로 제거 }).returning({ id: biddingCompanies.id }) if (biddingCompanyResult.length === 0) { @@ -728,6 +743,7 @@ export async function createQuotationVendor(input: any, userId: string) { // 협력업체 정보 업데이트 export async function updateQuotationVendor(id: number, input: any, userId: string) { try { + const userName = await getUserNameById(userId) const result = await db.transaction(async (tx) => { // 1. biddingCompanies 테이블 업데이트 const updateData: any = {} @@ -735,9 +751,9 @@ export async function updateQuotationVendor(id: number, input: any, userId: stri if (input.contactPerson !== undefined) updateData.contactPerson = input.contactPerson if (input.contactEmail !== undefined) updateData.contactEmail = input.contactEmail if (input.contactPhone !== undefined) updateData.contactPhone = input.contactPhone - if (input.awardRatio !== undefined) updateData.awardRatio = input.awardRatio - if (input.status !== undefined) updateData.status = input.status - updateData.updatedBy = userId + if (input.awardRatio !== undefined) updateData.awardRatio = input.awardRatio?.toString() + // status 필드가 스키마에 없으므로 제거 + // updatedBy 필드가 스키마에 없으므로 제거 updateData.updatedAt = new Date() if (Object.keys(updateData).length > 0) { @@ -1031,8 +1047,10 @@ export async function registerBidding(biddingId: number, userId: string) { // 캐시 무효화 revalidateTag(`bidding-${biddingId}`) + revalidateTag('bidding-detail') revalidateTag('quotation-vendors') revalidateTag('quotation-details') + revalidateTag('pr-items') revalidatePath(`/evcp/bid/${biddingId}`) return { @@ -1157,6 +1175,7 @@ export async function createRebidding(biddingId: number, userId: string) { // 업체 선정 사유 업데이트 export async function updateVendorSelectionReason(biddingId: number, selectedCompanyId: number, selectionReason: string, userId: string) { try { + const userName = await getUserNameById(userId) // vendorSelectionResults 테이블에 삽입 또는 업데이트 await db .insert(vendorSelectionResults) @@ -1164,7 +1183,7 @@ export async function updateVendorSelectionReason(biddingId: number, selectedCom biddingId, selectedCompanyId, selectionReason, - selectedBy: userId, + selectedBy: userName, selectedAt: new Date(), createdAt: new Date(), updatedAt: new Date() @@ -1174,7 +1193,7 @@ export async function updateVendorSelectionReason(biddingId: number, selectedCom set: { selectedCompanyId, selectionReason, - selectedBy: userId, + selectedBy: userName, selectedAt: new Date(), updatedAt: new Date() } @@ -1194,6 +1213,7 @@ export async function updateVendorSelectionReason(biddingId: number, selectedCom // 낙찰용 문서 업로드 export async function uploadAwardDocument(biddingId: number, file: File, userId: string) { try { + const userName = await getUserNameById(userId) const saveResult = await saveFile({ file, directory: `biddings/${biddingId}/award`, @@ -1211,10 +1231,9 @@ export async function uploadAwardDocument(biddingId: number, file: File, userId: documentType: 'other', title: '낙찰 관련 문서', description: '낙찰 관련 첨부파일', - uploadedBy: userId, + uploadedBy: userName, uploadedAt: new Date(), - createdAt: new Date(), - updatedAt: new Date() + // createdAt, updatedAt 필드가 스키마에 없으므로 제거 }).returning() return { @@ -1292,6 +1311,7 @@ export async function getAwardDocumentForDownload(documentId: number, biddingId: // 낙찰용 문서 삭제 export async function deleteAwardDocument(documentId: number, biddingId: number, userId: string) { try { + const userName = await getUserNameById(userId) // 문서 정보 조회 const documents = await db .select() @@ -1300,7 +1320,7 @@ export async function deleteAwardDocument(documentId: number, biddingId: number, eq(biddingDocuments.id, documentId), eq(biddingDocuments.biddingId, biddingId), eq(biddingDocuments.documentType, 'other'), - eq(biddingDocuments.uploadedBy, userId) + eq(biddingDocuments.uploadedBy, userName) )) .limit(1) @@ -1335,6 +1355,7 @@ export async function deleteAwardDocument(documentId: number, biddingId: number, // 낙찰 처리 (발주비율과 함께) export async function awardBidding(biddingId: number, selectionReason: string, userId: string) { try { + const userName = await getUserNameById(userId) // 낙찰된 업체들 조회 (isWinner가 true인 업체들) const awardedCompanies = await db .select({ @@ -1388,7 +1409,7 @@ export async function awardBidding(biddingId: number, selectionReason: string, u .set({ selectedCompanyId: firstAwardedCompany.companyId, selectionReason, - selectedBy: userId, + selectedBy: userName, selectedAt: new Date(), updatedAt: new Date() }) @@ -1401,7 +1422,7 @@ export async function awardBidding(biddingId: number, selectionReason: string, u biddingId, selectedCompanyId: firstAwardedCompany.companyId, selectionReason, - selectedBy: userId, + selectedBy: userName, selectedAt: new Date(), createdAt: new Date(), updatedAt: new Date() @@ -1532,6 +1553,7 @@ export async function saveBiddingDraft( userId: string ) { try { + const userName = await getUserNameById(userId) let totalAmount = 0 await db.transaction(async (tx) => { @@ -1668,7 +1690,7 @@ export interface PartnersBiddingListItem { // biddings 정보 biddingId: number biddingNumber: string - revision: number + revision: number | null projectName: string itemName: string title: string @@ -1883,6 +1905,7 @@ export async function submitPartnerResponse( userId: string ) { try { + const userName = await getUserNameById(userId) const result = await db.transaction(async (tx) => { // 0. 품목별 견적 정보 최종 저장 (본입찰 제출) - Upsert 방식 if (response.prItemQuotations && response.prItemQuotations.length > 0) { @@ -1981,7 +2004,7 @@ export async function submitPartnerResponse( const companyUpdateData: any = { respondedAt: new Date(), updatedAt: new Date(), - // updatedBy: userId, // 이 필드가 존재하지 않음 + // updatedBy: userName, // 이 필드가 존재하지 않음 } if (response.finalQuoteAmount !== undefined) { diff --git a/lib/bidding/detail/table/bidding-detail-header.tsx b/lib/bidding/detail/table/bidding-detail-header.tsx index 2798478c..8d18472f 100644 --- a/lib/bidding/detail/table/bidding-detail-header.tsx +++ b/lib/bidding/detail/table/bidding-detail-header.tsx @@ -40,107 +40,6 @@ export function BiddingDetailHeader({ bidding }: BiddingDetailHeaderProps) { router.push('/evcp/bid') } - const handleRegister = () => { - // 상태 검증 - if (bidding.status !== 'bidding_generated') { - toast({ - title: '실행 불가', - description: '입찰 등록은 입찰 생성 상태에서만 가능합니다.', - variant: 'destructive', - }) - return - } - - if (!confirm('입찰을 등록하시겠습니까?')) return - - startTransition(async () => { - const result = await registerBidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID - - if (result.success) { - toast({ - title: '성공', - description: result.message, - }) - router.refresh() - } else { - toast({ - title: '오류', - description: result.error, - variant: 'destructive', - }) - } - }) - } - - const handleMarkAsDisposal = () => { - // 상태 검증 - if (bidding.status !== 'bidding_closed') { - toast({ - title: '실행 불가', - description: '유찰 처리는 입찰 마감 상태에서만 가능합니다.', - variant: 'destructive', - }) - return - } - - if (!confirm('입찰을 유찰 처리하시겠습니까?')) return - - startTransition(async () => { - const result = await markAsDisposal(bidding.id, 'current-user') // TODO: 실제 사용자 ID - - if (result.success) { - toast({ - title: '성공', - description: result.message, - }) - router.refresh() - } else { - toast({ - title: '오류', - description: result.error, - variant: 'destructive', - }) - } - }) - } - - const handleCreateRebidding = () => { - // 상태 검증 - if (bidding.status !== 'bidding_disposal') { - toast({ - title: '실행 불가', - description: '재입찰은 유찰 상태에서만 가능합니다.', - variant: 'destructive', - }) - return - } - - if (!confirm('재입찰을 생성하시겠습니까?')) return - - startTransition(async () => { - const result = await createRebidding(bidding.id, 'current-user') // TODO: 실제 사용자 ID - - if (result.success) { - toast({ - title: '성공', - description: result.message, - }) - // 새로 생성된 입찰로 이동 - if (result.data) { - router.push(`/evcp/bid/${result.data.id}`) - } else { - router.refresh() - } - } else { - toast({ - title: '오류', - description: result.error, - variant: 'destructive', - }) - } - }) - } - return ( <div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <div className="px-6 py-4"> 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 64c31633..654d9941 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -106,7 +106,6 @@ export function BiddingDetailVendorToolbarActions({ <> <div className="flex items-center gap-2"> {/* 상태별 액션 버튼 */} - {bidding.status === 'set_target_price' && ( <Button variant="default" size="sm" @@ -116,27 +115,25 @@ export function BiddingDetailVendorToolbarActions({ <Send className="mr-2 h-4 w-4" /> 입찰 등록 </Button> - )} - <> - <Button - variant="destructive" - size="sm" - onClick={handleMarkAsDisposal} - disabled={isPending} - > - <XCircle className="mr-2 h-4 w-4" /> - 유찰 - </Button> - <Button - variant="default" - size="sm" - onClick={onOpenAwardDialog} - disabled={isPending} - > - <Trophy className="mr-2 h-4 w-4" /> - 낙찰 - </Button> - </> + <Button + variant="destructive" + size="sm" + onClick={handleMarkAsDisposal} + disabled={isPending} + > + <XCircle className="mr-2 h-4 w-4" /> + 유찰 + </Button> + <Button + variant="default" + size="sm" + onClick={onOpenAwardDialog} + disabled={isPending} + > + <Trophy className="mr-2 h-4 w-4" /> + 낙찰 + </Button> + {bidding.status === 'bidding_disposal' && ( <Button |
