diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-12 10:42:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-12 10:42:36 +0000 |
| commit | 8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 (patch) | |
| tree | 36bd57d147ba929f1d72918d1fb91ad2c4778624 /lib/bidding/selection/actions.ts | |
| parent | 57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff) | |
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'lib/bidding/selection/actions.ts')
| -rw-r--r-- | lib/bidding/selection/actions.ts | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/lib/bidding/selection/actions.ts b/lib/bidding/selection/actions.ts new file mode 100644 index 00000000..e17e9292 --- /dev/null +++ b/lib/bidding/selection/actions.ts @@ -0,0 +1,219 @@ +"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: [] + } + } + } + + const snapshots = companyData[0].quotationSnapshots as any[] + + // 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, + itemCode: prItemsForBidding.itemCode, + itemName: prItemsForBidding.itemName, + specification: prItemsForBidding.specification, + quantity: prItemsForBidding.quantity, + unit: prItemsForBidding.unit, + deliveryDate: prItemsForBidding.deliveryDate + }) + .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?.itemCode || `ITEM${item.prItemId}`, + itemName: prItem?.itemName || '품목 정보 없음', + specification: prItem?.specification || item.technicalSpecification || '-', + quantity: prItem?.quantity || 0, + unit: prItem?.unit || 'EA', + unitPrice: item.bidUnitPrice, + totalPrice: item.bidAmount, + deliveryDate: item.proposedDeliveryDate ? new Date(item.proposedDeliveryDate) : prItem?.deliveryDate ? new Date(prItem.deliveryDate) : 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: '견적 히스토리 조회 중 오류가 발생했습니다.' + } + } +} |
