diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-17 09:02:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-17 09:02:32 +0000 |
| commit | 7a1524ba54f43d0f2a19e4bca2c6a2e0b01c5ef1 (patch) | |
| tree | daa214d404c7fc78b32419a028724e5671a6c7a4 /lib/techsales-rfq | |
| parent | fa6a6093014c5d60188edfc9c4552e81c4b97bd1 (diff) | |
(대표님) 20250617 18시 작업사항
Diffstat (limited to 'lib/techsales-rfq')
| -rw-r--r-- | lib/techsales-rfq/service.ts | 773 | ||||
| -rw-r--r-- | lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx | 2 |
2 files changed, 149 insertions, 626 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); diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx index 7d5c359e..3e50a516 100644 --- a/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx +++ b/lib/techsales-rfq/table/detail-table/rfq-detail-column.tsx @@ -139,7 +139,7 @@ export function getRfqDetailColumns({ variant="link" className="p-0 h-auto font-normal text-left justify-start hover:underline" onClick={() => { - window.open(`/ko/evcp/vendors/${vendorId}/info`, '_blank'); + window.open(`/ko/evcp/tech-vendors/${vendorId}/info`, '_blank'); }} > {vendorName} |
