"use server"; import db from "@/db/db"; import { rfqsLast, rfqLastDetails,rfqPrItems, prItemsForBidding,biddingConditions,biddingCompanies, projects, biddings,generalContracts ,generalContractItems, vendors, rfqLastVendorResponses, rfqLastVendorQuotationItems, incoterms} from "@/db/schema"; import { eq, and } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { generateContractNumber } from "../general-contracts/service"; import { generateBiddingNumber } from "../bidding/service"; import { sendRFQInformation } from "@/lib/soap/ecc/send/create-po-rfq"; import { getCurrentSAPDate } from "@/lib/soap/utils"; import { SoapResponseError } from "@/lib/soap/types"; // ===== PO (SAP) 생성 ===== interface CreatePOParams { rfqId: number; vendorId: number; vendorName: string; totalAmount: number; currency: string; selectionReason?: string; } export async function createPO(params: CreatePOParams) { try { const session = await getServerSession(authOptions); if (!session?.user) { throw new Error("인증이 필요합니다."); } const userId = Number(session.user.id); // 1. RFQ 정보 조회 const [rfqData] = await db .select() .from(rfqsLast) .where(eq(rfqsLast.id, params.rfqId)); if (!rfqData) { throw new Error("RFQ 정보를 찾을 수 없습니다."); } // 2. 선정된 업체 확인 및 상세 정보 조회 (벤더 국가 정보 포함) const [selectedVendor] = await db .select({ detail: rfqLastDetails, vendor: vendors, }) .from(rfqLastDetails) .leftJoin(vendors, eq(rfqLastDetails.vendorsId, vendors.id)) .where( and( eq(rfqLastDetails.rfqsLastId, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true), eq(rfqLastDetails.isLatest, true) ) ); if (!selectedVendor || !selectedVendor.vendor) { throw new Error("선정된 업체 정보를 찾을 수 없습니다."); } const vendorData = selectedVendor.vendor; const detailData = selectedVendor.detail; // 벤더 국가 코드 확인 (SAP LANDS 필드용) // SAP 국가 코드는 2자리 (KR, US, CN 등) const vendorCountryCode = vendorData.country?.substring(0, 2).toUpperCase() || 'KR'; // 3. PR 아이템 정보 조회 const prItems = await db .select() .from(rfqPrItems) .where(eq(rfqPrItems.rfqsLastId, params.rfqId)); if (prItems.length === 0) { throw new Error("PR 아이템 정보를 찾을 수 없습니다."); } // 3-1. 선정된 업체의 최신 견적 응답 조회 const [vendorResponse] = await db .select() .from(rfqLastVendorResponses) .where( and( eq(rfqLastVendorResponses.rfqsLastId, params.rfqId), eq(rfqLastVendorResponses.vendorId, params.vendorId), eq(rfqLastVendorResponses.isLatest, true) ) ); if (!vendorResponse) { throw new Error("선정된 업체의 견적 응답을 찾을 수 없습니다."); } // 3-2. 견적 아이템별 가격 정보 조회 const quotationItems = await db .select() .from(rfqLastVendorQuotationItems) .where(eq(rfqLastVendorQuotationItems.vendorResponseId, vendorResponse.id)); if (quotationItems.length === 0) { throw new Error("견적 아이템 정보를 찾을 수 없습니다."); } // 3-3. PR 아이템 ID로 견적 아이템 매핑 생성 const quotationItemMap = new Map( quotationItems.map(item => [item.rfqPrItemId, item]) ); // 4. 필수 필드 설정 - 벤더 견적 응답 조건 우선, 구매자 제시 조건을 fallback으로 사용 // 우선순위: vendorResponse (벤더 제출 조건) > detailData (구매자 제시 조건) const validationErrors: string[] = []; // 헤더 필수 필드 검증 및 설정 // 1. LIFNR - 벤더 코드 if (!vendorData.vendorCode) { validationErrors.push(`❌ 벤더 코드 (Vendor ID: ${vendorData.id})`); } const vendorCode = vendorData.vendorCode || ''; // 2. ANFNR - RFQ 번호 const anfnr = rfqData.ANFNR || rfqData.rfqCode; if (!anfnr) { validationErrors.push(`❌ RFQ 번호 (ANFNR 또는 rfqCode)`); } // 3. WAERS - 통화 (벤더 견적 통화 우선) const currency = vendorResponse.vendorCurrency || vendorResponse.currency || detailData.currency || params.currency || ''; if (!currency) { validationErrors.push(`❌ 통화(Currency) - 벤더 견적 또는 RFQ 조건에 통화가 설정되지 않았습니다.`); } // 4. ZTERM - 지급조건 (벤더 제안 조건 우선) const paymentTermsCode = vendorResponse.vendorPaymentTermsCode || detailData.paymentTermsCode || ''; if (!paymentTermsCode) { validationErrors.push(`❌ 지급조건(Payment Terms) - 벤더 견적 또는 RFQ 조건에 지급조건이 설정되지 않았습니다.`); } // 5. INCO1 - 인코텀즈 코드 (벤더 제안 조건 우선) const incotermsCode = vendorResponse.vendorIncotermsCode || detailData.incotermsCode || ''; if (!incotermsCode) { validationErrors.push(`❌ 인코텀즈 코드(Incoterms) - 벤더 견적 또는 RFQ 조건에 인코텀즈가 설정되지 않았습니다.`); } // 6. INCO2 - 인코텀즈 상세 설명 (벤더 제안 조건 우선) let incotermsDescription = ''; if (incotermsCode) { // 우선순위: 1) 벤더 제출 상세 정보, 2) incoterms 테이블 조회, 3) 구매자 제시 상세 정보 if (vendorResponse.vendorIncotermsDetail) { incotermsDescription = vendorResponse.vendorIncotermsDetail; } else { const [incotermsData] = await db .select({ description: incoterms.description }) .from(incoterms) .where(eq(incoterms.code, incotermsCode)) .limit(1); if (incotermsData?.description) { incotermsDescription = incotermsData.description; } else if (detailData.incotermsDetail) { incotermsDescription = detailData.incotermsDetail; } else { validationErrors.push(`❌ 인코텀즈 상세 정보(INCO2) - 인코텀즈 코드 '${incotermsCode}'에 대한 설명을 찾을 수 없습니다.`); } } } // 7. MWSKZ - 세금코드 (벤더 제안 조건 우선) const taxCode = vendorResponse.vendorTaxCode || detailData.taxCode || ''; if (!taxCode) { validationErrors.push(`❌ 세금코드(Tax Code) - 벤더 견적 또는 RFQ 조건에 세금코드가 설정되지 않았습니다.`); } // PR 아이템 필드를 미리 검증하기 위해 순회 prItems.forEach((item, index) => { const itemNum = index + 1; if (!item.uom) { validationErrors.push(`❌ PR 아이템 ${itemNum}번 - 단위(UOM)`); } const quoteItem = quotationItemMap.get(item.id); if (!quoteItem) { validationErrors.push(`❌ PR 아이템 ${itemNum}번 - 견적 정보 없음 (PR Item ID: ${item.id})`); } else { const unitPrice = Number(quoteItem.unitPrice) || 0; const totalPrice = Number(quoteItem.totalPrice) || 0; if (unitPrice <= 0 || totalPrice <= 0) { validationErrors.push(`❌ PR 아이템 ${itemNum}번 - 가격 정보 (단가: ${unitPrice}, 총액: ${totalPrice})`); } } }); // 검증 에러가 있으면 한 번에 throw if (validationErrors.length > 0) { const errorMessage = `SAP PO 생성을 위한 필수 필드가 누락되었습니다:\n\n${validationErrors.join('\n')}\n\n위 필드들을 모두 입력해주세요.`; console.error("❌ 필수 필드 검증 실패:", errorMessage); throw new Error(errorMessage); } // 검증 완료: 이제 안전하게 사용 가능 (타입 단언) const validAnfnr = anfnr as string; const validVendorCode = vendorCode as string; const validPaymentTermsCode = paymentTermsCode as string; const validIncotermsCode = incotermsCode as string; const validTaxCode = taxCode as string; // 5. RFQ 정보 데이터 구성 (IF_EVCP_ECC_RFQ_INFORMATION 인터페이스 사용) const rfqInfoData = { T_RFQ_HEADER: [{ // 필수 필드 ANFNR: validAnfnr, LIFNR: validVendorCode, WAERS: currency, ZTERM: validPaymentTermsCode, INCO1: validIncotermsCode, INCO2: incotermsDescription.substring(0, 28), // SAP 최대 28자리 제한, incoterms 테이블의 description 사용 MWSKZ: validTaxCode, LANDS: vendorCountryCode, // 벤더 국가 코드 사용 // Optional 필드 VSTEL: detailData.placeOfShipping || '', // Place of Shipping LSTEL: detailData.placeOfDestination || '', // Place of Destination }], T_RFQ_ITEM: prItems.map((item, index) => { // 견적 아이템에서 실제 가격 정보 가져오기 (이미 검증됨) const quoteItem = quotationItemMap.get(item.id)!; // 검증 통과했으므로 non-null assertion // 가격 계산: SAP은 소수점을 포함한 문자열 형태로 받음 // DB에서 가져온 값을 명시적으로 숫자로 변환 (문자열이나 Decimal 타입일 수 있음) const unitPrice = Number(quoteItem.unitPrice); const quantity = Number(quoteItem.quantity || item.quantity); const totalPrice = Number(quoteItem.totalPrice) || (unitPrice * quantity); // 납기일 계산 (우선순위: 견적 납기일 > PR 납기일 > 현재일자) let deliveryDate = getCurrentSAPDate(); if (quoteItem.vendorDeliveryDate) { deliveryDate = new Date(quoteItem.vendorDeliveryDate).toISOString().split('T')[0].replace(/-/g, ''); } else if (item.deliveryDate) { deliveryDate = new Date(item.deliveryDate).toISOString().split('T')[0].replace(/-/g, ''); } return { // 필수 필드 ANFNR: validAnfnr, ANFPS: item.prItem || (index + 1).toString().padStart(5, '0'), // PR Item Number 사용 NETPR: unitPrice.toFixed(2), // 단가 (소수점 2자리) NETWR: totalPrice.toFixed(2), // 순액 (세금 제외) BRTWR: totalPrice.toFixed(2), // 총액: SAP이 taxCode(MWSKZ)로 세금 계산하도록 순액과 동일하게 전송 // Optional 필드 LFDAT: deliveryDate, }; }) }; console.log('📤 SAP으로 RFQ 정보 전송 시작:', { ANFNR: validAnfnr, LIFNR: validVendorCode, vendorName: vendorData.vendorName, vendorCountry: vendorCountryCode, itemCount: prItems.length, quotationItemCount: quotationItems.length, totalAmount: params.totalAmount, currency: currency, paymentTerms: validPaymentTermsCode, incoterms: `${validIncotermsCode} - ${incotermsDescription}`, taxCode: validTaxCode, dataSource: { currency: vendorResponse.vendorCurrency ? '벤더 견적' : (detailData.currency ? '구매자 조건' : 'params'), paymentTerms: vendorResponse.vendorPaymentTermsCode ? '벤더 견적' : '구매자 조건', incoterms: vendorResponse.vendorIncotermsCode ? '벤더 견적' : '구매자 조건', taxCode: vendorResponse.vendorTaxCode ? '벤더 견적' : '구매자 조건', } }); // 디버깅: 전송 데이터 전체 로그 (서버 측 로그이므로 모든 정보 포함) console.log('📦 RFQ 정보 전송 데이터 (전체):', JSON.stringify(rfqInfoData, null, 2)); // 6. SAP SOAP 요청 전송 (RFQ 인터페이스 사용) const sapResult = await sendRFQInformation(rfqInfoData); if (!sapResult.success) { throw new Error(`SAP RFQ 정보 전송 실패: ${sapResult.message}`); } console.log('✅ SAP RFQ 정보 전송 성공:', sapResult); // 7. 실제 RFQ 번호 추출 (SOAP 응답에서 추출하거나 ANFNR 사용) const actualPoNumber = sapResult.rfq_number || validAnfnr; // 8. DB에 실제 PO 번호 저장 및 RFQ 상태 업데이트 await db.transaction(async (tx) => { // rfqLastDetails 업데이트 await tx .update(rfqLastDetails) .set({ contractStatus: "진행중", contractCreatedAt: new Date(), contractNo: actualPoNumber, updatedAt: new Date(), updatedBy: userId, }) .where( and( eq(rfqLastDetails.rfqsLastId, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true) ) ); // RFQ 상태 업데이트 await tx .update(rfqsLast) .set({ status: "최종업체선정", // PO 생성 완료 상태 updatedAt: new Date(), updatedBy: userId, }) .where(eq(rfqsLast.id, params.rfqId)); }); revalidatePath(`/rfq/${params.rfqId}`); revalidatePath("/rfq"); return { success: true, message: "RFQ 정보가 성공적으로 SAP로 전송되었습니다.", poNumber: actualPoNumber, }; } catch (error) { console.error("❌ PO 생성 오류:", error); // 에러 객체에서 추가 정보 추출 (SOAP 응답 포함) const errorResponse: { success: false; error: string; responseData?: string; statusCode?: number; } = { success: false, error: error instanceof Error ? error.message : "PO 생성 중 오류가 발생했습니다." }; // SOAP 응답 에러인 경우 상세 정보 추가 if (error instanceof SoapResponseError) { if (error.responseText) { errorResponse.responseData = error.responseText; console.error("📄 SAP 응답 내용:", error.responseText); } if (error.statusCode) { errorResponse.statusCode = error.statusCode; } } return errorResponse; } } interface CreateGeneralContractParams { rfqId: number; vendorId: number; vendorName: string; totalAmount: number; currency: string; contractType: string; // 계약종류 추가 (UP, LE, IL 등) contractStartDate?: Date; contractEndDate?: Date; contractTerms?: string; } export async function createGeneralContract(params: CreateGeneralContractParams) { try { const session = await getServerSession(authOptions) if (!session?.user) { throw new Error("인증이 필요합니다.") } const userId = session.user.id; // 1. RFQ 정보 및 선정된 업체 확인 const [rfqData] = await db .select({ rfq: rfqsLast, vendor: rfqLastDetails, project: projects, }) .from(rfqsLast) .leftJoin(rfqLastDetails, eq(rfqLastDetails.rfqsLastId, rfqsLast.id)) .leftJoin(projects, eq(projects.id, rfqsLast.projectId)) .where( and( eq(rfqsLast.id, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true), eq(rfqLastDetails.isLatest, true) ) ); if (!rfqData || !rfqData.vendor) { throw new Error("RFQ 정보 또는 선정된 업체 정보를 찾을 수 없습니다."); } // 2. PR 아이템 정보 조회 (계약 아이템으로 변환용) const prItems = await db .select() .from(rfqPrItems) .where(eq(rfqPrItems.rfqsLastId, params.rfqId)); // 3. 계약번호 생성 - generateContractNumber 함수 사용 // 매개변수 순서: (userId, contractType) const contractNumber = await generateContractNumber( rfqData.rfq.picCode || undefined, // 발주담당자 코드 (userId) params.contractType // 계약종류 (UP, LE, IL 등) ); // 4. 트랜잭션으로 계약 생성 const result = await db.transaction(async (tx) => { // 4-1. generalContracts 테이블에 계약 생성 const [newContract] = await tx .insert(generalContracts) .values({ contractNumber, contractSourceType: 'estimate', // 견적에서 넘어온 계약 status: 'Draft', // 초안 상태로 시작 category: '일반계약', type: params.contractType, // 계약종류 저장 (UP, LE, IL 등) executionMethod: '일반계약', name: `${rfqData.project?.name || rfqData.rfq.itemName || ''} 일반계약`, selectionMethod: '견적', // 업체 정보 vendorId: params.vendorId, // 계약 기간 startDate: params.contractStartDate || new Date(), endDate: params.contractEndDate || null, // 연계 정보 linkedRfqOrItb: rfqData.rfq.rfqCode || undefined, // 금액 정보 contractAmount: params.totalAmount, currency: params.currency || 'KRW', totalAmount: params.totalAmount, // 조건 정보 (RFQ에서 가져옴) contractCurrency: rfqData.vendor.currency || 'KRW', paymentTerm: rfqData.vendor.paymentTermsCode || undefined, taxType: rfqData.vendor.taxCode || undefined, deliveryTerm: rfqData.vendor.incotermsCode || undefined, shippingLocation: rfqData.vendor.placeOfShipping || undefined, dischargeLocation: rfqData.vendor.placeOfDestination || undefined, contractDeliveryDate: rfqData.vendor.deliveryDate || undefined, // 연동제 적용 여부 interlockingSystem: rfqData.vendor.materialPriceRelatedYn ? 'Y' : 'N', // 시스템 정보 registeredById: userId, registeredAt: new Date(), lastUpdatedById: userId, lastUpdatedAt: new Date(), notes: params.contractTerms || rfqData.vendor.remark || undefined, }) .returning(); // 4-2. generalContractItems 테이블에 아이템 생성 if (prItems.length > 0) { const contractItems = prItems.map(item => ({ contractId: newContract.id, project: rfqData.project?.name || undefined, itemCode: item.materialCode || undefined, itemInfo: `${item.materialCategory || ''} / ${item.materialCode || ''}`, specification: item.materialDescription || undefined, quantity: item.quantity, quantityUnit: item.uom || undefined, contractDeliveryDate: item.deliveryDate || undefined, contractCurrency: params.currency || 'KRW', // 단가와 금액은 견적 데이터에서 가져와야 함 (현재는 총액을 아이템 수로 나눔) contractUnitPrice: params.totalAmount / prItems.length, contractAmount: params.totalAmount / prItems.length, })); await tx.insert(generalContractItems).values(contractItems); } // 4-3. rfqLastDetails 상태 업데이트 await tx .update(rfqLastDetails) .set({ contractStatus: "일반계약 진행중", contractCreatedAt: new Date(), contractNo: contractNumber, updatedAt: new Date(), updatedBy: userId, }) .where( and( eq(rfqLastDetails.rfqsLastId, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true) ) ); return newContract; }); revalidatePath(`/rfq/${params.rfqId}`); revalidatePath("/rfq"); revalidatePath("/contracts"); return { success: true, message: "일반계약이 성공적으로 생성되었습니다.", contractNumber: result.contractNumber, contractId: result.id, }; } catch (error) { console.error("일반계약 생성 오류:", error); return { success: false, error: error instanceof Error ? error.message : "일반계약 생성 중 오류가 발생했습니다." }; } } // ===== 입찰 생성 ===== interface CreateBiddingParams { rfqId: number; vendorId: number; vendorName: string; totalAmount: number; currency: string; contractType: "unit_price" | "general" | "sale"; // 계약구분 biddingType: string; // 입찰유형 (equipment, construction 등) awardCount: "single" | "multiple"; // 낙찰업체 수 biddingStartDate: Date; biddingEndDate: Date; biddingRequirements?: string; } export async function createBidding(params: CreateBiddingParams) { try { const session = await getServerSession(authOptions) if (!session?.user) { throw new Error("인증이 필요합니다.") } const userId = session.user.id; // 1. RFQ 정보 및 선정된 업체 확인 const [rfqData] = await db .select({ rfq: rfqsLast, vendor: rfqLastDetails, project: projects, }) .from(rfqsLast) .leftJoin(rfqLastDetails, eq(rfqLastDetails.rfqsLastId, rfqsLast.id)) .leftJoin(projects, eq(projects.id, rfqsLast.projectId)) .where( and( eq(rfqsLast.id, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true), eq(rfqLastDetails.isLatest, true) ) ); if (!rfqData || !rfqData.vendor) { throw new Error("RFQ 정보 또는 선정된 업체 정보를 찾을 수 없습니다."); } // 2. PR 아이템 정보 조회 const prItems = await db .select() .from(rfqPrItems) .where(eq(rfqPrItems.rfqsLastId, params.rfqId)); // 3. 트랜잭션으로 입찰 생성 const result = await db.transaction(async (tx) => { // 3-1. 입찰번호 생성 - generateBiddingNumber 함수 사용 const biddingNumber = await generateBiddingNumber( rfqData.rfq.picCode || undefined, // 발주담당자 코드 tx // 트랜잭션 컨텍스트 전달 ); // 3-2. biddings 테이블에 입찰 생성 const [newBidding] = await tx .insert(biddings) .values({ biddingNumber, revision: 0, biddingSourceType: 'estimate', projectId: rfqData.rfq.projectId || undefined, // 기본 정보 projectName: rfqData.project?.name || undefined, itemName: rfqData.rfq.itemName || undefined, title: `${rfqData.project?.name || rfqData.rfq.itemName || ''} 입찰`, description: params.biddingRequirements || rfqData.rfq.remark || undefined, // 계약 정보 - 파라미터에서 받은 값 사용 contractType: params.contractType, biddingType: params.biddingType, awardCount: params.awardCount, // 일정 관리 - 파라미터에서 받은 날짜 사용 biddingRegistrationDate: new Date(), submissionStartDate: params.biddingStartDate, submissionEndDate: params.biddingEndDate, // 예산 및 가격 정보 currency: params.currency || 'KRW', budget: params.totalAmount, targetPrice: params.totalAmount, // PR 정보 prNumber: rfqData.rfq.prNumber || undefined, hasPrDocument: false, // 상태 status: 'bidding_generated', isPublic: false, // 다이얼로그에서 체크박스로 받을 수 있음 isUrgent: false, // 다이얼로그에서 체크박스로 받을 수 있음 // 담당자 정보 managerName: rfqData.rfq.picName || undefined, // 메타 정보 createdBy: String(userId), createdAt: new Date(), updatedAt: new Date(), updatedBy: String(userId), ANFNR: rfqData.rfq.ANFNR || undefined, }) .returning(); // 3-3. PR 아이템을 입찰 아이템으로 변환 if (prItems.length > 0) { const biddingPrItems = prItems.map(item => ({ biddingId: newBidding.id, itemNumber: item.rfqItem || undefined, projectInfo: rfqData.project?.name || undefined, // 프로젝트 이름 사용 itemInfo: item.materialDescription || undefined, requestedDeliveryDate: item.deliveryDate || undefined, currency: params.currency || 'KRW', quantity: item.quantity, quantityUnit: item.uom || undefined, totalWeight: item.grossWeight || undefined, weightUnit: item.gwUom || undefined, materialDescription: item.materialDescription || undefined, prNumber: item.prNo || undefined, hasSpecDocument: !!item.specUrl, })); await tx.insert(prItemsForBidding).values(biddingPrItems); } // 3-4. 선정된 업체를 입찰 참여 업체로 등록 await tx.insert(biddingCompanies).values({ biddingId: newBidding.id, companyId: params.vendorId, invitationStatus: 'pending', preQuoteAmount: params.totalAmount, // 견적 금액을 사전견적으로 사용 isPreQuoteSelected: true, // 본입찰 대상으로 자동 선정 isBiddingInvited: true, notes: '견적에서 선정된 업체', }); // 3-5. 입찰 조건 생성 (RFQ 조건 활용) await tx.insert(biddingConditions).values({ biddingId: newBidding.id, paymentTerms: JSON.stringify([rfqData.vendor.paymentTermsCode]), taxConditions: JSON.stringify([rfqData.vendor.taxCode]), contractDeliveryDate: rfqData.vendor.deliveryDate || undefined, isPriceAdjustmentApplicable: rfqData.vendor.materialPriceRelatedYn || false, incoterms: JSON.stringify([rfqData.vendor.incotermsCode]), shippingPort: rfqData.vendor.placeOfShipping || undefined, destinationPort: rfqData.vendor.placeOfDestination || undefined, }); // 3-6. rfqLastDetails 상태 업데이트 await tx .update(rfqLastDetails) .set({ contractStatus: "입찰진행중", contractCreatedAt: new Date(), contractNo: biddingNumber, updatedAt: new Date(), updatedBy: userId, }) .where( and( eq(rfqLastDetails.rfqsLastId, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true) ) ); return newBidding; }); revalidatePath(`/rfq/${params.rfqId}`); revalidatePath("/rfq"); revalidatePath("/biddings"); return { success: true, message: "입찰이 성공적으로 생성되었습니다.", biddingNumber: result.biddingNumber, biddingId: result.id, }; } catch (error) { console.error("입찰 생성 오류:", error); return { success: false, error: error instanceof Error ? error.message : "입찰 생성 중 오류가 발생했습니다." }; } } // ===== 계약 타입 확인 ===== export async function checkContractStatus(rfqId: number) { try { const [detail] = await db .select({ contractStatus: rfqLastDetails.contractStatus, contractNo: rfqLastDetails.contractNo, contractCreatedAt: rfqLastDetails.contractCreatedAt, }) .from(rfqLastDetails) .where( and( eq(rfqLastDetails.rfqsLastId, rfqId), eq(rfqLastDetails.isSelected, true), eq(rfqLastDetails.isLatest, true) ) ); return { success: true, data: detail, hasContract: !!detail?.contractNo, }; } catch (error) { console.error("계약 상태 확인 오류:", error); return { success: false, error: error instanceof Error ? error.message : "계약 상태 확인 중 오류가 발생했습니다." }; } }