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.ts365
1 files changed, 211 insertions, 154 deletions
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts
index 7be91092..f7a30b3b 100644
--- a/lib/techsales-rfq/service.ts
+++ b/lib/techsales-rfq/service.ts
@@ -582,6 +582,12 @@ export async function addVendorToTechSalesRfq(input: {
})
.returning();
+ // 캐시 무효화
+ 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);
@@ -675,6 +681,11 @@ export async function addVendorsToTechSalesRfq(input: {
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,
@@ -737,6 +748,7 @@ export async function removeVendorFromTechSalesRfq(input: {
// 캐시 무효화 추가
revalidateTag("techSalesVendorQuotations");
revalidateTag(`techSalesRfq-${input.rfqId}`);
+ revalidateTag(`vendor-${input.vendorId}-quotations`);
revalidatePath("/evcp/budgetary-tech-sales-ship");
return { data: deletedQuotations[0], error: null };
@@ -811,6 +823,11 @@ export async function removeVendorsFromTechSalesRfq(input: {
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,
@@ -969,26 +986,30 @@ export async function sendTechSalesRfqToVendors(input: {
// 트랜잭션 시작
await db.transaction(async (tx) => {
- // 1. RFQ 상태 업데이트 (첫 발송인 경우에만)
- if (!isResend) {
- await tx.update(techSalesRfqs)
- .set({
- status: "RFQ Sent",
- rfqSendDate: new Date(),
- sentBy: Number(session.user.id),
- updatedBy: Number(session.user.id),
- updatedAt: new Date(),
- })
- .where(eq(techSalesRfqs.id, input.rfqId));
+ // 1. RFQ 상태 업데이트 (최초 발송인 경우 rfqSendDate 설정)
+ const updateData: Partial<typeof techSalesRfqs.$inferInsert> = {
+ status: "RFQ Sent",
+ sentBy: Number(session.user.id),
+ updatedBy: Number(session.user.id),
+ updatedAt: new Date(),
+ };
+
+ // rfqSendDate가 null인 경우에만 최초 전송일 설정
+ if (!rfq.rfqSendDate) {
+ updateData.rfqSendDate = new Date();
}
+ await tx.update(techSalesRfqs)
+ .set(updateData)
+ .where(eq(techSalesRfqs.id, input.rfqId));
+
// 2. 각 벤더에 대해 이메일 발송 처리
for (const quotation of vendorQuotations) {
if (!quotation.vendorId || !quotation.vendor) continue;
// 벤더에 속한 모든 사용자 조회
const vendorUsers = await db.query.users.findMany({
- where: eq(users.companyId, quotation.vendorId),
+ where: eq(users.companyId, quotation.vendor.id),
columns: {
id: true,
email: true,
@@ -1156,11 +1177,12 @@ export async function updateTechSalesVendorQuotation(data: {
updatedBy: number
}) {
try {
- // 현재 견적서 상태 확인
+ // 현재 견적서 상태 및 벤더 ID 확인
const currentQuotation = await db.query.techSalesVendorQuotations.findFirst({
where: eq(techSalesVendorQuotations.id, data.id),
columns: {
status: true,
+ vendorId: true,
}
});
@@ -1213,6 +1235,7 @@ export async function submitTechSalesVendorQuotation(data: {
where: eq(techSalesVendorQuotations.id, data.id),
columns: {
status: true,
+ vendorId: true,
}
});
@@ -1254,6 +1277,7 @@ export async function submitTechSalesVendorQuotation(data: {
// 캐시 무효화
revalidateTag("techSalesVendorQuotations")
+ revalidateTag(`vendor-${currentQuotation.vendorId}-quotations`)
revalidatePath(`/partners/techsales/rfq-ship/${data.id}`)
return { data: result[0], error: null }
@@ -1300,158 +1324,180 @@ export async function getVendorQuotations(input: {
from?: string;
to?: string;
}, vendorId: string) {
- unstable_noStore();
- try {
- const { page, perPage, sort, filters = [], search = "", from = "", to = "" } = input;
- const offset = (page - 1) * perPage;
- const limit = perPage;
-
- // 기본 조건: 해당 벤더의 견적서만 조회
- const baseConditions = [eq(techSalesVendorQuotations.vendorId, parseInt(vendorId))];
-
- // 검색 조건 추가
- if (search) {
- const s = `%${search}%`;
- const searchCondition = or(
- ilike(techSalesVendorQuotations.currency, s),
- ilike(techSalesVendorQuotations.status, s)
- );
- if (searchCondition) {
- baseConditions.push(searchCondition);
- }
- }
+ return unstable_cache(
+ async () => {
+ try {
+ const { page, perPage, sort, filters = [], search = "", from = "", to = "" } = input;
+ const offset = (page - 1) * perPage;
+ const limit = perPage;
- // 날짜 범위 필터
- if (from) {
- baseConditions.push(sql`${techSalesVendorQuotations.createdAt} >= ${from}`);
- }
- if (to) {
- baseConditions.push(sql`${techSalesVendorQuotations.createdAt} <= ${to}`);
- }
+ // 기본 조건: 해당 벤더의 견적서만 조회
+ const baseConditions = [eq(techSalesVendorQuotations.vendorId, parseInt(vendorId))];
- // 고급 필터 처리
- if (filters.length > 0) {
- const filterWhere = filterColumns({
- table: techSalesVendorQuotations,
- filters: filters as Filter<typeof techSalesVendorQuotations>[],
- joinOperator: input.joinOperator || "and",
- });
- if (filterWhere) {
- baseConditions.push(filterWhere);
- }
- }
+ // 검색 조건 추가
+ if (search) {
+ const s = `%${search}%`;
+ const searchCondition = or(
+ ilike(techSalesVendorQuotations.currency, s),
+ ilike(techSalesVendorQuotations.status, s)
+ );
+ if (searchCondition) {
+ baseConditions.push(searchCondition);
+ }
+ }
- // 최종 WHERE 조건
- const finalWhere = baseConditions.length > 0
- ? and(...baseConditions)
- : undefined;
+ // 날짜 범위 필터
+ if (from) {
+ baseConditions.push(sql`${techSalesVendorQuotations.createdAt} >= ${from}`);
+ }
+ if (to) {
+ baseConditions.push(sql`${techSalesVendorQuotations.createdAt} <= ${to}`);
+ }
- // 정렬 기준 설정
- let orderBy: OrderByType[] = [desc(techSalesVendorQuotations.updatedAt)];
-
- if (sort?.length) {
- orderBy = sort.map(item => {
- switch (item.id) {
- case 'id':
- return item.desc ? desc(techSalesVendorQuotations.id) : techSalesVendorQuotations.id;
- case 'status':
- return item.desc ? desc(techSalesVendorQuotations.status) : techSalesVendorQuotations.status;
- case 'currency':
- return item.desc ? desc(techSalesVendorQuotations.currency) : techSalesVendorQuotations.currency;
- case 'totalPrice':
- return item.desc ? desc(techSalesVendorQuotations.totalPrice) : techSalesVendorQuotations.totalPrice;
- case 'validUntil':
- return item.desc ? desc(techSalesVendorQuotations.validUntil) : techSalesVendorQuotations.validUntil;
- case 'submittedAt':
- return item.desc ? desc(techSalesVendorQuotations.submittedAt) : techSalesVendorQuotations.submittedAt;
- case 'createdAt':
- return item.desc ? desc(techSalesVendorQuotations.createdAt) : techSalesVendorQuotations.createdAt;
- case 'updatedAt':
- return item.desc ? desc(techSalesVendorQuotations.updatedAt) : techSalesVendorQuotations.updatedAt;
- default:
- return item.desc ? desc(techSalesVendorQuotations.updatedAt) : techSalesVendorQuotations.updatedAt;
+ // 고급 필터 처리
+ if (filters.length > 0) {
+ const filterWhere = filterColumns({
+ table: techSalesVendorQuotations,
+ filters: filters as Filter<typeof techSalesVendorQuotations>[],
+ joinOperator: input.joinOperator || "and",
+ });
+ if (filterWhere) {
+ baseConditions.push(filterWhere);
+ }
}
- });
- }
- // 조인을 포함한 데이터 조회
- const data = await db
- .select({
- id: techSalesVendorQuotations.id,
- rfqId: techSalesVendorQuotations.rfqId,
- vendorId: techSalesVendorQuotations.vendorId,
- status: techSalesVendorQuotations.status,
- currency: techSalesVendorQuotations.currency,
- totalPrice: techSalesVendorQuotations.totalPrice,
- validUntil: techSalesVendorQuotations.validUntil,
- submittedAt: techSalesVendorQuotations.submittedAt,
- remark: techSalesVendorQuotations.remark,
- createdAt: techSalesVendorQuotations.createdAt,
- updatedAt: techSalesVendorQuotations.updatedAt,
- createdBy: techSalesVendorQuotations.createdBy,
- updatedBy: techSalesVendorQuotations.updatedBy,
- // RFQ 정보
- rfqCode: techSalesRfqs.rfqCode,
- materialCode: techSalesRfqs.materialCode,
- dueDate: techSalesRfqs.dueDate,
- rfqStatus: techSalesRfqs.status,
- // 아이템 정보
- itemName: items.itemName,
- // 프로젝트 정보 (JSON에서 추출)
- projNm: sql<string>`${techSalesRfqs.projectSnapshot}->>'projNm'`,
- // 첨부파일 개수
- attachmentCount: sql<number>`(
- SELECT COUNT(*)
- FROM tech_sales_attachments
- WHERE tech_sales_attachments.tech_sales_rfq_id = ${techSalesRfqs.id}
- )`,
- })
- .from(techSalesVendorQuotations)
- .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id))
- .leftJoin(items, eq(techSalesRfqs.itemId, items.id))
- .where(finalWhere)
- .orderBy(...orderBy)
- .limit(limit)
- .offset(offset);
-
- // 총 개수 조회
- const totalResult = await db
- .select({ count: sql<number>`count(*)` })
- .from(techSalesVendorQuotations)
- .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id))
- .leftJoin(items, eq(techSalesRfqs.itemId, items.id))
- .where(finalWhere);
+ // 최종 WHERE 조건
+ const finalWhere = baseConditions.length > 0
+ ? and(...baseConditions)
+ : undefined;
- const total = totalResult[0]?.count || 0;
- const pageCount = Math.ceil(total / perPage);
+ // 정렬 기준 설정
+ let orderBy: OrderByType[] = [desc(techSalesVendorQuotations.updatedAt)];
+
+ if (sort?.length) {
+ orderBy = sort.map(item => {
+ switch (item.id) {
+ case 'id':
+ return item.desc ? desc(techSalesVendorQuotations.id) : techSalesVendorQuotations.id;
+ case 'status':
+ return item.desc ? desc(techSalesVendorQuotations.status) : techSalesVendorQuotations.status;
+ case 'currency':
+ return item.desc ? desc(techSalesVendorQuotations.currency) : techSalesVendorQuotations.currency;
+ case 'totalPrice':
+ return item.desc ? desc(techSalesVendorQuotations.totalPrice) : techSalesVendorQuotations.totalPrice;
+ case 'validUntil':
+ return item.desc ? desc(techSalesVendorQuotations.validUntil) : techSalesVendorQuotations.validUntil;
+ case 'submittedAt':
+ return item.desc ? desc(techSalesVendorQuotations.submittedAt) : techSalesVendorQuotations.submittedAt;
+ case 'createdAt':
+ return item.desc ? desc(techSalesVendorQuotations.createdAt) : techSalesVendorQuotations.createdAt;
+ case 'updatedAt':
+ return item.desc ? desc(techSalesVendorQuotations.updatedAt) : techSalesVendorQuotations.updatedAt;
+ default:
+ return item.desc ? desc(techSalesVendorQuotations.updatedAt) : techSalesVendorQuotations.updatedAt;
+ }
+ });
+ }
- return { data, pageCount, total };
- } catch (err) {
- console.error("Error fetching vendor quotations:", err);
- return { data: [], pageCount: 0, total: 0 };
- }
+ // 조인을 포함한 데이터 조회
+ const data = await db
+ .select({
+ id: techSalesVendorQuotations.id,
+ rfqId: techSalesVendorQuotations.rfqId,
+ vendorId: techSalesVendorQuotations.vendorId,
+ status: techSalesVendorQuotations.status,
+ currency: techSalesVendorQuotations.currency,
+ totalPrice: techSalesVendorQuotations.totalPrice,
+ validUntil: techSalesVendorQuotations.validUntil,
+ submittedAt: techSalesVendorQuotations.submittedAt,
+ remark: techSalesVendorQuotations.remark,
+ createdAt: techSalesVendorQuotations.createdAt,
+ updatedAt: techSalesVendorQuotations.updatedAt,
+ createdBy: techSalesVendorQuotations.createdBy,
+ updatedBy: techSalesVendorQuotations.updatedBy,
+ // RFQ 정보
+ rfqCode: techSalesRfqs.rfqCode,
+ materialCode: techSalesRfqs.materialCode,
+ dueDate: techSalesRfqs.dueDate,
+ rfqStatus: techSalesRfqs.status,
+ // 아이템 정보
+ itemName: items.itemName,
+ // 프로젝트 정보 (JSON에서 추출)
+ projNm: sql<string>`${techSalesRfqs.projectSnapshot}->>'projNm'`,
+ // 첨부파일 개수
+ attachmentCount: sql<number>`(
+ SELECT COUNT(*)
+ FROM tech_sales_attachments
+ WHERE tech_sales_attachments.tech_sales_rfq_id = ${techSalesRfqs.id}
+ )`,
+ })
+ .from(techSalesVendorQuotations)
+ .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id))
+ .leftJoin(items, eq(techSalesRfqs.itemId, items.id))
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .limit(limit)
+ .offset(offset);
+
+ // 총 개수 조회
+ const totalResult = await db
+ .select({ count: sql<number>`count(*)` })
+ .from(techSalesVendorQuotations)
+ .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id))
+ .leftJoin(items, eq(techSalesRfqs.itemId, items.id))
+ .where(finalWhere);
+
+ const total = totalResult[0]?.count || 0;
+ const pageCount = Math.ceil(total / perPage);
+
+ return { data, pageCount, total };
+ } catch (err) {
+ console.error("Error fetching vendor quotations:", err);
+ return { data: [], pageCount: 0, total: 0 };
+ }
+ },
+ [JSON.stringify(input), vendorId], // 캐싱 키
+ {
+ revalidate: 60, // 1분간 캐시
+ tags: [
+ "techSalesVendorQuotations",
+ `vendor-${vendorId}-quotations`
+ ],
+ }
+ )();
}
/**
* 벤더용 기술영업 견적서 상태별 개수 조회
*/
export async function getQuotationStatusCounts(vendorId: string) {
- unstable_noStore();
- try {
- const result = await db
- .select({
- status: techSalesVendorQuotations.status,
- count: sql<number>`count(*)`,
- })
- .from(techSalesVendorQuotations)
- .where(eq(techSalesVendorQuotations.vendorId, parseInt(vendorId)))
- .groupBy(techSalesVendorQuotations.status);
+ return unstable_cache(
+ async () => {
+ try {
+ const result = await db
+ .select({
+ status: techSalesVendorQuotations.status,
+ count: sql<number>`count(*)`,
+ })
+ .from(techSalesVendorQuotations)
+ .where(eq(techSalesVendorQuotations.vendorId, parseInt(vendorId)))
+ .groupBy(techSalesVendorQuotations.status);
- return { data: result, error: null };
- } catch (err) {
- console.error("Error fetching quotation status counts:", err);
- return { data: null, error: getErrorMessage(err) };
- }
+ return { data: result, error: null };
+ } catch (err) {
+ console.error("Error fetching quotation status counts:", err);
+ return { data: null, error: getErrorMessage(err) };
+ }
+ },
+ [vendorId], // 캐싱 키
+ {
+ revalidate: 60, // 1분간 캐시
+ tags: [
+ "techSalesVendorQuotations",
+ `vendor-${vendorId}-quotations`
+ ],
+ }
+ )();
}
/**
@@ -1544,6 +1590,16 @@ export async function acceptTechSalesVendorQuotation(quotationId: number) {
revalidateTag(`techSalesRfq-${result.rfqId}`)
revalidateTag("techSalesRfqs")
+ // 해당 RFQ의 모든 벤더 캐시 무효화 (선택된 벤더와 거절된 벤더들)
+ const allVendorsInRfq = await db.query.techSalesVendorQuotations.findMany({
+ where: eq(techSalesVendorQuotations.rfqId, result.rfqId),
+ columns: { vendorId: true }
+ });
+
+ for (const vendorQuotation of allVendorsInRfq) {
+ revalidateTag(`vendor-${vendorQuotation.vendorId}-quotations`);
+ }
+
return { success: true, data: result }
} catch (error) {
console.error("벤더 견적 승인 오류:", error)
@@ -1581,6 +1637,7 @@ export async function rejectTechSalesVendorQuotation(quotationId: number, reject
// 캐시 무효화
revalidateTag("techSalesVendorQuotations")
revalidateTag(`techSalesRfq-${result[0].rfqId}`)
+ revalidateTag(`vendor-${result[0].vendorId}-quotations`)
return { success: true, data: result[0] }
} catch (error) {
@@ -1944,7 +2001,7 @@ export async function sendQuotationSubmittedNotificationToVendor(quotationId: nu
}
// 프로젝트 정보 준비
- const projectInfo = (quotation.rfq.projectSnapshot as Record<string, any>) || {};
+ const projectInfo = (quotation.rfq.projectSnapshot as Record<string, unknown>) || {};
// 시리즈 정보 처리
const seriesInfo = quotation.rfq.seriesSnapshot ? quotation.rfq.seriesSnapshot.map((series: SeriesSnapshot) => ({
@@ -2063,7 +2120,7 @@ export async function sendQuotationSubmittedNotificationToManager(quotationId: n
}
// 프로젝트 정보 준비
- const projectInfo = (quotation.rfq.projectSnapshot as Record<string, any>) || {};
+ const projectInfo = (quotation.rfq.projectSnapshot as Record<string, unknown>) || {};
// 시리즈 정보 처리
const seriesInfo = quotation.rfq.seriesSnapshot ? quotation.rfq.seriesSnapshot.map((series: SeriesSnapshot) => ({
@@ -2197,7 +2254,7 @@ export async function sendQuotationAcceptedNotification(quotationId: number) {
}
// 프로젝트 정보 준비
- const projectInfo = (quotation.rfq.projectSnapshot as Record<string, any>) || {};
+ const projectInfo = (quotation.rfq.projectSnapshot as Record<string, unknown>) || {};
// 시리즈 정보 처리
const seriesInfo = quotation.rfq.seriesSnapshot ? quotation.rfq.seriesSnapshot.map((series: SeriesSnapshot) => ({
@@ -2331,7 +2388,7 @@ export async function sendQuotationRejectedNotification(quotationId: number) {
}
// 프로젝트 정보 준비
- const projectInfo = (quotation.rfq.projectSnapshot as Record<string, any>) || {};
+ const projectInfo = (quotation.rfq.projectSnapshot as Record<string, unknown>) || {};
// 시리즈 정보 처리
const seriesInfo = quotation.rfq.seriesSnapshot ? quotation.rfq.seriesSnapshot.map((series: SeriesSnapshot) => ({