"use server"; import db from "@/db/db"; import { rfqsLast, rfqLastDetails,rfqPrItems, prItemsForBidding,biddingConditions,biddingCompanies, projects, biddings,generalContracts ,generalContractItems} 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"; // ===== PO (SAP) 생성 ===== interface CreatePOParams { rfqId: number; vendorId: number; vendorName: string; totalAmount: number; currency: string; selectionReason?: string; } export async function createPO(params: CreatePOParams) { try { const userId = 1; // TODO: 실제 사용자 ID 가져오기 // 1. 선정된 업체 확인 const [selectedVendor] = await db .select() .from(rfqLastDetails) .where( and( eq(rfqLastDetails.rfqsLastId, params.rfqId), eq(rfqLastDetails.vendorsId, params.vendorId), eq(rfqLastDetails.isSelected, true), eq(rfqLastDetails.isLatest, true) ) ); if (!selectedVendor) { throw new Error("선정된 업체 정보를 찾을 수 없습니다."); } // 2. SAP 연동 로직 (TODO: 실제 구현 필요) // - SAP API 호출 // - PO 번호 생성 // - 아이템 정보 전송 // - 결재 라인 설정 // 3. 계약 상태 업데이트 await db.transaction(async (tx) => { // rfqLastDetails에 계약 정보 업데이트 await tx .update(rfqLastDetails) .set({ contractStatus: "진행중", contractCreatedAt: new Date(), contractNo: `PO-${Date.now()}`, // TODO: 실제 PO 번호로 변경 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(), // }) // .where(eq(rfqsLast.id, params.rfqId)); }); revalidatePath(`/rfq/${params.rfqId}`); revalidatePath("/rfq"); return { success: true, message: "PO가 성공적으로 생성되었습니다.", poNumber: `PO-${Date.now()}`, // TODO: 실제 PO 번호 반환 }; } catch (error) { console.error("PO 생성 오류:", error); return { success: false, error: error instanceof Error ? error.message : "PO 생성 중 오류가 발생했습니다." }; } } 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 : "계약 상태 확인 중 오류가 발생했습니다." }; } }