summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/service.ts')
-rw-r--r--lib/techsales-rfq/service.ts773
1 files changed, 148 insertions, 625 deletions
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts
index d74c54b4..c3c14aff 100644
--- a/lib/techsales-rfq/service.ts
+++ b/lib/techsales-rfq/service.ts
@@ -9,7 +9,6 @@ import {
users,
techSalesRfqComments,
techSalesRfqItems,
- projectSeries,
biddingProjects
} from "@/db/schema";
import { and, desc, eq, ilike, or, sql, inArray } from "drizzle-orm";
@@ -29,7 +28,7 @@ import { GetTechSalesRfqsSchema } from "./validations";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { sendEmail } from "../mail/sendEmail";
-import { formatDate, formatDateToQuarter } from "../utils";
+import { formatDate } from "../utils";
import { techVendors, techVendorPossibleItems } from "@/db/schema/techVendors";
// 정렬 타입 정의
@@ -90,136 +89,6 @@ async function generateRfqCodes(tx: any, count: number, year?: number): Promise<
*
* 나머지 벤더, 첨부파일 등은 생성 이후 처리
*/
-// export async function createTechSalesRfq(input: {
-// // 프로젝트 관련
-// biddingProjectId: number;
-// // 조선 아이템 관련
-// itemShipbuildingId: number;
-// // 자재 관련 (자재그룹 코드들을 CSV로)
-// materialGroupCodes: string[];
-// // 기본 정보
-// dueDate?: Date;
-// remark?: string;
-// createdBy: number;
-// }) {
-// unstable_noStore();
-// console.log('🔍 createTechSalesRfq 호출됨:', {
-// biddingProjectId: input.biddingProjectId,
-// itemShipbuildingId: input.itemShipbuildingId,
-// materialGroupCodes: input.materialGroupCodes,
-// dueDate: input.dueDate,
-// remark: input.remark,
-// createdBy: input.createdBy
-// });
-
-// try {
-// let result: typeof techSalesRfqs.$inferSelect | undefined;
-
-// // 트랜잭션으로 처리
-// await db.transaction(async (tx) => {
-// // 실제 프로젝트 정보 조회
-// const biddingProject = await tx.query.biddingProjects.findFirst({
-// where: (biddingProjects, { eq }) => eq(biddingProjects.id, input.biddingProjectId)
-// });
-
-// if (!biddingProject) {
-// throw new Error(`프로젝트 ID ${input.biddingProjectId}를 찾을 수 없습니다.`);
-// }
-
-// // 프로젝트 시리즈 정보 조회
-// const seriesInfo = await tx.query.projectSeries.findMany({
-// where: (projectSeries, { eq }) => eq(projectSeries.pspid, biddingProject.pspid)
-// });
-
-// // 프로젝트 스냅샷 생성
-// const projectSnapshot = {
-// pspid: biddingProject.pspid,
-// projNm: biddingProject.projNm || undefined,
-// sector: biddingProject.sector || undefined,
-// projMsrm: biddingProject.projMsrm ? Number(biddingProject.projMsrm) : undefined,
-// kunnr: biddingProject.kunnr || undefined,
-// kunnrNm: biddingProject.kunnrNm || undefined,
-// cls1: biddingProject.cls1 || undefined,
-// cls1Nm: biddingProject.cls1Nm || undefined,
-// ptype: biddingProject.ptype || undefined,
-// ptypeNm: biddingProject.ptypeNm || undefined,
-// pmodelCd: biddingProject.pmodelCd || undefined,
-// pmodelNm: biddingProject.pmodelNm || undefined,
-// pmodelSz: biddingProject.pmodelSz || undefined,
-// pmodelUom: biddingProject.pmodelUom || undefined,
-// txt04: biddingProject.txt04 || undefined,
-// txt30: biddingProject.txt30 || undefined,
-// estmPm: biddingProject.estmPm || undefined,
-// pspCreatedAt: biddingProject.createdAt,
-// pspUpdatedAt: biddingProject.updatedAt,
-// };
-
-// // 시리즈 스냅샷 생성
-// const seriesSnapshot = seriesInfo.map(series => ({
-// pspid: series.pspid,
-// sersNo: series.sersNo.toString(),
-// scDt: series.scDt || undefined,
-// klDt: series.klDt || undefined,
-// lcDt: series.lcDt || undefined,
-// dlDt: series.dlDt || undefined,
-// dockNo: series.dockNo || undefined,
-// dockNm: series.dockNm || undefined,
-// projNo: series.projNo || undefined,
-// post1: series.post1 || undefined,
-// }));
-
-// // RFQ 코드 생성
-// const rfqCode = await generateRfqCodes(tx, 1);
-
-// // 기본 due date 설정 (7일 후)
-// const dueDate = input.dueDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
-
-// // itemShipbuildingId 유효성 검증
-// console.log('🔍 itemShipbuildingId 검증:', input.itemShipbuildingId);
-// const existingItemShipbuilding = await tx.query.itemShipbuilding.findFirst({
-// where: (itemShipbuilding, { eq }) => eq(itemShipbuilding.id, input.itemShipbuildingId),
-// columns: { id: true, itemCode: true, itemList: true }
-// });
-
-// if (!existingItemShipbuilding) {
-// throw new Error(`itemShipbuildingId ${input.itemShipbuildingId}에 해당하는 itemShipbuilding 레코드를 찾을 수 없습니다.`);
-// }
-
-// console.log('✅ itemShipbuilding 찾음:', existingItemShipbuilding);
-
-// // 새 기술영업 RFQ 작성 (스냅샷 포함)
-// const [newRfq] = await tx.insert(techSalesRfqs).values({
-// rfqCode: rfqCode[0],
-// rfqType: "SHIP",
-// itemShipbuildingId: input.itemShipbuildingId,
-// biddingProjectId: input.biddingProjectId,
-// materialCode: input.materialGroupCodes.join(','), // 모든 materialCode를 CSV로 저장
-// dueDate,
-// remark: input.remark,
-// createdBy: input.createdBy,
-// updatedBy: input.createdBy,
-// // 스냅샷 데이터 추가
-// projectSnapshot,
-// seriesSnapshot,
-// }).returning();
-
-// result = newRfq;
-// });
-
-// // 캐시 무효화
-// revalidateTag("techSalesRfqs");
-// revalidatePath("/evcp/budgetary-tech-sales-ship");
-
-// if (!result) {
-// throw new Error(`RFQ 생성에 실패했습니다. 입력값: ${JSON.stringify(input)}`);
-// }
-
-// return { data: [result], error: null };
-// } catch (err) {
-// console.error("Error creating RFQ:", err);
-// return { data: null, error: getErrorMessage(err) };
-// }
-// }
/**
* 직접 조인을 사용하여 RFQ 데이터 조회하는 함수
@@ -545,166 +414,7 @@ export async function getTechSalesDashboardWithJoin(input: {
}
}
-/**
- * 기술영업 RFQ에 벤더 추가 (단일)
- */
-export async function addVendorToTechSalesRfq(input: {
- rfqId: number;
- vendorId: number;
- createdBy: number;
-}) {
- unstable_noStore();
- try {
- // 이미 해당 RFQ에 벤더가 추가되어 있는지 확인
- const existingQuotation = await db
- .select()
- .from(techSalesVendorQuotations)
- .where(
- and(
- eq(techSalesVendorQuotations.rfqId, input.rfqId),
- eq(techSalesVendorQuotations.vendorId, input.vendorId)
- )
- )
- .limit(1);
-
- if (existingQuotation.length > 0) {
- return {
- data: null,
- error: "이미 해당 벤더가 이 RFQ에 추가되어 있습니다."
- };
- }
-
- // 새 벤더 견적서 레코드 생성
- const [newQuotation] = await db
- .insert(techSalesVendorQuotations)
- .values({
- rfqId: input.rfqId,
- vendorId: input.vendorId,
- status: "Draft",
- totalPrice: "0",
- currency: null,
- createdBy: input.createdBy,
- updatedBy: input.createdBy,
- })
- .returning();
-
- // 캐시 무효화
- revalidateTag("techSalesRfqs");
- revalidateTag("techSalesVendorQuotations");
- revalidateTag(`techSalesRfq-${input.rfqId}`);
- revalidateTag(`vendor-${input.vendorId}-quotations`);
- revalidatePath("/evcp/budgetary-tech-sales-ship");
- return { data: newQuotation, error: null };
- } catch (err) {
- console.error("Error adding vendor to RFQ:", err);
- return { data: null, error: getErrorMessage(err) };
- }
-}
-
-/**
- * 기술영업 RFQ에 여러 벤더 추가 (다중)
- */
-export async function addVendorsToTechSalesRfq(input: {
- rfqId: number;
- vendorIds: number[];
- createdBy: number;
-}) {
- unstable_noStore();
- try {
- const results: typeof techSalesVendorQuotations.$inferSelect[] = [];
- const errors: string[] = [];
-
- // 트랜잭션으로 처리
- await db.transaction(async (tx) => {
- // 1. RFQ 상태 확인
- const rfq = await tx.query.techSalesRfqs.findFirst({
- where: eq(techSalesRfqs.id, input.rfqId),
- columns: {
- id: true,
- status: true
- }
- });
-
- if (!rfq) {
- throw new Error("RFQ를 찾을 수 없습니다");
- }
-
- // 2. 각 벤더에 대해 처리
- for (const vendorId of input.vendorIds) {
- try {
- // 이미 해당 RFQ에 벤더가 추가되어 있는지 확인
- const existingQuotation = await tx
- .select()
- .from(techSalesVendorQuotations)
- .where(
- and(
- eq(techSalesVendorQuotations.rfqId, input.rfqId),
- eq(techSalesVendorQuotations.vendorId, vendorId)
- )
- )
- .limit(1);
-
- if (existingQuotation.length > 0) {
- errors.push(`벤더 ID ${vendorId}는 이미 추가되어 있습니다.`);
- continue;
- }
-
- // 새 벤더 견적서 레코드 생성
- const [newQuotation] = await tx
- .insert(techSalesVendorQuotations)
- .values({
- rfqId: input.rfqId,
- vendorId: vendorId,
- status: "Draft",
- totalPrice: "0",
- currency: "USD",
- createdBy: input.createdBy,
- updatedBy: input.createdBy,
- })
- .returning();
-
- results.push(newQuotation);
- } catch (vendorError) {
- console.error(`Error adding vendor ${vendorId}:`, vendorError);
- errors.push(`벤더 ID ${vendorId} 추가 중 오류가 발생했습니다.`);
- }
- }
-
- // 3. RFQ 상태가 "RFQ Created"이고 성공적으로 추가된 벤더가 있는 경우 상태 업데이트
- if (rfq.status === "RFQ Created" && results.length > 0) {
- await tx.update(techSalesRfqs)
- .set({
- status: "RFQ Vendor Assignned",
- updatedBy: input.createdBy,
- updatedAt: new Date()
- })
- .where(eq(techSalesRfqs.id, input.rfqId));
- }
- });
-
- // 캐시 무효화 추가
- revalidateTag("techSalesRfqs");
- revalidateTag("techSalesVendorQuotations");
- revalidateTag(`techSalesRfq-${input.rfqId}`);
- revalidatePath("/evcp/budgetary-tech-sales-ship");
-
- // 벤더별 캐시도 무효화
- for (const vendorId of input.vendorIds) {
- revalidateTag(`vendor-${vendorId}-quotations`);
- }
-
- return {
- data: results,
- error: errors.length > 0 ? errors.join(", ") : null,
- successCount: results.length,
- errorCount: errors.length
- };
- } catch (err) {
- console.error("Error adding vendors to RFQ:", err);
- return { data: null, error: getErrorMessage(err) };
- }
-}
/**
* 기술영업 RFQ에서 벤더 제거 (Draft 상태 체크 포함)
@@ -753,11 +463,16 @@ export async function removeVendorFromTechSalesRfq(input: {
)
.returning();
- // 캐시 무효화 추가
+ // RFQ 타입 조회 및 캐시 무효화
+ const rfqForCache = await db.query.techSalesRfqs.findFirst({
+ where: eq(techSalesRfqs.id, input.rfqId),
+ columns: { rfqType: true }
+ });
+
revalidateTag("techSalesVendorQuotations");
revalidateTag(`techSalesRfq-${input.rfqId}`);
revalidateTag(`vendor-${input.vendorId}-quotations`);
- revalidatePath("/evcp/budgetary-tech-sales-ship");
+ revalidatePath(getTechSalesRevalidationPath(rfqForCache?.rfqType || "SHIP"));
return { data: deletedQuotations[0], error: null };
} catch (err) {
@@ -826,10 +541,15 @@ export async function removeVendorsFromTechSalesRfq(input: {
}
});
- // 캐시 무효화 추가
+ // RFQ 타입 조회 및 캐시 무효화
+ const rfqForCache2 = await db.query.techSalesRfqs.findFirst({
+ where: eq(techSalesRfqs.id, input.rfqId),
+ columns: { rfqType: true }
+ });
+
revalidateTag("techSalesVendorQuotations");
revalidateTag(`techSalesRfq-${input.rfqId}`);
- revalidatePath("/evcp/budgetary-tech-sales-ship");
+ revalidatePath(getTechSalesRevalidationPath(rfqForCache2?.rfqType || "SHIP"));
// 벤더별 캐시도 무효화
for (const vendorId of input.vendorIds) {
@@ -987,10 +707,10 @@ export async function sendTechSalesRfqToVendors(input: {
updatedAt: new Date(),
};
- // rfqSendDate가 null인 경우에만 최초 전송일 설정
- if (!rfq.rfqSendDate) {
- updateData.rfqSendDate = new Date();
- }
+ // rfqSendDate가 null인 경우에만 최초 전송일 설정
+ if (!rfq.rfqSendDate) {
+ updateData.rfqSendDate = new Date();
+ }
await tx.update(techSalesRfqs)
.set(updateData)
@@ -1021,26 +741,11 @@ export async function sendTechSalesRfqToVendors(input: {
// 대표 언어 결정 (첫 번째 사용자의 언어 또는 기본값)
const language = vendorUsers[0]?.language || "ko";
- // 시리즈 정보 처리 - 직접 조회
- const seriesInfo = rfq.biddingProject?.pspid ? await db.query.projectSeries.findMany({
- where: eq(projectSeries.pspid, rfq.biddingProject.pspid)
- }).then(series => series.map(s => ({
- sersNo: s.sersNo.toString(),
- klQuarter: s.klDt ? formatDateToQuarter(s.klDt) : '',
- scDt: s.scDt,
- lcDt: s.lcDt,
- dlDt: s.dlDt,
- dockNo: s.dockNo,
- dockNm: s.dockNm,
- projNo: s.projNo,
- post1: s.post1,
- }))) : [];
-
// RFQ 아이템 목록 조회
const rfqItemsResult = await getTechSalesRfqItems(rfq.id);
const rfqItems = rfqItemsResult.data || [];
- // 이메일 컨텍스트 구성
+ // 이메일 컨텍스트 구성 (시리즈 정보 제거, 프로젝트 정보 간소화)
const emailContext = {
language: language,
rfq: {
@@ -1072,27 +777,15 @@ export async function sendTechSalesRfqToVendors(input: {
email: sender.email,
},
project: {
- // 기본 정보
+ // 기본 정보만 유지
id: rfq.biddingProject?.pspid || '',
name: rfq.biddingProject?.projNm || '',
sector: rfq.biddingProject?.sector || '',
shipType: rfq.biddingProject?.ptypeNm || '',
-
- // 추가 프로젝트 정보
shipCount: rfq.biddingProject?.projMsrm || 0,
- ownerCode: rfq.biddingProject?.kunnr || '',
ownerName: rfq.biddingProject?.kunnrNm || '',
- classCode: rfq.biddingProject?.cls1 || '',
className: rfq.biddingProject?.cls1Nm || '',
- shipTypeCode: rfq.biddingProject?.ptype || '',
- shipModelCode: rfq.biddingProject?.pmodelCd || '',
- shipModelName: rfq.biddingProject?.pmodelNm || '',
- shipModelSize: rfq.biddingProject?.pmodelSz || '',
- shipModelUnit: rfq.biddingProject?.pmodelUom || '',
- estimateStatus: rfq.biddingProject?.txt30 || '',
- projectManager: rfq.biddingProject?.estmPm || '',
},
- series: seriesInfo,
details: {
currency: quotation.currency || 'USD',
},
@@ -1106,8 +799,8 @@ export async function sendTechSalesRfqToVendors(input: {
await sendEmail({
to: vendorEmailsString,
subject: isResend
- ? `[기술영업 RFQ 재전송] ${rfq.rfqCode} - ${rfqItems.length > 0 ? rfqItems.map(item => item.itemList).join(', ') : '견적 요청'} ${emailContext.versionInfo}`
- : `[기술영업 RFQ] ${rfq.rfqCode} - ${rfqItems.length > 0 ? rfqItems.map(item => item.itemList).join(', ') : '견적 요청'}`,
+ ? `[기술영업 RFQ 재전송] ${rfq.rfqCode} - ${rfqItems.length > 0 ? rfqItems.map(item => item.itemList).join(', ') : '견적 요청'} ${emailContext.versionInfo}`
+ : `[기술영업 RFQ] ${rfq.rfqCode} - ${rfqItems.length > 0 ? rfqItems.map(item => item.itemList).join(', ') : '견적 요청'}`,
template: 'tech-sales-rfq-invite-ko', // 기술영업용 템플릿
context: emailContext,
cc: sender.email, // 발신자를 CC에 추가
@@ -1120,7 +813,7 @@ export async function sendTechSalesRfqToVendors(input: {
revalidateTag("techSalesRfqs");
revalidateTag("techSalesVendorQuotations");
revalidateTag(`techSalesRfq-${input.rfqId}`);
- revalidatePath("/evcp/budgetary-tech-sales-ship");
+ revalidatePath(getTechSalesRevalidationPath(rfq?.rfqType || "SHIP"));
return {
success: true,
@@ -1633,22 +1326,6 @@ export async function acceptTechSalesVendorQuotation(quotationId: number) {
})
.where(eq(techSalesVendorQuotations.id, quotationId))
- // // 3. 같은 RFQ의 다른 견적들을 Rejected로 변경
- // await tx
- // .update(techSalesVendorQuotations)
- // .set({
- // status: "Rejected",
- // rejectionReason: "다른 벤더가 선택됨",
- // updatedAt: new Date(),
- // })
- // .where(
- // and(
- // eq(techSalesVendorQuotations.rfqId, quotation.rfqId),
- // ne(techSalesVendorQuotations.id, quotationId),
- // eq(techSalesVendorQuotations.status, "Submitted")
- // )
- // )
-
// 4. RFQ 상태를 Closed로 변경
await tx
.update(techSalesRfqs)
@@ -1667,28 +1344,6 @@ export async function acceptTechSalesVendorQuotation(quotationId: number) {
console.error("벤더 견적 선택 알림 메일 발송 실패:", error);
});
- // // 거절된 견적들에 대한 알림 메일 발송 - 트랜잭션 완료 후 별도로 처리
- // setTimeout(async () => {
- // try {
- // const rejectedQuotations = await db.query.techSalesVendorQuotations.findMany({
- // where: and(
- // eq(techSalesVendorQuotations.rfqId, result.rfqId),
- // ne(techSalesVendorQuotations.id, quotationId),
- // eq(techSalesVendorQuotations.status, "Rejected")
- // ),
- // columns: { id: true }
- // });
-
- // for (const rejectedQuotation of rejectedQuotations) {
- // sendQuotationRejectedNotification(rejectedQuotation.id).catch(error => {
- // console.error("벤더 견적 거절 알림 메일 발송 실패:", error);
- // });
- // }
- // } catch (error) {
- // console.error("거절된 견적 알림 메일 발송 중 오류:", error);
- // }
- // }, 1000); // 1초 후 실행
-
// 캐시 무효화
revalidateTag("techSalesVendorQuotations")
revalidateTag(`techSalesRfq-${result.rfqId}`)
@@ -1714,45 +1369,6 @@ export async function acceptTechSalesVendorQuotation(quotationId: number) {
}
}
-// /**
-// * 기술영업 벤더 견적 거절
-// */
-// export async function rejectTechSalesVendorQuotation(quotationId: number, rejectionReason?: string) {
-// // try {
-// // const result = await db
-// // .update(techSalesVendorQuotations)
-// // .set({
-// // status: "Rejected" as any,
-// // rejectionReason: rejectionReason || "기술영업 담당자에 의해 거절됨",
-// // updatedAt: new Date(),
-// // })
-// // .where(eq(techSalesVendorQuotations.id, quotationId))
-// // .returning()
-
-// // if (result.length === 0) {
-// // throw new Error("견적을 찾을 수 없습니다")
-// // }
-
-// // // 메일 발송 (백그라운드에서 실행)
-// // sendQuotationRejectedNotification(quotationId).catch(error => {
-// // console.error("벤더 견적 거절 알림 메일 발송 실패:", error);
-// // });
-
-// // // 캐시 무효화
-// // revalidateTag("techSalesVendorQuotations")
-// // revalidateTag(`techSalesRfq-${result[0].rfqId}`)
-// // revalidateTag(`vendor-${result[0].vendorId}-quotations`)
-
-// // return { success: true, data: result[0] }
-// // } catch (error) {
-// // console.error("벤더 견적 거절 오류:", error)
-// // return {
-// // success: false,
-// // error: error instanceof Error ? error.message : "벤더 견적 거절에 실패했습니다"
-// // }
-// // }
-// }
-
/**
* 기술영업 RFQ 첨부파일 생성 (파일 업로드)
*/
@@ -1827,10 +1443,15 @@ export async function createTechSalesRfqAttachments(params: {
}
});
- // 캐시 무효화
+ // RFQ 타입 조회하여 캐시 무효화
+ const rfqType = await db.query.techSalesRfqs.findFirst({
+ where: eq(techSalesRfqs.id, techSalesRfqId),
+ columns: { rfqType: true }
+ });
+
revalidateTag("techSalesRfqs");
revalidateTag(`techSalesRfq-${techSalesRfqId}`);
- revalidatePath("/evcp/budgetary-tech-sales-ship");
+ revalidatePath(getTechSalesRevalidationPath(rfqType?.rfqType || "SHIP"));
return { data: results, error: null };
} catch (err) {
@@ -1918,10 +1539,15 @@ export async function deleteTechSalesRfqAttachment(attachmentId: number) {
return deletedAttachment[0];
});
- // 캐시 무효화
+ // RFQ 타입 조회하여 캐시 무효화
+ const attachmentRfq = await db.query.techSalesRfqs.findFirst({
+ where: eq(techSalesRfqs.id, attachment.techSalesRfqId!),
+ columns: { rfqType: true }
+ });
+
revalidateTag("techSalesRfqs");
revalidateTag(`techSalesRfq-${attachment.techSalesRfqId}`);
- revalidatePath("/evcp/budgetary-tech-sales-ship");
+ revalidatePath(getTechSalesRevalidationPath(attachmentRfq?.rfqType || "SHIP"));
return { data: result, error: null };
} catch (err) {
@@ -2103,27 +1729,11 @@ export async function sendQuotationSubmittedNotificationToVendor(quotationId: nu
return { success: false, error: "벤더 이메일 주소가 없습니다" };
}
- // 프로젝트 시리즈 정보 조회
- const seriesData = quotation.rfq.biddingProject?.pspid
- ? await db.query.projectSeries.findMany({
- where: eq(projectSeries.pspid, quotation.rfq.biddingProject.pspid)
- })
- : [];
+ // RFQ 아이템 정보 조회
+ const rfqItemsResult = await getTechSalesRfqItems(quotation.rfq.id);
+ const rfqItems = rfqItemsResult.data || [];
- // 시리즈 정보 처리
- const seriesInfo = seriesData.map(series => ({
- sersNo: series.sersNo?.toString() || '',
- klQuarter: series.klDt ? formatDateToQuarter(series.klDt) : '',
- scDt: series.scDt,
- lcDt: series.lcDt,
- dlDt: series.dlDt,
- dockNo: series.dockNo,
- dockNm: series.dockNm,
- projNo: series.projNo,
- post1: series.post1,
- }));
-
- // 이메일 컨텍스트 구성
+ // 이메일 컨텍스트 구성 (시리즈 정보 제거, 프로젝트 정보 간소화)
const emailContext = {
language: vendorUsers[0]?.language || "ko",
quotation: {
@@ -2144,6 +1754,14 @@ export async function sendQuotationSubmittedNotificationToVendor(quotationId: nu
materialCode: quotation.rfq.materialCode,
description: quotation.rfq.remark,
},
+ items: rfqItems.map(item => ({
+ itemCode: item.itemCode,
+ itemList: item.itemList,
+ workType: item.workType,
+ shipType: item.shipType,
+ subItemName: item.subItemName,
+ itemType: item.itemType,
+ })),
vendor: {
id: quotation.vendor.id,
code: quotation.vendor.vendorCode,
@@ -2155,9 +1773,7 @@ export async function sendQuotationSubmittedNotificationToVendor(quotationId: nu
shipCount: quotation.rfq.biddingProject?.projMsrm ? Number(quotation.rfq.biddingProject.projMsrm) : 0,
ownerName: quotation.rfq.biddingProject?.kunnrNm || '',
className: quotation.rfq.biddingProject?.cls1Nm || '',
- shipModelName: quotation.rfq.biddingProject?.pmodelNm || '',
},
- series: seriesInfo,
manager: {
name: quotation.rfq.createdByUser?.name || '',
email: quotation.rfq.createdByUser?.email || '',
@@ -2225,27 +1841,11 @@ export async function sendQuotationSubmittedNotificationToManager(quotationId: n
return { success: false, error: "담당자 이메일 주소가 없습니다" };
}
- // 프로젝트 시리즈 정보 조회
- const seriesData = quotation.rfq.biddingProject?.pspid
- ? await db.query.projectSeries.findMany({
- where: eq(projectSeries.pspid, quotation.rfq.biddingProject.pspid)
- })
- : [];
-
- // 시리즈 정보 처리
- const seriesInfo = seriesData.map(series => ({
- sersNo: series.sersNo?.toString() || '',
- klQuarter: series.klDt ? formatDateToQuarter(series.klDt) : '',
- scDt: series.scDt,
- lcDt: series.lcDt,
- dlDt: series.dlDt,
- dockNo: series.dockNo,
- dockNm: series.dockNm,
- projNo: series.projNo,
- post1: series.post1,
- }));
+ // RFQ 아이템 정보 조회
+ const rfqItemsResult = await getTechSalesRfqItems(quotation.rfq.id);
+ const rfqItems = rfqItemsResult.data || [];
- // 이메일 컨텍스트 구성
+ // 이메일 컨텍스트 구성 (시리즈 정보 제거, 프로젝트 정보 간소화)
const emailContext = {
language: "ko",
quotation: {
@@ -2266,6 +1866,14 @@ export async function sendQuotationSubmittedNotificationToManager(quotationId: n
materialCode: quotation.rfq.materialCode,
description: quotation.rfq.remark,
},
+ items: rfqItems.map(item => ({
+ itemCode: item.itemCode,
+ itemList: item.itemList,
+ workType: item.workType,
+ shipType: item.shipType,
+ subItemName: item.subItemName,
+ itemType: item.itemType,
+ })),
vendor: {
id: quotation.vendor.id,
code: quotation.vendor.vendorCode,
@@ -2277,9 +1885,7 @@ export async function sendQuotationSubmittedNotificationToManager(quotationId: n
shipCount: quotation.rfq.biddingProject?.projMsrm ? Number(quotation.rfq.biddingProject.projMsrm) : 0,
ownerName: quotation.rfq.biddingProject?.kunnrNm || '',
className: quotation.rfq.biddingProject?.cls1Nm || '',
- shipModelName: quotation.rfq.biddingProject?.pmodelNm || '',
},
- series: seriesInfo,
manager: {
name: manager.name || '',
email: manager.email,
@@ -2362,27 +1968,11 @@ export async function sendQuotationAcceptedNotification(quotationId: number) {
return { success: false, error: "벤더 이메일 주소가 없습니다" };
}
- // 프로젝트 시리즈 정보 조회
- const seriesData = quotation.rfq.biddingProject?.pspid
- ? await db.query.projectSeries.findMany({
- where: eq(projectSeries.pspid, quotation.rfq.biddingProject.pspid)
- })
- : [];
-
- // 시리즈 정보 처리
- const seriesInfo = seriesData.map(series => ({
- sersNo: series.sersNo?.toString() || '',
- klQuarter: series.klDt ? formatDateToQuarter(series.klDt) : '',
- scDt: series.scDt,
- lcDt: series.lcDt,
- dlDt: series.dlDt,
- dockNo: series.dockNo,
- dockNm: series.dockNm,
- projNo: series.projNo,
- post1: series.post1,
- }));
+ // RFQ 아이템 정보 조회
+ const rfqItemsResult = await getTechSalesRfqItems(quotation.rfq.id);
+ const rfqItems = rfqItemsResult.data || [];
- // 이메일 컨텍스트 구성
+ // 이메일 컨텍스트 구성 (시리즈 정보 제거, 프로젝트 정보 간소화)
const emailContext = {
language: vendorUsers[0]?.language || "ko",
quotation: {
@@ -2403,6 +1993,14 @@ export async function sendQuotationAcceptedNotification(quotationId: number) {
materialCode: quotation.rfq.materialCode,
description: quotation.rfq.remark,
},
+ items: rfqItems.map(item => ({
+ itemCode: item.itemCode,
+ itemList: item.itemList,
+ workType: item.workType,
+ shipType: item.shipType,
+ subItemName: item.subItemName,
+ itemType: item.itemType,
+ })),
vendor: {
id: quotation.vendor.id,
code: quotation.vendor.vendorCode,
@@ -2414,9 +2012,7 @@ export async function sendQuotationAcceptedNotification(quotationId: number) {
shipCount: quotation.rfq.biddingProject?.projMsrm ? Number(quotation.rfq.biddingProject.projMsrm) : 0,
ownerName: quotation.rfq.biddingProject?.kunnrNm || '',
className: quotation.rfq.biddingProject?.cls1Nm || '',
- shipModelName: quotation.rfq.biddingProject?.pmodelNm || '',
},
- series: seriesInfo,
manager: {
name: quotation.rfq.createdByUser?.name || '',
email: quotation.rfq.createdByUser?.email || '',
@@ -2442,143 +2038,6 @@ export async function sendQuotationAcceptedNotification(quotationId: number) {
}
}
-/**
- * 벤더 견적 거절 알림 메일 발송
- */
-export async function sendQuotationRejectedNotification(quotationId: number) {
- try {
- // 견적서 정보 조회
- const quotation = await db.query.techSalesVendorQuotations.findFirst({
- where: eq(techSalesVendorQuotations.id, quotationId),
- with: {
- rfq: {
- with: {
- biddingProject: true,
- createdByUser: {
- columns: {
- id: true,
- name: true,
- email: true,
- }
- }
- }
- },
- vendor: {
- columns: {
- id: true,
- vendorName: true,
- vendorCode: true,
- }
- }
- }
- });
-
- if (!quotation || !quotation.rfq || !quotation.vendor) {
- console.error("견적서 또는 관련 정보를 찾을 수 없습니다");
- return { success: false, error: "견적서 정보를 찾을 수 없습니다" };
- }
-
- // 벤더 사용자들 조회
- const vendorUsers = await db.query.users.findMany({
- where: eq(users.companyId, quotation.vendor.id),
- columns: {
- id: true,
- email: true,
- name: true,
- language: true
- }
- });
-
- const vendorEmails = vendorUsers
- .filter(user => user.email)
- .map(user => user.email)
- .join(", ");
-
- if (!vendorEmails) {
- console.warn(`벤더 ID ${quotation.vendor.id}에 등록된 이메일 주소가 없습니다`);
- return { success: false, error: "벤더 이메일 주소가 없습니다" };
- }
-
- // 프로젝트 시리즈 정보 조회
- const seriesData = quotation.rfq.biddingProject?.pspid
- ? await db.query.projectSeries.findMany({
- where: eq(projectSeries.pspid, quotation.rfq.biddingProject.pspid)
- })
- : [];
-
- // 시리즈 정보 처리
- const seriesInfo = seriesData.map(series => ({
- sersNo: series.sersNo?.toString() || '',
- klQuarter: series.klDt ? formatDateToQuarter(series.klDt) : '',
- scDt: series.scDt,
- lcDt: series.lcDt,
- dlDt: series.dlDt,
- dockNo: series.dockNo,
- dockNm: series.dockNm,
- projNo: series.projNo,
- post1: series.post1,
- }));
-
- // 이메일 컨텍스트 구성
- const emailContext = {
- language: vendorUsers[0]?.language || "ko",
- quotation: {
- id: quotation.id,
- currency: quotation.currency,
- totalPrice: quotation.totalPrice,
- validUntil: quotation.validUntil,
- rejectionReason: quotation.rejectionReason,
- remark: quotation.remark,
- },
- rfq: {
- id: quotation.rfq.id,
- code: quotation.rfq.rfqCode,
- title: quotation.rfq.description || '',
- projectCode: quotation.rfq.biddingProject?.pspid || '',
- projectName: quotation.rfq.biddingProject?.projNm || '',
- dueDate: quotation.rfq.dueDate,
- materialCode: quotation.rfq.materialCode,
- description: quotation.rfq.remark,
- },
- vendor: {
- id: quotation.vendor.id,
- code: quotation.vendor.vendorCode,
- name: quotation.vendor.vendorName,
- },
- project: {
- name: quotation.rfq.biddingProject?.projNm || '',
- sector: quotation.rfq.biddingProject?.sector || '',
- shipCount: quotation.rfq.biddingProject?.projMsrm ? Number(quotation.rfq.biddingProject.projMsrm) : 0,
- ownerName: quotation.rfq.biddingProject?.kunnrNm || '',
- className: quotation.rfq.biddingProject?.cls1Nm || '',
- shipModelName: quotation.rfq.biddingProject?.pmodelNm || '',
- },
- series: seriesInfo,
- manager: {
- name: quotation.rfq.createdByUser?.name || '',
- email: quotation.rfq.createdByUser?.email || '',
- },
- systemUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://60.101.108.100/ko/partners',
- companyName: 'Samsung Heavy Industries',
- year: new Date().getFullYear(),
- };
-
- // 이메일 발송
- await sendEmail({
- to: vendorEmails,
- subject: `[견적 거절 알림] ${quotation.rfq.rfqCode} - 견적 결과 안내`,
- template: 'tech-sales-quotation-rejected-ko',
- context: emailContext,
- });
-
- console.log(`벤더 견적 거절 알림 메일 발송 완료: ${vendorEmails}`);
- return { success: true };
- } catch (error) {
- console.error("벤더 견적 거절 알림 메일 발송 오류:", error);
- return { success: false, error: "메일 발송 중 오류가 발생했습니다" };
- }
-}
-
// ==================== Vendor Communication 관련 ====================
export interface TechSalesAttachment {
@@ -3288,6 +2747,22 @@ export async function addTechVendorToTechSalesRfq(input: {
}
/**
+ * RFQ 타입에 따른 캐시 무효화 경로 반환
+ */
+function getTechSalesRevalidationPath(rfqType: "SHIP" | "TOP" | "HULL"): string {
+ switch (rfqType) {
+ case "SHIP":
+ return "/evcp/budgetary-tech-sales-ship";
+ case "TOP":
+ return "/evcp/budgetary-tech-sales-top";
+ case "HULL":
+ return "/evcp/budgetary-tech-sales-hull";
+ default:
+ return "/evcp/budgetary-tech-sales-ship";
+ }
+}
+
+/**
* 기술영업 RFQ에 여러 벤더 추가 (techVendors 기반)
*/
export async function addTechVendorsToTechSalesRfq(input: {
@@ -3300,17 +2775,38 @@ export async function addTechVendorsToTechSalesRfq(input: {
try {
return await db.transaction(async (tx) => {
const results = [];
+ const errors: string[] = [];
+
+ // 1. RFQ 상태 및 타입 확인
+ const rfq = await tx.query.techSalesRfqs.findFirst({
+ where: eq(techSalesRfqs.id, input.rfqId),
+ columns: {
+ id: true,
+ status: true,
+ rfqType: true
+ }
+ });
+
+ if (!rfq) {
+ throw new Error("RFQ를 찾을 수 없습니다");
+ }
+ // 2. 각 벤더에 대해 처리
for (const vendorId of input.vendorIds) {
- // 벤더가 이미 추가되어 있는지 확인
- const existingQuotation = await tx.query.techSalesVendorQuotations.findFirst({
- where: and(
- eq(techSalesVendorQuotations.rfqId, input.rfqId),
- eq(techSalesVendorQuotations.vendorId, vendorId)
- )
- });
+ try {
+ // 벤더가 이미 추가되어 있는지 확인
+ const existingQuotation = await tx.query.techSalesVendorQuotations.findFirst({
+ where: and(
+ eq(techSalesVendorQuotations.rfqId, input.rfqId),
+ eq(techSalesVendorQuotations.vendorId, vendorId)
+ )
+ });
+
+ if (existingQuotation) {
+ errors.push(`벤더 ID ${vendorId}는 이미 추가되어 있습니다.`);
+ continue;
+ }
- if (!existingQuotation) {
// 새로운 견적서 레코드 생성
const [quotation] = await tx
.insert(techSalesVendorQuotations)
@@ -3324,13 +2820,40 @@ export async function addTechVendorsToTechSalesRfq(input: {
.returning({ id: techSalesVendorQuotations.id });
results.push(quotation);
+ } catch (vendorError) {
+ console.error(`Error adding vendor ${vendorId}:`, vendorError);
+ errors.push(`벤더 ID ${vendorId} 추가 중 오류가 발생했습니다.`);
}
}
- // 캐시 무효화
+ // 3. RFQ 상태가 "RFQ Created"이고 성공적으로 추가된 벤더가 있는 경우 상태 업데이트
+ if (rfq.status === "RFQ Created" && results.length > 0) {
+ await tx.update(techSalesRfqs)
+ .set({
+ status: "RFQ Vendor Assignned",
+ updatedBy: input.createdBy,
+ updatedAt: new Date()
+ })
+ .where(eq(techSalesRfqs.id, input.rfqId));
+ }
+
+ // 캐시 무효화 (RFQ 타입에 따른 동적 경로)
revalidateTag("techSalesRfqs");
+ revalidateTag("techSalesVendorQuotations");
+ revalidateTag(`techSalesRfq-${input.rfqId}`);
+ revalidatePath(getTechSalesRevalidationPath(rfq.rfqType || "SHIP"));
- return { data: results, error: null };
+ // 벤더별 캐시도 무효화
+ for (const vendorId of input.vendorIds) {
+ revalidateTag(`vendor-${vendorId}-quotations`);
+ }
+
+ return {
+ data: results,
+ error: errors.length > 0 ? errors.join(", ") : null,
+ successCount: results.length,
+ errorCount: errors.length
+ };
});
} catch (err) {
console.error("Error adding tech vendors to RFQ:", err);