summaryrefslogtreecommitdiff
path: root/lib/bidding/selection/actions.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-12 10:42:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-12 10:42:36 +0000
commit8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 (patch)
tree36bd57d147ba929f1d72918d1fb91ad2c4778624 /lib/bidding/selection/actions.ts
parent57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff)
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'lib/bidding/selection/actions.ts')
-rw-r--r--lib/bidding/selection/actions.ts219
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: '견적 히스토리 조회 중 오류가 발생했습니다.'
+ }
+ }
+}