"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" 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 = data.attachments.map(file => ({ biddingId: data.biddingId, companyId: null, documentType: 'selection_result' as const, fileName: file.name, originalFileName: file.name, fileSize: file.size, mimeType: file.type, filePath: `/uploads/bidding/${data.biddingId}/selection/${file.name}`, // 실제 파일 저장 로직 필요 uploadedBy: session.user.id })) 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 getQuotationHistory(biddingId: number, vendorId: number) { try { // biddingCompanies에서 해당 벤더의 스냅샷 데이터 조회 const companyData = await db .select({ quotationSnapshots: biddingCompanies.quotationSnapshots }) .from(biddingCompanies) .where(and( eq(biddingCompanies.biddingId, biddingId), eq(biddingCompanies.companyId, vendorId) )) .limit(1) // 데이터 존재 여부 및 유효성 체크 if (!companyData.length || !companyData[0]?.quotationSnapshots) { return { success: true, data: { history: [] } } } let snapshots = companyData[0].quotationSnapshots // quotationSnapshots가 JSONB 타입이므로 파싱이 필요할 수 있음 if (typeof snapshots === 'string') { try { snapshots = JSON.parse(snapshots) } catch (parseError) { console.error('Failed to parse quotationSnapshots:', parseError) return { success: true, data: { history: [] } } } } // snapshots가 배열인지 확인 if (!Array.isArray(snapshots)) { console.error('quotationSnapshots is not an array:', typeof snapshots) return { success: true, data: { history: [] } } } // PR 항목 정보 조회 (스냅샷의 prItemId로 매핑하기 위해) const prItemIds = snapshots.flatMap(snapshot => snapshot.items?.map((item: any) => item.prItemId) || [] ).filter((id: number, index: number, arr: number[]) => arr.indexOf(id) === index) const prItems = prItemIds.length > 0 ? await db .select({ id: prItemsForBidding.id, itemNumber: prItemsForBidding.itemNumber, itemInfo: prItemsForBidding.itemInfo, quantity: prItemsForBidding.quantity, quantityUnit: prItemsForBidding.quantityUnit, requestedDeliveryDate: prItemsForBidding.requestedDeliveryDate }) .from(prItemsForBidding) .where(sql`${prItemsForBidding.id} IN ${prItemIds}`) : [] // PR 항목을 Map으로 변환하여 빠른 조회를 위해 const prItemMap = new Map(prItems.map(item => [item.id, item])) // bidding 정보 조회 (targetPrice, currency) const biddingInfo = await db .select({ targetPrice: biddings.targetPrice, currency: biddings.currency }) .from(biddings) .where(eq(biddings.id, biddingId)) .limit(1) const targetPrice = biddingInfo[0]?.targetPrice ? parseFloat(biddingInfo[0].targetPrice.toString()) : null const currency = biddingInfo[0]?.currency || 'KRW' // 스냅샷 데이터를 변환 const history = snapshots.map((snapshot: any) => { const vsTargetPrice = targetPrice && targetPrice > 0 ? ((snapshot.totalAmount - targetPrice) / targetPrice) * 100 : 0 const items = snapshot.items?.map((item: any) => { const prItem = prItemMap.get(item.prItemId) return { itemCode: prItem?.itemNumber || `ITEM${item.prItemId}`, itemName: prItem?.itemInfo || '품목 정보 없음', quantity: prItem?.quantity || 0, unit: prItem?.quantityUnit || 'EA', unitPrice: item.bidUnitPrice, totalPrice: item.bidAmount, deliveryDate: item.proposedDeliveryDate ? new Date(item.proposedDeliveryDate) : prItem?.requestedDeliveryDate ? new Date(prItem.requestedDeliveryDate) : new Date() } }) || [] return { id: snapshot.id, round: snapshot.round, submittedAt: new Date(snapshot.submittedAt), totalAmount: snapshot.totalAmount, currency: snapshot.currency || currency, vsTargetPrice: parseFloat(vsTargetPrice.toFixed(2)), items } }) return { success: true, data: { history } } } catch (error) { console.error('Failed to get quotation history:', error) return { success: false, error: '견적 히스토리 조회 중 오류가 발생했습니다.' } } }