diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-26 18:57:54 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-26 18:57:54 +0900 |
| commit | c775a993930e806f56ea116941574015ee518170 (patch) | |
| tree | c40605a7f9563fd8aa5f36da5255bbc37f691426 /lib/bidding | |
| parent | 2b5d063ab408a163c016358251192a07a337eaa7 (diff) | |
| parent | 94bc7b0181f74343e291d3bd164681044ea19714 (diff) | |
(김준회) dolce 개발건 입찰건과 merge
Diffstat (limited to 'lib/bidding')
| -rw-r--r-- | lib/bidding/approval-actions.ts | 53 | ||||
| -rw-r--r-- | lib/bidding/detail/service.ts | 18 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-award-dialog.tsx | 136 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-vendor-table.tsx | 9 | ||||
| -rw-r--r-- | lib/bidding/failure/biddings-closure-dialog.tsx | 5 | ||||
| -rw-r--r-- | lib/bidding/failure/biddings-failure-table.tsx | 3 | ||||
| -rw-r--r-- | lib/bidding/handlers.ts | 86 | ||||
| -rw-r--r-- | lib/bidding/service.ts | 81 |
8 files changed, 325 insertions, 66 deletions
diff --git a/lib/bidding/approval-actions.ts b/lib/bidding/approval-actions.ts index 06f5c206..dd88164d 100644 --- a/lib/bidding/approval-actions.ts +++ b/lib/bidding/approval-actions.ts @@ -479,11 +479,47 @@ export async function requestBiddingClosureWithApproval(data: { * ``` */ /** + * 낙찰할 업체 정보 조회 헬퍼 함수 + */ +export async function getAwardedCompaniesForApproval(biddingId: number) { + const { default: db } = await import('@/db/db'); + const { biddingCompanies, vendors } = await import('@/db/schema'); + const { eq, and } = await import('drizzle-orm'); + + const awardedCompanies = await db + .select({ + companyId: biddingCompanies.companyId, + companyName: vendors.vendorName, + finalQuoteAmount: biddingCompanies.finalQuoteAmount, + awardRatio: biddingCompanies.awardRatio + }) + .from(biddingCompanies) + .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) + .where(and( + eq(biddingCompanies.biddingId, biddingId), + eq(biddingCompanies.isWinner, true) + )); + + return awardedCompanies.map(company => ({ + companyId: company.companyId, + companyName: company.companyName || '', + finalQuoteAmount: parseFloat(company.finalQuoteAmount?.toString() || '0'), + awardRatio: parseFloat(company.awardRatio?.toString() || '0') + })); +} + +/** * 낙찰 결재를 위한 공통 데이터 준비 헬퍼 함수 */ export async function prepareBiddingAwardApprovalData(data: { biddingId: number; selectionReason: string; + awardedCompanies?: Array<{ + companyId: number; + companyName: string | null; + finalQuoteAmount: number; + awardRatio: number; + }>; }) { // 1. 입찰 정보 조회 (템플릿 변수용) debugLog('[BiddingAwardApproval] 입찰 정보 조회 시작'); @@ -510,6 +546,14 @@ export async function prepareBiddingAwardApprovalData(data: { title: biddingInfo[0].title, }); + // 낙찰할 업체 정보 조회 (파라미터로 제공되지 않은 경우) + const awardedCompanies = data.awardedCompanies || await getAwardedCompaniesForApproval(data.biddingId); + + if (awardedCompanies.length === 0) { + debugError('[BiddingAwardApproval] 낙찰된 업체가 없습니다.'); + throw new Error('낙찰된 업체가 없습니다. 먼저 발주비율을 산정해주세요.'); + } + // 2. 템플릿 변수 매핑 debugLog('[BiddingAwardApproval] 템플릿 변수 매핑 시작'); const requestedAt = new Date(); @@ -518,6 +562,7 @@ export async function prepareBiddingAwardApprovalData(data: { biddingId: data.biddingId, selectionReason: data.selectionReason, requestedAt, + awardedCompanies, // 낙찰 업체 정보 전달 }); debugLog('[BiddingAwardApproval] 템플릿 변수 매핑 완료', { variableKeys: Object.keys(variables), @@ -532,6 +577,12 @@ export async function prepareBiddingAwardApprovalData(data: { export async function requestBiddingAwardWithApproval(data: { biddingId: number; selectionReason: string; + awardedCompanies: Array<{ + companyId: number; + companyName: string | null; + finalQuoteAmount: number; + awardRatio: number; + }>; currentUser: { id: number; epId: string | null; email?: string }; approvers?: string[]; // Knox EP ID 배열 (결재선) }) { @@ -577,6 +628,7 @@ export async function requestBiddingAwardWithApproval(data: { const { bidding, variables } = await prepareBiddingAwardApprovalData({ biddingId: data.biddingId, selectionReason: data.selectionReason, + awardedCompanies: data.awardedCompanies, }); // 4. 결재 워크플로우 시작 (Saga 패턴) @@ -589,6 +641,7 @@ export async function requestBiddingAwardWithApproval(data: { { biddingId: data.biddingId, selectionReason: data.selectionReason, + awardedCompanies: data.awardedCompanies, // ✅ 결재 상신 시점의 낙찰 대상 정보 currentUserId: data.currentUser.id, // ✅ 결재 승인 후 핸들러 실행 시 필요 }, diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index c9ad43ff..8f9bf018 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -912,13 +912,11 @@ 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}`) + // 캐시 무효화 (API를 통한 방식) + const { revalidateViaCronJob } = await import('@/lib/revalidation-utils'); + await revalidateViaCronJob({ + tags: [`bidding-${biddingId}`, 'bidding-detail', 'quotation-vendors', 'quotation-details', 'pr-items'] + }); debugSuccess(`registerBidding: Success. Invited ${selectedCompanies.length} companies.`) return { @@ -1290,9 +1288,9 @@ async function updateBiddingAmounts(biddingId: number) { await db .update(biddings) .set({ - targetPrice: totalTargetAmount, - budget: totalBudgetAmount, - finalBidPrice: totalActualAmount, + targetPrice: totalTargetAmount.toString(), + budget: totalBudgetAmount.toString(), + finalBidPrice: totalActualAmount.toString(), updatedAt: new Date() }) .where(eq(biddings.id, biddingId)) diff --git a/lib/bidding/detail/table/bidding-award-dialog.tsx b/lib/bidding/detail/table/bidding-award-dialog.tsx index b168e884..613e2fc8 100644 --- a/lib/bidding/detail/table/bidding-award-dialog.tsx +++ b/lib/bidding/detail/table/bidding-award-dialog.tsx @@ -27,14 +27,27 @@ import { import { Trophy, Building2, Calculator } from 'lucide-react' import { useToast } from '@/hooks/use-toast' import { getAwardedCompanies } from '@/lib/bidding/detail/service' -import { requestBiddingAwardWithApproval } from '@/lib/bidding/approval-actions' +import { requestBiddingAwardWithApproval, prepareBiddingAwardApprovalData } from '@/lib/bidding/approval-actions' import { AwardSimpleFileUpload } from './components/award-simple-file-upload' +import { ApprovalPreviewDialog } from '@/lib/approval/approval-preview-dialog' interface BiddingAwardDialogProps { biddingId: number open: boolean onOpenChange: (open: boolean) => void onSuccess: () => void + onApprovalPreview?: (data: { + templateName: string + variables: Record<string, string> + title: string + selectionReason: string + awardedCompanies: { + companyId: number + companyName: string | null + finalQuoteAmount: number + awardRatio: number + }[] + }) => void } interface AwardedCompany { @@ -48,7 +61,8 @@ export function BiddingAwardDialog({ biddingId, open, onOpenChange, - onSuccess + onSuccess, + onApprovalPreview }: BiddingAwardDialogProps) { const { toast } = useToast() const { data: session } = useSession() @@ -56,7 +70,10 @@ export function BiddingAwardDialog({ const [selectionReason, setSelectionReason] = React.useState('') const [awardedCompanies, setAwardedCompanies] = React.useState<AwardedCompany[]>([]) const [isLoading, setIsLoading] = React.useState(false) -const userId = session?.user?.id || '2'; + const [isApprovalDialogOpen, setIsApprovalDialogOpen] = React.useState(false) + const [approvalVariables, setApprovalVariables] = React.useState<Record<string, string>>({}) + const [approvalTitle, setApprovalTitle] = React.useState('') + const userId = session?.user?.id || '2'; // 낙찰된 업체 정보 로드 React.useEffect(() => { if (open) { @@ -86,9 +103,8 @@ const userId = session?.user?.id || '2'; }, 0) }, [awardedCompanies]) - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - + // 다음단계 버튼 핸들러 - 결재 준비 및 부모로 데이터 전달 + const handleNextStep = async () => { if (!selectionReason.trim()) { toast({ title: '유효성 오류', @@ -107,45 +123,83 @@ const userId = session?.user?.id || '2'; return } - // 서버 액션을 사용하여 결재 상신 - startTransition(async () => { - try { - const result = await requestBiddingAwardWithApproval({ - biddingId, + try { + // 결재 데이터 준비 (템플릿 변수, 제목 등) + const approvalData = await prepareBiddingAwardApprovalData({ + biddingId, + selectionReason: selectionReason.trim(), + awardedCompanies, + }) + + // 결재 준비 완료 - 부모 컴포넌트의 결재 미리보기 콜백 호출 + if (onApprovalPreview) { + onApprovalPreview({ + templateName: '입찰 결과 업체 선정 품의 요청서', + variables: approvalData.variables, + title: `낙찰 - ${approvalData.bidding.title}`, selectionReason: selectionReason.trim(), - currentUser: { - id: Number(userId), - epId: session?.user?.epId || null, - email: session?.user?.email || undefined, - }, + awardedCompanies, }) + } else { + // 기존 로직 유지 (내부 결재 다이얼로그 사용) + setApprovalVariables(approvalData.variables) + setApprovalTitle(`낙찰 - ${approvalData.bidding.title}`) + setIsApprovalDialogOpen(true) + } + } catch (error) { + console.error('결재 준비 중 오류 발생:', error) + toast({ + title: '오류', + description: '결재 준비 중 오류가 발생했습니다.', + variant: 'destructive', + }) + } + } - if (result.status === 'pending_approval') { - toast({ - title: '성공', - description: '낙찰 결재가 상신되었습니다.', - }) - onOpenChange(false) - setSelectionReason('') - onSuccess() - } else { - toast({ - title: '오류', - description: '결재 상신에 실패했습니다.', - variant: 'destructive', - }) - } - } catch (error) { - console.error('낙찰 결재 상신 실패:', error) + // 결재 상신 핸들러 - 결재 완료 시 실제 낙찰 등록 실행 + const handleApprovalSubmit = async ({ approvers, title, attachments }: { approvers: string[], title: string, attachments?: File[] }) => { + try { + if (!session?.user?.id || !session.user.epId) { toast({ title: '오류', - description: error instanceof Error ? error.message : '결재 상신 중 오류가 발생했습니다.', + description: '필요한 정보가 없습니다.', variant: 'destructive', }) + return } - }) - } + // 결재 상신 + const result = await requestBiddingAwardWithApproval({ + biddingId, + selectionReason: selectionReason.trim(), + awardedCompanies, // ✅ 결재 상신 시점의 낙찰 대상 정보 전달 + currentUser: { + id: Number(session.user.id), + epId: session.user.epId, + email: session.user.email || undefined, + }, + approvers, + }) + + if (result.status === 'pending_approval') { + toast({ + title: '낙찰 결재 상신 완료', + description: `결재가 상신되었습니다. (ID: ${result.approvalId})`, + }) + setIsApprovalDialogOpen(false) + onOpenChange(false) + setSelectionReason('') + onSuccess() + } + } catch (error) { + console.error('결재 상신 중 오류 발생:', error) + toast({ + title: '오류', + description: '결재 상신 중 오류가 발생했습니다.', + variant: 'destructive', + }) + } + } return ( <Dialog open={open} onOpenChange={onOpenChange}> @@ -160,7 +214,7 @@ const userId = session?.user?.id || '2'; </DialogDescription> </DialogHeader> - <form onSubmit={handleSubmit}> + <div> <div className="space-y-6"> {/* 낙찰 업체 정보 */} <Card> @@ -266,14 +320,16 @@ const userId = session?.user?.id || '2'; 취소 </Button> <Button - type="submit" + type="button" + onClick={handleNextStep} disabled={isPending || awardedCompanies.length === 0} > - {isPending ? '상신 중...' : '결재 상신'} + 다음단계 </Button> </DialogFooter> - </form> + </div> </DialogContent> </Dialog> + ) } diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx index 08fc0293..edb72aca 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx @@ -15,6 +15,7 @@ import { Bidding } from '@/db/schema' import { PriceAdjustmentDialog } from '@/components/bidding/price-adjustment-dialog' import { QuotationHistoryDialog } from './quotation-history-dialog' import { ApprovalPreviewDialog } from '@/lib/approval/approval-preview-dialog' +import { ApplicationReasonDialog } from '@/lib/rfq-last/vendor/application-reason-dialog' import { requestBiddingAwardWithApproval } from '@/lib/bidding/approval-actions' import { useToast } from '@/hooks/use-toast' @@ -106,6 +107,12 @@ export function BiddingDetailVendorTableContent({ variables: Record<string, string> title: string selectionReason: string + awardedCompanies: { + companyId: number + companyName: string | null + finalQuoteAmount: number + awardRatio: number + }[] } | null>(null) const [isApprovalPreviewDialogOpen, setIsApprovalPreviewDialogOpen] = React.useState(false) @@ -204,6 +211,7 @@ export function BiddingDetailVendorTableContent({ const result = await requestBiddingAwardWithApproval({ biddingId, selectionReason: approvalPreviewData.selectionReason, + awardedCompanies: approvalPreviewData.awardedCompanies, currentUser: { id: Number(session.user.id), epId: session.user.epId || null, @@ -273,6 +281,7 @@ export function BiddingDetailVendorTableContent({ onSuccess={onRefresh} onApprovalPreview={(data) => { setApprovalPreviewData(data) + setIsAwardDialogOpen(false) setIsApprovalPreviewDialogOpen(true) }} /> diff --git a/lib/bidding/failure/biddings-closure-dialog.tsx b/lib/bidding/failure/biddings-closure-dialog.tsx index cea1f42a..1655d91b 100644 --- a/lib/bidding/failure/biddings-closure-dialog.tsx +++ b/lib/bidding/failure/biddings-closure-dialog.tsx @@ -43,15 +43,12 @@ interface BiddingsClosureDialogProps { } try { - // 결재자 선택 단계로 데이터 전달 + // 결재자 선택 단계로 데이터 전달 (다이얼로그 닫기는 부모 컴포넌트에서 처리) await onApprovalPreview({ description: description.trim(), files: files, biddingId: bidding.id, }) - - // 다이얼로그 닫기 - onOpenChange(false) } catch (error) { console.error('결재 미리보기 준비 실패:', error) toast.error('결재 미리보기 준비 중 오류가 발생했습니다.') diff --git a/lib/bidding/failure/biddings-failure-table.tsx b/lib/bidding/failure/biddings-failure-table.tsx index c4e3be06..fc931307 100644 --- a/lib/bidding/failure/biddings-failure-table.tsx +++ b/lib/bidding/failure/biddings-failure-table.tsx @@ -284,6 +284,9 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) { description: data.description,
files: data.files,
})
+
+ // 폐찰 다이얼로그 닫고 결재 미리보기 열기
+ setBiddingClosureDialogOpen(false)
setIsApprovalPreviewDialogOpen(true)
} catch (error) {
console.error('결재 준비 중 오류 발생:', error)
diff --git a/lib/bidding/handlers.ts b/lib/bidding/handlers.ts index a5cc72ae..760d7900 100644 --- a/lib/bidding/handlers.ts +++ b/lib/bidding/handlers.ts @@ -418,7 +418,7 @@ export async function requestBiddingClosureInternal(payload: { await db .update(biddings) .set({ - status: 'closed', + status: 'bid_closure', updatedBy: payload.currentUserId.toString(), updatedAt: new Date(), remarks: payload.description, // 폐찰 사유를 remarks에 저장 @@ -490,6 +490,12 @@ export async function requestBiddingClosureInternal(payload: { export async function requestBiddingAwardInternal(payload: { biddingId: number; selectionReason: string; + awardedCompanies: Array<{ + companyId: number; + companyName: string | null; + finalQuoteAmount: number; + awardRatio: number; + }>; // ✅ 결재 상신 시점의 낙찰 대상 정보 currentUserId: number; // ✅ 결재 상신한 사용자 ID }) { debugLog('[BiddingAwardHandler] 낙찰 핸들러 시작', { @@ -506,25 +512,83 @@ export async function requestBiddingAwardInternal(payload: { } try { - // 기존 awardBidding 함수 로직을 재구성하여 실행 - const { awardBidding } = await import('@/lib/bidding/detail/service'); - - const result = await awardBidding(payload.biddingId, payload.selectionReason, payload.currentUserId.toString()); + // ✅ 결재 상신 시점의 낙찰 대상 정보를 직접 사용하여 처리 + const { default: db } = await import('@/db/db'); + const { biddings, vendorSelectionResults } = await import('@/db/schema'); + const { eq } = await import('drizzle-orm'); - if (!result.success) { - debugError('[BiddingAwardHandler] 낙찰 처리 실패', result.error); - throw new Error(result.error || '낙찰 처리에 실패했습니다.'); + // 1. 최종입찰가 계산 (낙찰된 업체의 견적금액 * 발주비율의 합) + let finalBidPrice = 0; + for (const company of payload.awardedCompanies) { + finalBidPrice += company.finalQuoteAmount * (company.awardRatio / 100); } + await db.transaction(async (tx) => { + // 1. 입찰 상태를 낙찰로 변경하고 최종입찰가 업데이트 + await tx + .update(biddings) + .set({ + status: 'vendor_selected', + finalBidPrice: finalBidPrice.toString(), + updatedAt: new Date() + }) + .where(eq(biddings.id, payload.biddingId)); + + // 2. 선정 사유 저장 (첫 번째 낙찰 업체 기준으로 저장) + const firstAwardedCompany = payload.awardedCompanies[0]; + + // 기존 선정 결과 확인 + const existingResult = await tx + .select() + .from(vendorSelectionResults) + .where(eq(vendorSelectionResults.biddingId, payload.biddingId)) + .limit(1); + + if (existingResult.length > 0) { + // 업데이트 + await tx + .update(vendorSelectionResults) + .set({ + selectedCompanyId: firstAwardedCompany.companyId, + selectionReason: payload.selectionReason, + selectedBy: payload.currentUserId.toString(), + selectedAt: new Date(), + updatedAt: new Date() + }) + .where(eq(vendorSelectionResults.biddingId, payload.biddingId)); + } else { + // 삽입 + await tx + .insert(vendorSelectionResults) + .values({ + biddingId: payload.biddingId, + selectedCompanyId: firstAwardedCompany.companyId, + selectionReason: payload.selectionReason, + selectedBy: payload.currentUserId.toString(), + selectedAt: new Date(), + createdAt: new Date(), + updatedAt: new Date() + }); + } + }); + + // 캐시 무효화 (API를 통한 방식) + const { revalidateViaCronJob } = await import('@/lib/revalidation-utils'); + await revalidateViaCronJob({ + tags: [`bidding-${payload.biddingId}`, 'quotation-vendors', 'quotation-details'] + }); + debugSuccess('[BiddingAwardHandler] 낙찰 완료', { biddingId: payload.biddingId, selectionReason: payload.selectionReason, + awardedCompaniesCount: payload.awardedCompanies.length, + finalBidPrice, }); return { success: true, biddingId: payload.biddingId, - message: `입찰이 낙찰 처리되었습니다.`, + message: `입찰이 낙찰 처리되었습니다. 최종입찰가: ${finalBidPrice.toLocaleString()}원`, }; } catch (error) { debugError('[BiddingAwardHandler] 낙찰 중 에러', error); @@ -562,7 +626,7 @@ export async function mapBiddingAwardToTemplateVariables(payload: { bidPicName: biddings.bidPicName, supplyPicName: biddings.supplyPicName, targetPrice: biddings.targetPrice, - winnerCount: biddings.winnerCount, + awardCount: biddings.awardCount, }) .from(biddings) .where(eq(biddings.id, biddingId)) @@ -606,7 +670,7 @@ export async function mapBiddingAwardToTemplateVariables(payload: { const title = bidding.title || '낙찰'; const biddingTitle = bidding.title || ''; const biddingNumber = bidding.biddingNumber || ''; - const winnerCount = (bidding.winnerCount || 1).toString(); + const winnerCount = (bidding.awardCount === 'single' ? 1 : bidding.awardCount === 'multiple' ? 2 : 1).toString(); const contractType = bidding.biddingType || ''; const budget = bidding.targetPrice ? bidding.targetPrice.toLocaleString() : ''; const targetPrice = bidding.targetPrice ? bidding.targetPrice.toLocaleString() : ''; diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index d1a0d25c..521f4c33 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -32,7 +32,8 @@ import { SQL, like, notInArray, - inArray + inArray, + isNull } from 'drizzle-orm' import { revalidatePath } from 'next/cache' import { filterColumns } from '@/lib/filter-columns' @@ -3603,6 +3604,83 @@ export async function increaseRoundOrRebid(biddingId: number, userId: string | u }) } + // 11. 첨부파일 복제 (SHI용 및 협력업체용 첨부파일만) + const existingDocuments = await tx + .select() + .from(biddingDocuments) + .where(and( + eq(biddingDocuments.biddingId, biddingId), + // PR 아이템에 연결된 첨부파일은 제외 (SHI용과 협력업체용만 복제) + isNull(biddingDocuments.prItemId), + // SHI용(evaluation_doc) 또는 협력업체용(company_proposal) 문서만 복제 + or( + eq(biddingDocuments.documentType, 'evaluation_doc'), + eq(biddingDocuments.documentType, 'company_proposal') + ) + )) + + if (existingDocuments.length > 0) { + for (const doc of existingDocuments) { + try { + // 기존 파일을 Buffer로 읽어서 File 객체 생성 + const { readFileSync, existsSync } = await import('fs') + const { join } = await import('path') + + const oldFilePath = doc.filePath.startsWith('/uploads/') + ? join(process.cwd(), 'public', doc.filePath) + : doc.filePath + + if (!existsSync(oldFilePath)) { + console.warn(`원본 파일이 존재하지 않음: ${oldFilePath}`) + continue + } + + // 파일 내용을 읽어서 Buffer 생성 + const fileBuffer = readFileSync(oldFilePath) + + // Buffer를 File 객체로 변환 (브라우저 File API 시뮬레이션) + const file = new File([fileBuffer], doc.fileName, { + type: doc.mimeType || 'application/octet-stream' + }) + + // saveFile을 사용하여 새 파일 저장 + const saveResult = await saveFile({ + file, + directory: `biddings/${newBidding.id}/attachments/${doc.documentType === 'evaluation_doc' ? 'shi' : 'vendor'}`, + originalName: `copied_${Date.now()}_${doc.fileName}`, + userId: userName + }) + + if (saveResult.success) { + // 새 첨부파일 레코드 삽입 + await tx.insert(biddingDocuments).values({ + biddingId: newBidding.id, + companyId: doc.companyId, + prItemId: null as any, + specificationMeetingId: null, + documentType: doc.documentType, + fileName: saveResult.fileName!, + originalFileName: doc.originalFileName, + fileSize: saveResult.fileSize!, + mimeType: doc.mimeType, + filePath: saveResult.publicPath!, + title: `${doc.documentType === 'evaluation_doc' ? 'SHI용' : '협력업체용'} 첨부파일 복제 - ${doc.originalFileName}`, + description: doc.description, + isPublic: doc.isPublic, + isRequired: doc.isRequired, + uploadedBy: userName, + uploadedAt: new Date(), + }) + } else { + console.error(`첨부파일 저장 실패 (${doc.fileName}):`, saveResult.error) + } + + } catch (error) { + console.error(`첨부파일 복제 실패 (${doc.fileName}):`, error) + } + } + } + revalidatePath('/bidding') revalidatePath(`/bidding/${biddingId}`) // 기존 입찰 페이지도 갱신 revalidatePath(`/bidding/${newBidding.id}`) @@ -3984,6 +4062,7 @@ export async function getBiddingsForFailure(input: GetBiddingsSchema) { basicConditions.push( or( eq(biddings.status, 'bidding_disposal'), + eq(biddings.status, 'approval_pending'), eq(biddings.status, 'bid_closure') )! ) |
