summaryrefslogtreecommitdiff
path: root/lib/rfq-last/contract-actions.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfq-last/contract-actions.ts')
-rw-r--r--lib/rfq-last/contract-actions.ts329
1 files changed, 261 insertions, 68 deletions
diff --git a/lib/rfq-last/contract-actions.ts b/lib/rfq-last/contract-actions.ts
index 1144cf4f..082716a0 100644
--- a/lib/rfq-last/contract-actions.ts
+++ b/lib/rfq-last/contract-actions.ts
@@ -1,9 +1,15 @@
"use server";
import db from "@/db/db";
-import { rfqsLast, rfqLastDetails } from "@/db/schema";
+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 {
@@ -63,13 +69,13 @@ export async function createPO(params: CreatePOParams) {
);
// RFQ 상태 업데이트
- await tx
- .update(rfqsLast)
- .set({
- status: "PO 생성 완료",
- updatedAt: new Date(),
- })
- .where(eq(rfqsLast.id, params.rfqId));
+ // await tx
+ // .update(rfqsLast)
+ // .set({
+ // status: "PO 생성 완료",
+ // updatedAt: new Date(),
+ // })
+ // .where(eq(rfqsLast.id, params.rfqId));
});
revalidatePath(`/rfq/${params.rfqId}`);
@@ -89,14 +95,13 @@ export async function createPO(params: CreatePOParams) {
}
}
-// ===== 일반계약 생성 =====
interface CreateGeneralContractParams {
rfqId: number;
vendorId: number;
vendorName: string;
totalAmount: number;
currency: string;
- contractType?: string;
+ contractType: string; // 계약종류 추가 (UP, LE, IL 등)
contractStartDate?: Date;
contractEndDate?: Date;
contractTerms?: string;
@@ -104,40 +109,126 @@ interface CreateGeneralContractParams {
export async function createGeneralContract(params: CreateGeneralContractParams) {
try {
- const userId = 1; // TODO: 실제 사용자 ID 가져오기
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ throw new Error("인증이 필요합니다.")
+ }
- // 1. 선정된 업체 확인
- const [selectedVendor] = await db
- .select()
- .from(rfqLastDetails)
+ 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(rfqLastDetails.rfqsLastId, params.rfqId),
+ eq(rfqsLast.id, params.rfqId),
eq(rfqLastDetails.vendorsId, params.vendorId),
eq(rfqLastDetails.isSelected, true),
eq(rfqLastDetails.isLatest, true)
)
);
- if (!selectedVendor) {
- throw new Error("선정된 업체 정보를 찾을 수 없습니다.");
+ if (!rfqData || !rfqData.vendor) {
+ throw new Error("RFQ 정보 또는 선정된 업체 정보를 찾을 수 없습니다.");
}
- // 2. 계약 생성 로직 (TODO: 실제 구현 필요)
- // - 계약서 템플릿 선택
- // - 계약 조건 설정
- // - 계약서 문서 생성
- // - 전자서명 프로세스 시작
+ // 2. PR 아이템 정보 조회 (계약 아이템으로 변환용)
+ const prItems = await db
+ .select()
+ .from(rfqPrItems)
+ .where(eq(rfqPrItems.rfqsLastId, params.rfqId));
- // 3. 계약 상태 업데이트
- await db.transaction(async (tx) => {
- // rfqLastDetails에 계약 정보 업데이트
+ // 3. 계약번호 생성 - generateContractNumber 함수 사용
+ const contractNumber = await generateContractNumber(
+ params.contractType, // 계약종류 (UP, LE, IL 등)
+ rfqData.rfq.picCode || undefined // 발주담당자 코드
+ );
+
+ // 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: "진행중",
+ contractStatus: "일반계약 진행중",
contractCreatedAt: new Date(),
- contractNo: `CONTRACT-${Date.now()}`, // TODO: 실제 계약번호로 변경
+ contractNo: contractNumber,
updatedAt: new Date(),
updatedBy: userId,
})
@@ -149,23 +240,18 @@ export async function createGeneralContract(params: CreateGeneralContractParams)
)
);
- // RFQ 상태 업데이트
- await tx
- .update(rfqsLast)
- .set({
- status: "일반계약 진행중",
- updatedAt: new Date(),
- })
- .where(eq(rfqsLast.id, params.rfqId));
+ return newContract;
});
revalidatePath(`/rfq/${params.rfqId}`);
revalidatePath("/rfq");
+ revalidatePath("/contracts");
return {
success: true,
message: "일반계약이 성공적으로 생성되었습니다.",
- contractNumber: `CONTRACT-${Date.now()}`, // TODO: 실제 계약번호 반환
+ contractNumber: result.contractNumber,
+ contractId: result.id,
};
} catch (error) {
console.error("일반계약 생성 오류:", error);
@@ -183,49 +269,162 @@ interface CreateBiddingParams {
vendorName: string;
totalAmount: number;
currency: string;
- biddingType?: string; // 공개입찰, 제한입찰 등
- biddingStartDate?: Date;
- biddingEndDate?: Date;
+ 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 userId = 1; // TODO: 실제 사용자 ID 가져오기
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ throw new Error("인증이 필요합니다.")
+ }
- // 1. 선정된 업체 확인
- const [selectedVendor] = await db
- .select()
- .from(rfqLastDetails)
+ 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(rfqLastDetails.rfqsLastId, params.rfqId),
+ eq(rfqsLast.id, params.rfqId),
eq(rfqLastDetails.vendorsId, params.vendorId),
eq(rfqLastDetails.isSelected, true),
eq(rfqLastDetails.isLatest, true)
)
);
- if (!selectedVendor) {
- throw new Error("선정된 업체 정보를 찾을 수 없습니다.");
+ if (!rfqData || !rfqData.vendor) {
+ throw new Error("RFQ 정보 또는 선정된 업체 정보를 찾을 수 없습니다.");
}
- // 2. 입찰 생성 로직 (TODO: 실제 구현 필요)
- // - 입찰 공고 생성
- // - 입찰 참가자격 설정
- // - 입찰 일정 등록
- // - 평가 기준 설정
- // - 입찰 시스템 등록
+ // 2. PR 아이템 정보 조회
+ const prItems = await db
+ .select()
+ .from(rfqPrItems)
+ .where(eq(rfqPrItems.rfqsLastId, params.rfqId));
- // 3. 입찰 상태 업데이트
- await db.transaction(async (tx) => {
- // rfqLastDetails에 입찰 정보 업데이트
+ // 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: `BID-${Date.now()}`, // TODO: 실제 입찰번호로 변경
+ contractNo: biddingNumber,
updatedAt: new Date(),
updatedBy: userId,
})
@@ -237,23 +436,18 @@ export async function createBidding(params: CreateBiddingParams) {
)
);
- // RFQ 상태 업데이트
- await tx
- .update(rfqsLast)
- .set({
- status: "입찰 진행중",
- updatedAt: new Date(),
- })
- .where(eq(rfqsLast.id, params.rfqId));
+ return newBidding;
});
revalidatePath(`/rfq/${params.rfqId}`);
revalidatePath("/rfq");
+ revalidatePath("/biddings");
return {
success: true,
message: "입찰이 성공적으로 생성되었습니다.",
- biddingNumber: `BID-${Date.now()}`, // TODO: 실제 입찰번호 반환
+ biddingNumber: result.biddingNumber,
+ biddingId: result.id,
};
} catch (error) {
console.error("입찰 생성 오류:", error);
@@ -263,7 +457,6 @@ export async function createBidding(params: CreateBiddingParams) {
};
}
}
-
// ===== 계약 타입 확인 =====
export async function checkContractStatus(rfqId: number) {
try {