"use server" import db from "@/db/db" import { eq, and, sql } from "drizzle-orm" import { biddings, biddingCompanies, prItemsForBidding, vendors, generalContracts, generalContractItems, biddingConditions } from "@/db/schema" import { createPurchaseOrder } from "@/lib/soap/ecc/send/create-po" import { getCurrentSAPDate } from "@/lib/soap/utils" import { generateContractNumber } from "@/lib/general-contracts/service" // TO Contract export async function transmitToContract(biddingId: number, userId: number) { try { // 1. 입찰 정보 조회 (단순 쿼리) 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] // 2. 입찰 조건 정보 조회 const biddingConditionData = await db.select() .from(biddingConditions) .where(eq(biddingConditions.biddingId, biddingId)) .limit(1) const biddingCondition = biddingConditionData.length > 0 ? biddingConditionData[0] : null // 3. 낙찰된 업체들 조회 (별도 쿼리) let winnerCompaniesData: { companyId: number; finalQuoteAmount: string | null; vendorCode: string | null; vendorName: string | null; }[] = [] try { // 2.1 biddingCompanies만 먼저 조회 (join 제거) const biddingCompaniesRaw = await db.select() .from(biddingCompanies) .where( and( eq(biddingCompanies.biddingId, biddingId), eq(biddingCompanies.isWinner, true) ) ) // 2.2 각 company에 대한 vendor 정보 개별 조회 for (const bc of biddingCompaniesRaw) { try { const vendorData = await db.select() .from(vendors) .where(eq(vendors.id, bc.companyId)) .limit(1) const vendor = vendorData.length > 0 ? vendorData[0] : null winnerCompaniesData.push({ companyId: bc.companyId, finalQuoteAmount: bc.finalQuoteAmount, vendorCode: vendor?.vendorCode || null, vendorName: vendor?.vendorName || null as string | null, }) } catch (vendorError) { console.error('Vendor query error for', bc.companyId, ':', vendorError) // vendor 정보 없이도 진행 winnerCompaniesData.push({ companyId: bc.companyId, finalQuoteAmount: bc.finalQuoteAmount, vendorCode: null as string | null, vendorName: null as string | null, }) } } } catch (queryError) { console.error('Query error:', queryError) throw new Error(`biddingCompanies 쿼리 실패: ${queryError}`) } // 상태 검증 if (biddingData.status !== 'vendor_selected') { throw new Error("업체 선정이 완료되지 않은 입찰입니다.") } // 낙찰된 업체 검증 if (winnerCompaniesData.length === 0) { throw new Error("낙찰된 업체가 없습니다.") } for (const winnerCompany of winnerCompaniesData) { // 계약 번호 자동 생성 (실제 규칙에 맞게) const contractNumber = await generateContractNumber(userId.toString(), biddingData.contractType) console.log('Generated contractNumber:', contractNumber) // general-contract 생성 const contractResult = await db.insert(generalContracts).values({ contractNumber, revision: 0, contractSourceType: 'bid', // 입찰에서 생성됨 status: 'Draft', category: biddingData.contractType || 'general', name: biddingData.title, vendorId: winnerCompany.companyId, linkedBidNumber: biddingData.biddingNumber, contractAmount: winnerCompany.finalQuoteAmount || null, contractStartDate: biddingData.contractStartDate || null, contractEndDate: biddingData.contractEndDate || null, currency: biddingData.currency || 'KRW', // 계약 조건 정보 추가 paymentTerm: biddingCondition?.paymentTerms || null, taxType: biddingCondition?.taxConditions || 'V0', deliveryTerm: biddingCondition?.incoterms || 'FOB', shippingLocation: biddingCondition?.shippingPort || null, dischargeLocation: biddingCondition?.destinationPort || null, registeredById: userId, lastUpdatedById: userId, }).returning({ id: generalContracts.id }) console.log('contractResult', contractResult) const contractId = contractResult[0].id // 4. PR 아이템들로 general-contract-items 생성 const prItems = await db.select() .from(prItemsForBidding) .where(eq(prItemsForBidding.biddingId, biddingId)) if (prItems.length > 0) { console.log(`Creating ${prItems.length} contract items for contract ${contractId}`) for (const prItem of prItems) { await db.insert(generalContractItems).values({ contractId, project: prItem.projectInfo || '', itemCode: prItem.itemNumber || '', itemInfo: prItem.itemInfo || '', specification: prItem.materialDescription || '', quantity: prItem.quantity || null, quantityUnit: prItem.quantityUnit || '', contractUnitPrice: prItem.annualUnitPrice || null, contractAmount: prItem.annualUnitPrice && prItem.quantity ? (prItem.annualUnitPrice * prItem.quantity) : null, contractCurrency: biddingData.currency || 'KRW', contractDeliveryDate: prItem.requestedDeliveryDate || null, }) } console.log(`Created ${prItems.length} contract items`) } else { console.log('No PR items found for this bidding') } } 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 biddingData = await db.select() .from(biddings) .where(eq(biddings.id, biddingId)) .limit(1) if (!biddingData || biddingData.length === 0) { throw new Error("입찰 정보를 찾을 수 없습니다.") } const bidding = biddingData[0] if (bidding.status !== 'vendor_selected') { throw new Error("업체 선정이 완료되지 않은 입찰입니다.") } // 2. 입찰 조건 정보 조회 const biddingConditionData = await db.select() .from(biddingConditions) .where(eq(biddingConditions.biddingId, biddingId)) .limit(1) const biddingCondition = biddingConditionData.length > 0 ? biddingConditionData[0] : null // 3. 낙찰된 업체들 조회 const winnerCompaniesRaw = await db.select({ companyId: biddingCompanies.companyId, finalQuoteAmount: biddingCompanies.finalQuoteAmount, vendorCode: vendors.vendorCode, vendorName: vendors.vendorName }) .from(biddingCompanies) .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) .where( and( eq(biddingCompanies.biddingId, biddingId), eq(biddingCompanies.isWinner, true) ) ) if (winnerCompaniesRaw.length === 0) { throw new Error("낙찰된 업체가 없습니다.") } // 4. PR 아이템 조회 const prItems = await db.select() .from(prItemsForBidding) .where(eq(prItemsForBidding.biddingId, biddingId)) // 5. PO 데이터 구성 (bidding condition 정보 사용) const poData = { T_Bidding_HEADER: winnerCompaniesRaw.map((company, index) => ({ ANFNR: bidding.biddingNumber, LIFNR: company.vendorCode || `VENDOR${company.companyId}`, ZPROC_IND: 'A', // 구매 처리 상태 ANGNR: bidding.biddingNumber, WAERS: bidding.currency || 'KRW', ZTERM: biddingCondition?.paymentTerms || '0001', // 지급조건 INCO1: biddingCondition?.incoterms || 'FOB', // Incoterms INCO2: biddingCondition?.destinationPort || biddingCondition?.shippingPort || 'Seoul, Korea', MWSKZ: biddingCondition?.taxConditions || 'V0', // 세금 코드 LANDS: 'KR', ZRCV_DT: getCurrentSAPDate(), ZATTEN_IND: 'Y', IHRAN: getCurrentSAPDate(), TEXT: `PO from Bidding: ${bidding.title}`, })), T_Bidding_ITEM: prItems.map((item, index) => ({ ANFNR: bidding.biddingNumber, ANFPS: (index + 1).toString().padStart(5, '0'), LIFNR: winnerCompaniesRaw[0]?.vendorCode || `VENDOR${winnerCompaniesRaw[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 전송 console.log('SAP으로 PO 전송할 poData', poData) 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 전송에 실패했습니다.') } }