"use server" import db from "@/db/db" import { eq, and, sql, isNull } from "drizzle-orm" import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" // @ts-ignore - Next.js cache import issue in server actions const { revalidatePath } = require("next/cache") import { biddings, biddingCompanies, prItemsForBidding, companyPrItemBids, vendors, generalContracts, generalContractItems, vendorSelectionResults, biddingDocuments } from "@/db/schema" import { saveFile } from '@/lib/file-stroage' interface SaveSelectionResultData { biddingId: number summary: string attachments?: File[] } export async function saveSelectionResult(data: SaveSelectionResultData) { try { const session = await getServerSession(authOptions) if (!session?.user?.id) { return { success: false, error: '인증되지 않은 사용자입니다.' } } // 기존 선정결과 확인 (selectedCompanyId가 null인 레코드) // 타입 에러를 무시하고 전체 조회 후 필터링 const allResults = await db .select() .from(vendorSelectionResults) .where(eq(vendorSelectionResults.biddingId, data.biddingId)) // @ts-ignore const existingResult = allResults.filter((result: any) => result.selectedCompanyId === null).slice(0, 1) const resultData = { biddingId: data.biddingId, selectedCompanyId: null, // 전체 선정결과 selectionReason: '전체 선정결과', evaluationSummary: data.summary, hasResultDocuments: data.attachments && data.attachments.length > 0, selectedBy: session.user.id } let resultId: number if (existingResult.length > 0) { // 업데이트 await db .update(vendorSelectionResults) .set({ ...resultData, updatedAt: new Date() }) .where(eq(vendorSelectionResults.id, existingResult[0].id)) resultId = existingResult[0].id } else { // 새로 생성 const insertResult = await db.insert(vendorSelectionResults).values(resultData).returning({ id: vendorSelectionResults.id }) resultId = insertResult[0].id } // 첨부파일 처리 if (data.attachments && data.attachments.length > 0) { // 기존 첨부파일 삭제 (documentType이 'selection_result'인 것들) await db .delete(biddingDocuments) .where(and( eq(biddingDocuments.biddingId, data.biddingId), eq(biddingDocuments.documentType, 'selection_result') )) // 새 첨부파일 저장 const documentInserts: Array = [] for (const file of data.attachments) { // saveFile을 사용하여 파일 저장 const saveResult = await saveFile({ file, directory: `bidding/${data.biddingId}/selection`, originalName: file.name, userId: session.user.id }) if (saveResult.success && saveResult.publicPath) { documentInserts.push({ biddingId: data.biddingId, companyId: null, documentType: 'selection_result' as const, fileName: saveResult.fileName || file.name, originalFileName: saveResult.originalName || file.name, fileSize: saveResult.fileSize || file.size, mimeType: file.type, filePath: saveResult.publicPath, uploadedBy: session.user.id }) } else { console.error('Failed to save file:', saveResult.error) } } if (documentInserts.length > 0) { await db.insert(biddingDocuments).values(documentInserts) } } revalidatePath(`/evcp/bid-selection/${data.biddingId}/detail`) return { success: true, message: '선정결과가 성공적으로 저장되었습니다.' } } catch (error) { console.error('Failed to save selection result:', error) return { success: false, error: '선정결과 저장 중 오류가 발생했습니다.' } } } // 선정결과 조회 export async function getSelectionResult(biddingId: number) { try { // 선정결과 조회 (selectedCompanyId가 null인 레코드) const allResults = await db .select() .from(vendorSelectionResults) .where(eq(vendorSelectionResults.biddingId, biddingId)) // @ts-ignore const existingResult = allResults.filter((result: any) => result.selectedCompanyId === null).slice(0, 1) if (existingResult.length === 0) { return { success: true, data: { summary: '', attachments: [] } } } const result = existingResult[0] // 첨부파일 조회 const documents = await db .select({ id: biddingDocuments.id, fileName: biddingDocuments.fileName, originalFileName: biddingDocuments.originalFileName, fileSize: biddingDocuments.fileSize, mimeType: biddingDocuments.mimeType, filePath: biddingDocuments.filePath, uploadedAt: biddingDocuments.uploadedAt }) .from(biddingDocuments) .where(and( eq(biddingDocuments.biddingId, biddingId), eq(biddingDocuments.documentType, 'selection_result') )) return { success: true, data: { summary: result.evaluationSummary || '', attachments: documents.map(doc => ({ id: doc.id, fileName: doc.fileName || doc.originalFileName || '', originalFileName: doc.originalFileName || '', fileSize: doc.fileSize || 0, mimeType: doc.mimeType || '', filePath: doc.filePath || '', uploadedAt: doc.uploadedAt })) } } } catch (error) { console.error('Failed to get selection result:', error) return { success: false, error: '선정결과 조회 중 오류가 발생했습니다.', data: { summary: '', attachments: [] } } } } // 견적 히스토리 조회 export async function getQuotationHistory(biddingId: number, vendorId: number) { try { // 현재 bidding의 biddingNumber와 originalBiddingNumber 조회 const currentBiddingInfo = await db .select({ biddingNumber: biddings.biddingNumber, originalBiddingNumber: biddings.originalBiddingNumber }) .from(biddings) .where(eq(biddings.id, biddingId)) .limit(1) if (!currentBiddingInfo.length) { return { success: true, data: { history: [] } } } const baseNumber = currentBiddingInfo[0].originalBiddingNumber || currentBiddingInfo[0].biddingNumber.split('-')[0] // 동일한 originalBiddingNumber를 가진 모든 bidding 조회 const relatedBiddings = await db .select({ id: biddings.id, biddingNumber: biddings.biddingNumber, targetPrice: biddings.targetPrice, currency: biddings.currency, createdAt: biddings.createdAt }) .from(biddings) .where(eq(biddings.originalBiddingNumber, baseNumber)) .orderBy(biddings.createdAt) // 각 bidding에 대한 벤더의 견적 정보 및 상세 아이템 조회 const historyPromises = relatedBiddings.map(async (bidding) => { // 1. 견적 헤더 정보 조회 (ID 포함) const biddingCompanyData = await db .select({ id: biddingCompanies.id, finalQuoteAmount: biddingCompanies.finalQuoteAmount, responseSubmittedAt: biddingCompanies.finalQuoteSubmittedAt, isFinalSubmission: biddingCompanies.isFinalSubmission }) .from(biddingCompanies) .where(and( eq(biddingCompanies.biddingId, bidding.id), eq(biddingCompanies.companyId, vendorId) )) .limit(1) if (!biddingCompanyData.length || !biddingCompanyData[0].finalQuoteAmount || !biddingCompanyData[0].responseSubmittedAt) { return null } // 2. 아이템별 견적 및 상세 정보 조회 (Join 사용) const prItemBids = await db .select({ // 견적 정보 bidUnitPrice: companyPrItemBids.bidUnitPrice, bidAmount: companyPrItemBids.bidAmount, proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate, // 아이템 상세 정보 prItemId: prItemsForBidding.id, itemNumber: prItemsForBidding.itemNumber, itemInfo: prItemsForBidding.itemInfo, quantity: prItemsForBidding.quantity, quantityUnit: prItemsForBidding.quantityUnit, requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate }) .from(companyPrItemBids) .innerJoin(prItemsForBidding, eq(companyPrItemBids.prItemId, prItemsForBidding.id)) .where(eq(companyPrItemBids.biddingCompanyId, biddingCompanyData[0].id)) // 아이템 매핑 const items = prItemBids.map(bid => ({ itemCode: bid.itemNumber || `ITEM${bid.prItemId}`, itemName: bid.itemInfo || '품목 정보 없음', quantity: bid.quantity ? parseFloat(bid.quantity.toString()) : 0, unit: bid.quantityUnit || 'EA', unitPrice: bid.bidUnitPrice ? parseFloat(bid.bidUnitPrice.toString()) : 0, totalPrice: bid.bidAmount ? parseFloat(bid.bidAmount.toString()) : 0, deliveryDate: bid.proposedDeliveryDate ? new Date(bid.proposedDeliveryDate) : bid.requestedDeliveryDate ? new Date(bid.requestedDeliveryDate) : new Date() })) const targetPrice = bidding.targetPrice ? parseFloat(bidding.targetPrice.toString()) : null const totalAmount = parseFloat(biddingCompanyData[0].finalQuoteAmount.toString()) const vsTargetPrice = targetPrice && targetPrice > 0 ? ((totalAmount - targetPrice) / targetPrice) * 100 : 0 return { biddingId: bidding.id, biddingNumber: bidding.biddingNumber, submittedAt: new Date(biddingCompanyData[0].responseSubmittedAt), totalAmount, currency: bidding.currency || 'KRW', vsTargetPrice: parseFloat(vsTargetPrice.toFixed(2)), items } }) const historyData = (await Promise.all(historyPromises)).filter(Boolean) // biddingNumber의 suffix를 기준으로 정렬 (-01, -02, -03 등) const sortedHistory = historyData.sort((a, b) => { const aSuffix = a!.biddingNumber.split('-')[1] ? parseInt(a!.biddingNumber.split('-')[1]) : 0 const bSuffix = b!.biddingNumber.split('-')[1] ? parseInt(b!.biddingNumber.split('-')[1]) : 0 return aSuffix - bSuffix }) // 회차 정보 추가 const history = sortedHistory.map((item, index) => ({ id: item!.biddingId, round: index + 1, // 1차, 2차, 3차... ...item! })) return { success: true, data: { history } } } catch (error) { console.error('Failed to get quotation history:', error) return { success: false, error: '견적 히스토리 조회 중 오류가 발생했습니다.' } } }