"use server" import db from "@/db/db" import { eq, and } from "drizzle-orm" import { biddings, biddingCompanies, prItemsForBidding, vendors, generalContracts, generalContractItems } from "@/db/schema" import { createPurchaseOrder } from "@/lib/soap/ecc/send/create-po" import { getCurrentSAPDate } from "@/lib/soap/utils" // TO Contract 서버 액션 export async function transmitToContract(biddingId: number, userId: number) { console.log('=== transmitToContract STARTED ===') console.log('biddingId:', biddingId, 'userId:', userId) try { // 1. 입찰 정보 조회 (단순 쿼리) console.log('Querying bidding...') const bidding = await db.select() .from(biddings) .where(eq(biddings.id, biddingId)) .limit(1) if (!bidding || bidding.length === 0) { throw new Error("입찰 정보를 찾을 수 없습니다.") } const biddingData = bidding[0] console.log('biddingData', biddingData) // 2. 낙찰된 업체들 조회 (별도 쿼리) console.log('Querying bidding companies...') let winnerCompaniesData = [] try { // 2.1 biddingCompanies만 먼저 조회 (join 제거) console.log('Step 1: Querying biddingCompanies only...') const biddingCompaniesRaw = await db.select() .from(biddingCompanies) .where( and( eq(biddingCompanies.biddingId, biddingId), eq(biddingCompanies.isWinner, true) ) ) console.log('biddingCompaniesRaw:', biddingCompaniesRaw) // 2.2 각 company에 대한 vendor 정보 개별 조회 for (const bc of biddingCompaniesRaw) { console.log('Processing companyId:', bc.companyId) try { const vendorData = await db.select() .from(vendors) .where(eq(vendors.id, bc.companyId)) .limit(1) const vendor = vendorData.length > 0 ? vendorData[0] : null console.log('Vendor data for', bc.companyId, ':', vendor) winnerCompaniesData.push({ companyId: bc.companyId, finalQuoteAmount: bc.finalQuoteAmount, vendorCode: vendor?.vendorCode || null, vendorName: vendor?.vendorName || null, }) } catch (vendorError) { console.error('Vendor query error for', bc.companyId, ':', vendorError) // vendor 정보 없이도 진행 winnerCompaniesData.push({ companyId: bc.companyId, finalQuoteAmount: bc.finalQuoteAmount, vendorCode: null, vendorName: null, }) } } console.log('winnerCompaniesData type:', typeof winnerCompaniesData) console.log('winnerCompaniesData length:', winnerCompaniesData?.length) console.log('winnerCompaniesData:', winnerCompaniesData) } catch (queryError) { console.error('Query error:', queryError) throw new Error(`biddingCompanies 쿼리 실패: ${queryError}`) } // 상태 검증 console.log('biddingData.status', biddingData.status) if (biddingData.status !== 'vendor_selected') { throw new Error("업체 선정이 완료되지 않은 입찰입니다.") } // 낙찰된 업체 검증 if (winnerCompaniesData.length === 0) { throw new Error("낙찰된 업체가 없습니다.") } console.log('Processing', winnerCompaniesData.length, 'winner companies') for (const winnerCompany of winnerCompaniesData) { // 계약 번호 자동 생성 (현재 시간 기반) const contractNumber = `CONTRACT-BID-${Date.now()}-${winnerCompany.companyId}` console.log('contractNumber', contractNumber) // general-contract 생성 const contractResult = await db.insert(generalContracts).values({ contractNumber, revision: 0, contractSourceType: 'bid', // 입찰에서 생성됨 status: 'Draft', category: biddingData.contractType as any, // 단가계약, 일반계약, 매각계약 name: biddingData.title, selectionMethod: '입찰', vendorId: winnerCompany.companyId, linkedBidNumber: biddingData.biddingNumber, contractAmount: winnerCompany.finalQuoteAmount || undefined, currency: biddingData.currency || 'KRW', registeredById: userId, // TODO: 현재 사용자 ID로 변경 필요 lastUpdatedById: userId, // TODO: 현재 사용자 ID로 변경 필요 }).returning({ id: generalContracts.id }) console.log('contractResult', contractResult) const contractId = contractResult[0].id // 3. PR 아이템들로 general-contract-items 생성 (일단 생략) console.log('Skipping PR items creation for now') } return { success: true, message: `${winnerCompaniesData.length}개의 계약서가 생성되었습니다.` } } catch (error) { console.error('TO Contract 실패:', error) throw new Error(error instanceof Error ? error.message : '계약서 생성에 실패했습니다.') } } // TO PO 서버 액션 export async function transmitToPO(biddingId: number) { try { // 1. 입찰 정보 및 낙찰 업체 조회 const bidding = await db.query.biddings.findFirst({ where: eq(biddings.id, biddingId), with: { biddingCompanies: { where: eq(biddingCompanies.isWinner, true), // 낙찰된 업체만 with: { vendor: true } }, prItemsForBidding: true } }) if (!bidding) { throw new Error("입찰 정보를 찾을 수 없습니다.") } if (bidding.status !== 'vendor_selected') { throw new Error("업체 선정이 완료되지 않은 입찰입니다.") } const winnerCompanies = bidding.biddingCompanies.filter(bc => bc.isWinner) if (winnerCompanies.length === 0) { throw new Error("낙찰된 업체가 없습니다.") } // 2. PO 데이터 구성 const poData = { T_Bidding_HEADER: winnerCompanies.map((company, index) => ({ ANFNR: bidding.biddingNumber, LIFNR: company.vendor?.vendorCode || `VENDOR${company.companyId}`, ZPROC_IND: 'A', // 구매 처리 상태 ANGNR: bidding.biddingNumber, WAERS: bidding.currency || 'KRW', ZTERM: '0001', // 기본 지급조건 INCO1: 'FOB', INCO2: 'Seoul, Korea', MWSKZ: 'V0', // 세금 코드 LANDS: 'KR', ZRCV_DT: getCurrentSAPDate(), ZATTEN_IND: 'Y', IHRAN: getCurrentSAPDate(), TEXT: `PO from Bidding: ${bidding.title}`, })), T_Bidding_ITEM: bidding.prItemsForBidding?.map((item, index) => ({ ANFNR: bidding.biddingNumber, ANFPS: (index + 1).toString().padStart(5, '0'), LIFNR: winnerCompanies[0]?.vendor?.vendorCode || `VENDOR${winnerCompanies[0]?.companyId}`, NETPR: item.annualUnitPrice?.toString() || '0', PEINH: '1', BPRME: item.quantityUnit || 'EA', NETWR: item.annualUnitPrice && item.quantity ? (item.annualUnitPrice * item.quantity).toString() : '0', BRTWR: item.annualUnitPrice && item.quantity ? ((item.annualUnitPrice * item.quantity) * 1.1).toString() // 10% 부가세 가정 : '0', LFDAT: item.requestedDeliveryDate?.toISOString().split('T')[0] || getCurrentSAPDate(), })) || [], T_PR_RETURN: [{ ANFNR: bidding.biddingNumber, ANFPS: '00001', EBELN: `PR${bidding.biddingNumber}`, EBELP: '00001', MSGTY: 'S', MSGTXT: 'Success' }] } // 3. SAP으로 PO 전송 const result = await createPurchaseOrder(poData) if (!result.success) { throw new Error(result.message) } return { success: true, message: result.message } } catch (error) { console.error('TO PO 실패:', error) throw new Error(error instanceof Error ? error.message : 'PO 전송에 실패했습니다.') } }