summaryrefslogtreecommitdiff
path: root/lib/rfqs-tech
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-02 08:37:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-02 08:37:21 +0000
commit15969dfedffc4e215c81d507164bc2bb383974e5 (patch)
treecf854737fb7575f3fe41629400a623363ade3663 /lib/rfqs-tech
parent612abffa6d570ca919cddad75899259b33e28eba (diff)
(최겸) 기술영업 해양 rfq 캐시 삭제 및 add vendor 필터 추가
Diffstat (limited to 'lib/rfqs-tech')
-rw-r--r--lib/rfqs-tech/service.ts1780
-rw-r--r--lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table-column.tsx2
-rw-r--r--lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table.tsx68
3 files changed, 835 insertions, 1015 deletions
diff --git a/lib/rfqs-tech/service.ts b/lib/rfqs-tech/service.ts
index 26012491..fac18a43 100644
--- a/lib/rfqs-tech/service.ts
+++ b/lib/rfqs-tech/service.ts
@@ -1,11 +1,10 @@
// src/lib/tasks/service.ts
"use server"; // Next.js 서버 액션에서 직접 import하려면 (선택)
-import { revalidatePath, revalidateTag, unstable_noStore } from "next/cache";
+import { revalidatePath, revalidateTag } from "next/cache";
import db from "@/db/db";
import { filterColumns } from "@/lib/filter-columns";
-import { unstable_cache } from "@/lib/unstable-cache";
import { getErrorMessage } from "@/lib/handle-error";
import { GetRfqsSchema, CreateRfqSchema, UpdateRfqSchema, CreateRfqItemSchema, GetMatchedVendorsSchema, UpdateRfqVendorSchema, GetTBESchema, GetCBESchema, createCbeEvaluationSchema } from "./validations";
@@ -41,113 +40,94 @@ interface InviteVendorsInput {
/**
* 복잡한 조건으로 Rfq 목록을 조회 (+ pagination) 하고,
* 총 개수에 따라 pageCount를 계산해서 리턴.
- * Next.js의 unstable_cache를 사용해 일정 시간 캐시.
*/
export async function getRfqs(input: GetRfqsSchema) {
- return unstable_cache(
- async () => {
- try {
- const offset = (input.page - 1) * input.perPage;
- // const advancedTable = input.flags.includes("advancedTable");
- const advancedTable = true;
-
- // advancedTable 모드면 filterColumns()로 where 절 구성
- const advancedWhere = filterColumns({
- table: rfqsView,
- filters: input.filters,
- joinOperator: input.joinOperator,
- });
+ try {
+ const offset = (input.page - 1) * input.perPage;
+ // const advancedTable = input.flags.includes("advancedTable");
+ const advancedTable = true;
+
+ // advancedTable 모드면 filterColumns()로 where 절 구성
+ const advancedWhere = filterColumns({
+ table: rfqsView,
+ filters: input.filters,
+ joinOperator: input.joinOperator,
+ });
- let globalWhere
- if (input.search) {
- const s = `%${input.search}%`
- globalWhere = or(ilike(rfqsView.rfqCode, s), ilike(rfqsView.projectCode, s)
- , ilike(rfqsView.projectName, s), ilike(rfqsView.dueDate, s), ilike(rfqsView.status, s)
- )
- // 필요시 여러 칼럼 OR조건 (status, priority, etc)
- }
+ let globalWhere
+ if (input.search) {
+ const s = `%${input.search}%`
+ globalWhere = or(ilike(rfqsView.rfqCode, s), ilike(rfqsView.projectCode, s)
+ , ilike(rfqsView.projectName, s), ilike(rfqsView.dueDate, s), ilike(rfqsView.status, s)
+ )
+ // 필요시 여러 칼럼 OR조건 (status, priority, etc)
+ }
- const whereConditions = [];
- if (advancedWhere) whereConditions.push(advancedWhere);
- if (globalWhere) whereConditions.push(globalWhere);
+ const whereConditions = [];
+ if (advancedWhere) whereConditions.push(advancedWhere);
+ if (globalWhere) whereConditions.push(globalWhere);
- // 조건이 있을 때만 and() 사용
- const finalWhere = whereConditions.length > 0
- ? and(...whereConditions)
- : undefined;
+ // 조건이 있을 때만 and() 사용
+ const finalWhere = whereConditions.length > 0
+ ? and(...whereConditions)
+ : undefined;
- const orderBy =
- input.sort.length > 0
- ? input.sort.map((item) =>
- item.desc ? desc(rfqsView[item.id]) : asc(rfqsView[item.id])
- )
- : [asc(rfqsView.createdAt)];
-
- // 트랜잭션 내부에서 Repository 호출
- const { data, total } = await db.transaction(async (tx) => {
- const data = await selectRfqs(tx, {
- where: finalWhere,
- orderBy,
- offset,
- limit: input.perPage,
- });
+ const orderBy =
+ input.sort.length > 0
+ ? input.sort.map((item) =>
+ item.desc ? desc(rfqsView[item.id]) : asc(rfqsView[item.id])
+ )
+ : [asc(rfqsView.createdAt)];
+
+ // 트랜잭션 내부에서 Repository 호출
+ const { data, total } = await db.transaction(async (tx) => {
+ const data = await selectRfqs(tx, {
+ where: finalWhere,
+ orderBy,
+ offset,
+ limit: input.perPage,
+ });
- const total = await countRfqs(tx, finalWhere);
- return { data, total };
- });
+ const total = await countRfqs(tx, finalWhere);
+ return { data, total };
+ });
- const pageCount = Math.ceil(total / input.perPage);
+ const pageCount = Math.ceil(total / input.perPage);
- return { data, pageCount };
- } catch (err) {
- console.error("getRfqs 에러:", err); // 자세한 에러 로깅
+ return { data, pageCount };
+ } catch (err) {
+ console.error("getRfqs 에러:", err); // 자세한 에러 로깅
- // 에러 발생 시 디폴트
- return { data: [], pageCount: 0 };
- }
- },
- [JSON.stringify(input)],
- {
- revalidate: 3600,
- tags: [`rfqs`],
- }
- )();
+ // 에러 발생 시 디폴트
+ return { data: [], pageCount: 0 };
+ }
}
/** Status별 개수 */
export async function getRfqStatusCounts() {
- return unstable_cache(
- async () => {
- try {
- const initial: Record<Rfq["status"], number> = {
- DRAFT: 0,
- PUBLISHED: 0,
- EVALUATION: 0,
- AWARDED: 0,
- };
+ try {
+ const initial: Record<Rfq["status"], number> = {
+ DRAFT: 0,
+ PUBLISHED: 0,
+ EVALUATION: 0,
+ AWARDED: 0,
+ };
- const result = await db.transaction(async (tx) => {
- const rows = await groupByStatus(tx);
- return rows.reduce<Record<Rfq["status"], number>>((acc, { status, count }) => {
- acc[status] = count;
- return acc;
- }, initial);
- });
+ const result = await db.transaction(async (tx) => {
+ const rows = await groupByStatus(tx);
+ return rows.reduce<Record<Rfq["status"], number>>((acc, { status, count }) => {
+ acc[status] = count;
+ return acc;
+ }, initial);
+ });
- return result;
- } catch {
- return {} as Record<Rfq["status"], number>;
- }
- },
- [`rfq-status-counts`],
- {
- revalidate: 3600,
- tags: [`rfqs`],
- }
- )();
+ return result;
+ } catch {
+ return {} as Record<Rfq["status"], number>;
+ }
}
@@ -161,7 +141,6 @@ export async function getRfqStatusCounts() {
* 전체 Rfq 개수를 고정
*/
export async function createRfq(input: CreateRfqSchema) {
- unstable_noStore();
try {
await db.transaction(async (tx) => {
await insertRfq(tx, {
@@ -174,8 +153,6 @@ export async function createRfq(input: CreateRfqSchema) {
});
});
- revalidateTag("rfqs");
-
return { data: null, error: null };
} catch (err) {
return { data: null, error: getErrorMessage(err) };
@@ -188,7 +165,6 @@ export async function createRfq(input: CreateRfqSchema) {
/** 단건 업데이트 */
export async function modifyRfq(input: UpdateRfqSchema & { id: number }) {
- unstable_noStore();
try {
await db.transaction(async (tx) => {
await updateRfq(tx, input.id, {
@@ -200,7 +176,6 @@ export async function modifyRfq(input: UpdateRfqSchema & { id: number }) {
});
});
- revalidateTag("rfqs");
return { data: null, error: null };
} catch (err) {
@@ -213,7 +188,6 @@ export async function modifyRfqs(input: {
status?: Rfq["status"];
dueDate?: Date
}) {
- unstable_noStore();
try {
await db.transaction(async (tx) => {
await updateRfqs(tx, input.ids, {
@@ -222,7 +196,7 @@ export async function modifyRfqs(input: {
});
});
- revalidateTag("rfqs");
+
return { data: null, error: null };
} catch (err) {
@@ -237,7 +211,6 @@ export async function modifyRfqs(input: {
/** 단건 삭제 */
export async function removeRfq(input: { id: number }) {
- unstable_noStore();
try {
await db.transaction(async (tx) => {
// 삭제
@@ -245,7 +218,7 @@ export async function removeRfq(input: { id: number }) {
// 바로 새 Rfq 생성
});
- revalidateTag("rfqs");
+
return { data: null, error: null };
} catch (err) {
@@ -255,14 +228,13 @@ export async function removeRfq(input: { id: number }) {
/** 복수 삭제 */
export async function removeRfqs(input: { ids: number[] }) {
- unstable_noStore();
try {
await db.transaction(async (tx) => {
// 삭제
await deleteRfqsByIds(tx, input.ids);
});
- revalidateTag("rfqs");
+
return { data: null, error: null };
} catch (err) {
@@ -275,8 +247,6 @@ export async function removeRfqs(input: { ids: number[] }) {
* RFQ 아이템 삭제 함수
*/
export async function deleteRfqItem(input: { id: number, rfqId: number }) {
- unstable_noStore(); // Next.js 서버 액션 캐싱 방지
-
try {
// 삭제 작업 수행
await db
@@ -288,12 +258,6 @@ export async function deleteRfqItem(input: { id: number, rfqId: number }) {
)
);
- console.log(`Deleted RFQ item: ${input.id} for RFQ ${input.rfqId}`);
-
- // 캐시 무효화
- revalidateTag("rfqs");
- revalidateTag(`rfq-${input.rfqId}`);
-
return { data: null, error: null };
} catch (err) {
console.error("Error in deleteRfqItem:", err);
@@ -303,8 +267,6 @@ export async function deleteRfqItem(input: { id: number, rfqId: number }) {
// createRfqItem 함수 수정 (id 파라미터 추가)
export async function createRfqItem(input: CreateRfqItemSchema & { id?: number }) {
- unstable_noStore();
-
try {
// DB 트랜잭션
await db.transaction(async (tx) => {
@@ -321,7 +283,6 @@ export async function createRfqItem(input: CreateRfqItemSchema & { id?: number }
})
.where(eq(rfqItems.id, input.id));
- console.log(`Updated RFQ item with id: ${input.id}`);
} else {
// 기존 로직: 같은 itemCode로 이미 존재하는지 확인 후 업데이트/생성
const existingItems = await tx
@@ -363,10 +324,6 @@ export async function createRfqItem(input: CreateRfqItemSchema & { id?: number }
}
});
- // 캐시 무효화
- revalidateTag("rfqs");
- revalidateTag(`rfq-${input.rfqId}`);
-
return { data: null, error: null };
} catch (err) {
console.error("Error in createRfqItem:", err);
@@ -463,9 +420,6 @@ export async function processRfqAttachments(args: {
const newCount = countRow?.cnt ?? 0;
- // 3) revalidateTag 등 캐시 무효화
- revalidateTag("rfq-attachments");
-
return { ok: true, updatedItemCount: newCount };
} catch (error) {
console.error("processRfqAttachments error:", error);
@@ -529,243 +483,240 @@ export const findRfqById = async (id: number): Promise<RfqViewWithItems | null>
};
export async function getMatchedVendors(input: GetMatchedVendorsSchema, rfqId: number) {
- return unstable_cache(
- async () => {
- // ─────────────────────────────────────────────────────
- // 1) rfq_items에서 distinct itemCode
- // ─────────────────────────────────────────────────────
- const itemRows = await db
- .select({ code: rfqItems.itemCode })
- .from(rfqItems)
- .where(eq(rfqItems.rfqId, rfqId))
- .groupBy(rfqItems.itemCode)
+ try {
+ // ─────────────────────────────────────────────────────
+ // 1) rfq_items에서 distinct itemCode
+ // ─────────────────────────────────────────────────────
+ const itemRows = await db
+ .select({ code: rfqItems.itemCode })
+ .from(rfqItems)
+ .where(eq(rfqItems.rfqId, rfqId))
+ .groupBy(rfqItems.itemCode)
- const itemCodes = itemRows.map((r) => r.code)
- const itemCount = itemCodes.length
- if (itemCount === 0) {
- return { data: [], pageCount: 0 }
- }
+ const itemCodes = itemRows.map((r) => r.code)
+ const itemCount = itemCodes.length
+ if (itemCount === 0) {
+ return { data: [], pageCount: 0 }
+ }
- // ─────────────────────────────────────────────────────
- // 2) vendorPossibleItems에서 모든 itemCodes를 보유한 vendor
- // ─────────────────────────────────────────────────────
- const inList = itemCodes.map((c) => `'${c}'`).join(",")
- const sqlVendorIds = await db.execute(
- sql`
- SELECT vpi.vendor_id AS "vendorId"
- FROM ${vendorPossibleItems} vpi
- WHERE vpi.item_code IN (${sql.raw(inList)})
- GROUP BY vpi.vendor_id
- HAVING COUNT(DISTINCT vpi.item_code) = ${itemCount}
- `
- )
- const vendorIdList = sqlVendorIds.rows.map((row: any) => +row.vendorId)
- if (vendorIdList.length === 0) {
- return { data: [], pageCount: 0 }
- }
+ // ─────────────────────────────────────────────────────
+ // 2) vendorPossibleItems에서 모든 itemCodes를 보유한 vendor
+ // ─────────────────────────────────────────────────────
+ const inList = itemCodes.map((c) => `'${c}'`).join(",")
+ const sqlVendorIds = await db.execute(
+ sql`
+ SELECT vpi.vendor_id AS "vendorId"
+ FROM ${vendorPossibleItems} vpi
+ WHERE vpi.item_code IN (${sql.raw(inList)})
+ GROUP BY vpi.vendor_id
+ HAVING COUNT(DISTINCT vpi.item_code) = ${itemCount}
+ `
+ )
+ const vendorIdList = sqlVendorIds.rows.map((row: any) => +row.vendorId)
+ if (vendorIdList.length === 0) {
+ return { data: [], pageCount: 0 }
+ }
- // ─────────────────────────────────────────────────────
- // 3) 필터/검색/정렬
- // ─────────────────────────────────────────────────────
- const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10)
- const limit = input.perPage ?? 10
+ // ─────────────────────────────────────────────────────
+ // 3) 필터/검색/정렬
+ // ─────────────────────────────────────────────────────
+ const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10)
+ const limit = input.perPage ?? 10
+
+ // (가) 커스텀 필터
+ // 여기서는 "뷰(vendorRfqView)"의 컬럼들에 대해 필터합니다.
+ const advancedWhere = filterColumns({
+ // 테이블이 아니라 "뷰"를 넘길 수도 있고,
+ // 혹은 columns 객체(연결된 모든 컬럼)로 넘겨도 됩니다.
+ table: vendorRfqView,
+ filters: input.filters ?? [],
+ joinOperator: input.joinOperator ?? "and",
+ })
- // (가) 커스텀 필터
- // 여기서는 "뷰(vendorRfqView)"의 컬럼들에 대해 필터합니다.
- const advancedWhere = filterColumns({
- // 테이블이 아니라 "뷰"를 넘길 수도 있고,
- // 혹은 columns 객체(연결된 모든 컬럼)로 넘겨도 됩니다.
- table: vendorRfqView,
- filters: input.filters ?? [],
- joinOperator: input.joinOperator ?? "and",
- })
+ // (나) 글로벌 검색
+ let globalWhere
+ if (input.search) {
+ const s = `%${input.search}%`
+ globalWhere = or(
+ sql`${vendorRfqView.vendorName} ILIKE ${s}`,
+ sql`${vendorRfqView.vendorCode} ILIKE ${s}`,
+ sql`${vendorRfqView.email} ILIKE ${s}`
+ )
+ }
- // (나) 글로벌 검색
- let globalWhere
- if (input.search) {
- const s = `%${input.search}%`
- globalWhere = or(
- sql`${vendorRfqView.vendorName} ILIKE ${s}`,
- sql`${vendorRfqView.vendorCode} ILIKE ${s}`,
- sql`${vendorRfqView.email} ILIKE ${s}`
- )
- }
+ // (다) 최종 where
+ // vendorId가 vendorIdList 내에 있어야 하고,
+ // 특정 rfqId(뷰에 담긴 값)도 일치해야 함.
+ const finalWhere = and(
+ inArray(vendorRfqView.vendorId, vendorIdList),
+ // 아래 라인은 rfq에 초대된 벤더만 필터링하는 조건으로 추정되지만
+ // rfq 를 진행하기 전에도 벤더를 보여줘야 하므로 주석처리하겠습니다
+ // eq(vendorRfqView.rfqId, rfqId),
+ advancedWhere,
+ globalWhere
+ )
+
+ // (라) 정렬
+ const orderBy = input.sort?.length
+ ? input.sort.map((s) => {
+ // "column id" -> vendorRfqView.* 중 하나
+ const col = (vendorRfqView as any)[s.id]
+ return s.desc ? desc(col) : asc(col)
+ })
+ : [asc(vendorRfqView.vendorId)]
- // (다) 최종 where
- // vendorId가 vendorIdList 내에 있어야 하고,
- // 특정 rfqId(뷰에 담긴 값)도 일치해야 함.
- const finalWhere = and(
- inArray(vendorRfqView.vendorId, vendorIdList),
- // 아래 라인은 rfq에 초대된 벤더만 필터링하는 조건으로 추정되지만
- // rfq 를 진행하기 전에도 벤더를 보여줘야 하므로 주석처리하겠습니다
- // eq(vendorRfqView.rfqId, rfqId),
- advancedWhere,
- globalWhere
+ // ─────────────────────────────────────────────────────
+ // 4) View에서 데이터 SELECT
+ // ─────────────────────────────────────────────────────
+ const [rows, total] = await db.transaction(async (tx) => {
+ const data = await tx
+ .select({
+ id: vendorRfqView.vendorId,
+ vendorID: vendorRfqView.vendorId,
+ vendorName: vendorRfqView.vendorName,
+ vendorCode: vendorRfqView.vendorCode,
+ address: vendorRfqView.address,
+ country: vendorRfqView.country,
+ email: vendorRfqView.email,
+ website: vendorRfqView.website,
+ vendorStatus: vendorRfqView.vendorStatus,
+ // rfqVendorStatus와 rfqVendorUpdated는 나중에 정확한 데이터로 교체할 예정
+ rfqVendorStatus: vendorRfqView.rfqVendorStatus,
+ rfqVendorUpdated: vendorRfqView.rfqVendorUpdated,
+ })
+ .from(vendorRfqView)
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .offset(offset)
+ .limit(limit)
+
+ // 중복 제거된 데이터 생성
+ const distinctData = Array.from(
+ new Map(data.map(row => [row.id, row])).values()
)
- // (라) 정렬
- const orderBy = input.sort?.length
- ? input.sort.map((s) => {
- // "column id" -> vendorRfqView.* 중 하나
- const col = (vendorRfqView as any)[s.id]
- return s.desc ? desc(col) : asc(col)
- })
- : [asc(vendorRfqView.vendorId)]
+ // 중복 제거된 총 개수 계산
+ const [{ count }] = await tx
+ .select({ count: sql<number>`count(DISTINCT ${vendorRfqView.vendorId})`.as("count") })
+ .from(vendorRfqView)
+ .where(finalWhere)
- // ─────────────────────────────────────────────────────
- // 4) View에서 데이터 SELECT
- // ─────────────────────────────────────────────────────
- const [rows, total] = await db.transaction(async (tx) => {
- const data = await tx
- .select({
- id: vendorRfqView.vendorId,
- vendorID: vendorRfqView.vendorId,
- vendorName: vendorRfqView.vendorName,
- vendorCode: vendorRfqView.vendorCode,
- address: vendorRfqView.address,
- country: vendorRfqView.country,
- email: vendorRfqView.email,
- website: vendorRfqView.website,
- vendorStatus: vendorRfqView.vendorStatus,
- // rfqVendorStatus와 rfqVendorUpdated는 나중에 정확한 데이터로 교체할 예정
- rfqVendorStatus: vendorRfqView.rfqVendorStatus,
- rfqVendorUpdated: vendorRfqView.rfqVendorUpdated,
- })
- .from(vendorRfqView)
- .where(finalWhere)
- .orderBy(...orderBy)
- .offset(offset)
- .limit(limit)
+ return [distinctData, Number(count)]
+ })
- // 중복 제거된 데이터 생성
- const distinctData = Array.from(
- new Map(data.map(row => [row.id, row])).values()
- )
- // 중복 제거된 총 개수 계산
- const [{ count }] = await tx
- .select({ count: sql<number>`count(DISTINCT ${vendorRfqView.vendorId})`.as("count") })
- .from(vendorRfqView)
- .where(finalWhere)
+ // ─────────────────────────────────────────────────────
+ // 4-1) 정확한 rfqVendorStatus와 rfqVendorUpdated 조회
+ // ─────────────────────────────────────────────────────
+ const distinctVendorIds = [...new Set(rows.map((r) => r.id))]
- return [distinctData, Number(count)]
+ // vendorResponses 테이블에서 정확한 상태와 업데이트 시간 조회
+ const vendorStatuses = await db
+ .select({
+ vendorId: vendorResponses.vendorId,
+ status: vendorResponses.responseStatus,
+ updatedAt: vendorResponses.updatedAt
})
-
-
- // ─────────────────────────────────────────────────────
- // 4-1) 정확한 rfqVendorStatus와 rfqVendorUpdated 조회
- // ─────────────────────────────────────────────────────
- const distinctVendorIds = [...new Set(rows.map((r) => r.id))]
-
- // vendorResponses 테이블에서 정확한 상태와 업데이트 시간 조회
- const vendorStatuses = await db
- .select({
- vendorId: vendorResponses.vendorId,
- status: vendorResponses.responseStatus,
- updatedAt: vendorResponses.updatedAt
- })
- .from(vendorResponses)
- .where(
- and(
- inArray(vendorResponses.vendorId, distinctVendorIds),
- eq(vendorResponses.rfqId, rfqId)
- )
+ .from(vendorResponses)
+ .where(
+ and(
+ inArray(vendorResponses.vendorId, distinctVendorIds),
+ eq(vendorResponses.rfqId, rfqId)
)
+ )
- // vendorId별 상태정보 맵 생성
- const statusMap = new Map<number, { status: string, updatedAt: Date }>()
- for (const vs of vendorStatuses) {
- statusMap.set(vs.vendorId, {
- status: vs.status,
- updatedAt: vs.updatedAt
- })
- }
+ // vendorId별 상태정보 맵 생성
+ const statusMap = new Map<number, { status: string, updatedAt: Date }>()
+ for (const vs of vendorStatuses) {
+ statusMap.set(vs.vendorId, {
+ status: vs.status,
+ updatedAt: vs.updatedAt
+ })
+ }
- // 정확한 상태 정보로 업데이트된 rows 생성
- const updatedRows = rows.map(row => ({
- ...row,
- rfqVendorStatus: statusMap.get(row.id)?.status || null,
- rfqVendorUpdated: statusMap.get(row.id)?.updatedAt || null
- }))
-
- // ─────────────────────────────────────────────────────
- // 5) 코멘트 조회: 기존과 동일
- // ─────────────────────────────────────────────────────
- console.log("distinctVendorIds", distinctVendorIds)
- const commAll = await db
- .select()
- .from(rfqComments)
- .where(
- and(
- inArray(rfqComments.vendorId, distinctVendorIds),
- eq(rfqComments.rfqId, rfqId),
- isNull(rfqComments.evaluationId),
- isNull(rfqComments.cbeId)
- )
+ // 정확한 상태 정보로 업데이트된 rows 생성
+ const updatedRows = rows.map(row => ({
+ ...row,
+ rfqVendorStatus: statusMap.get(row.id)?.status || null,
+ rfqVendorUpdated: statusMap.get(row.id)?.updatedAt || null
+ }))
+
+ // ─────────────────────────────────────────────────────
+ // 5) 코멘트 조회: 기존과 동일
+ // ─────────────────────────────────────────────────────
+ console.log("distinctVendorIds", distinctVendorIds)
+ const commAll = await db
+ .select()
+ .from(rfqComments)
+ .where(
+ and(
+ inArray(rfqComments.vendorId, distinctVendorIds),
+ eq(rfqComments.rfqId, rfqId),
+ isNull(rfqComments.evaluationId),
+ isNull(rfqComments.cbeId)
)
+ )
- const commByVendorId = new Map<number, any[]>()
- // 먼저 모든 사용자 ID를 수집
- const userIds = new Set(commAll.map(c => c.commentedBy));
- const userIdsArray = Array.from(userIds);
-
- // Drizzle의 select 메서드를 사용하여 사용자 정보를 가져옴
- const usersData = await db
- .select({
- id: users.id,
- email: users.email,
- })
- .from(users)
- .where(inArray(users.id, userIdsArray));
-
- // 사용자 ID를 키로 하는 맵 생성
- const userMap = new Map();
- for (const user of usersData) {
- userMap.set(user.id, user);
- }
-
- // 댓글 정보를 협력업체 ID별로 그룹화하고, 사용자 이메일 추가
- for (const c of commAll) {
- const vid = c.vendorId!
- if (!commByVendorId.has(vid)) {
- commByVendorId.set(vid, [])
- }
+ const commByVendorId = new Map<number, any[]>()
+ // 먼저 모든 사용자 ID를 수집
+ const userIds = new Set(commAll.map(c => c.commentedBy));
+ const userIdsArray = Array.from(userIds);
- // 사용자 정보 가져오기
- const user = userMap.get(c.commentedBy);
- const userEmail = user ? user.email : 'unknown@example.com'; // 사용자를 찾지 못한 경우 기본값 설정
+ // Drizzle의 select 메서드를 사용하여 사용자 정보를 가져옴
+ const usersData = await db
+ .select({
+ id: users.id,
+ email: users.email,
+ })
+ .from(users)
+ .where(inArray(users.id, userIdsArray));
- commByVendorId.get(vid)!.push({
- id: c.id,
- commentText: c.commentText,
- vendorId: c.vendorId,
- evaluationId: c.evaluationId,
- createdAt: c.createdAt,
- commentedBy: c.commentedBy,
- commentedByEmail: userEmail, // 이메일 추가
- })
- }
- // ─────────────────────────────────────────────────────
- // 6) rows에 comments 병합
- // ─────────────────────────────────────────────────────
- const final = updatedRows.map((row) => ({
- ...row,
- comments: commByVendorId.get(row.id) ?? [],
- }))
+ // 사용자 ID를 키로 하는 맵 생성
+ const userMap = new Map();
+ for (const user of usersData) {
+ userMap.set(user.id, user);
+ }
- // ─────────────────────────────────────────────────────
- // 7) 반환
- // ─────────────────────────────────────────────────────
- const pageCount = Math.ceil(total / limit)
- return { data: final, pageCount }
- },
- [JSON.stringify({ input, rfqId })],
- { revalidate: 3600, tags: ["vendors", `rfq-${rfqId}`] }
- )()
+ // 댓글 정보를 협력업체 ID별로 그룹화하고, 사용자 이메일 추가
+ for (const c of commAll) {
+ const vid = c.vendorId!
+ if (!commByVendorId.has(vid)) {
+ commByVendorId.set(vid, [])
+ }
+
+ // 사용자 정보 가져오기
+ const user = userMap.get(c.commentedBy);
+ const userEmail = user ? user.email : 'unknown@example.com'; // 사용자를 찾지 못한 경우 기본값 설정
+
+ commByVendorId.get(vid)!.push({
+ id: c.id,
+ commentText: c.commentText,
+ vendorId: c.vendorId,
+ evaluationId: c.evaluationId,
+ createdAt: c.createdAt,
+ commentedBy: c.commentedBy,
+ commentedByEmail: userEmail, // 이메일 추가
+ })
+ }
+ // ─────────────────────────────────────────────────────
+ // 6) rows에 comments 병합
+ // ─────────────────────────────────────────────────────
+ const final = updatedRows.map((row) => ({
+ ...row,
+ comments: commByVendorId.get(row.id) ?? [],
+ }))
+
+ // ─────────────────────────────────────────────────────
+ // 7) 반환
+ // ─────────────────────────────────────────────────────
+ const pageCount = Math.ceil(total / limit)
+ return { data: final, pageCount }
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
}
export async function inviteVendors(input: InviteVendorsInput) {
- unstable_noStore() // 서버 액션 캐싱 방지
try {
const { rfqId, vendorIds } = input
if (!rfqId || !Array.isArray(vendorIds) || vendorIds.length === 0) {
@@ -840,7 +791,7 @@ export async function inviteVendors(input: InviteVendorsInput) {
})
const { rfqRow, items, vendorRows, attachments } = rfqData
- const loginUrl = `http://${host}/en/partners/rfq`
+ const loginUrl = `http://${host}/en/partners/rfq-tech`
// 이메일 전송 오류를 기록할 배열
const emailErrors = []
@@ -922,22 +873,6 @@ export async function inviteVendors(input: InviteVendorsInput) {
console.log(`Updated RFQ #${rfqId} status to PUBLISHED`)
})
-
- // 캐시 무효화
- revalidateTag("tbe-vendors")
- revalidateTag("all-tbe-vendors")
- revalidateTag("rfq-vendors")
- revalidateTag("cbe-vendors")
- revalidateTag("rfqs")
- revalidateTag(`rfq-${rfqId}`)
- // revalidateTag("rfqs");
- // revalidateTag(`rfq-${rfqId}`);
- // revalidateTag("vendors");
- // revalidateTag("rfq-vendors");
- // vendorIds.forEach(vendorId => {
- // revalidateTag(`vendor-${vendorId}`);
- // });
-
// 이메일 오류가 있었는지 확인
if (emailErrors.length > 0) {
return {
@@ -960,273 +895,268 @@ export async function inviteVendors(input: InviteVendorsInput) {
* TBE용 평가 데이터 목록 조회
*/
export async function getTBE(input: GetTBESchema, rfqId: number) {
- return unstable_cache(
- async () => {
- // 1) 페이징
- const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10)
- const limit = input.perPage ?? 10
-
- // 2) 고급 필터
- const advancedWhere = filterColumns({
- table: vendorTbeView,
- filters: input.filters ?? [],
- joinOperator: input.joinOperator ?? "and",
- })
-
- // 3) 글로벌 검색
- let globalWhere
- if (input.search) {
- const s = `%${input.search}%`
- globalWhere = or(
- sql`${vendorTbeView.vendorName} ILIKE ${s}`,
- sql`${vendorTbeView.vendorCode} ILIKE ${s}`,
- sql`${vendorTbeView.email} ILIKE ${s}`
- )
- }
+ try {
+ // 1) 페이징
+ const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10)
+ const limit = input.perPage ?? 10
+
+ // 2) 고급 필터
+ const advancedWhere = filterColumns({
+ table: vendorTbeView,
+ filters: input.filters ?? [],
+ joinOperator: input.joinOperator ?? "and",
+ })
- // 4) REJECTED 아니거나 NULL
- const notRejected = or(
- ne(vendorTbeView.rfqVendorStatus, "REJECTED"),
- isNull(vendorTbeView.rfqVendorStatus)
+ // 3) 글로벌 검색
+ let globalWhere
+ if (input.search) {
+ const s = `%${input.search}%`
+ globalWhere = or(
+ sql`${vendorTbeView.vendorName} ILIKE ${s}`,
+ sql`${vendorTbeView.vendorCode} ILIKE ${s}`,
+ sql`${vendorTbeView.email} ILIKE ${s}`
)
+ }
- // 5) finalWhere
- const finalWhere = and(
- eq(vendorTbeView.rfqId, rfqId),
- advancedWhere,
- globalWhere
- )
+ // 4) REJECTED 아니거나 NULL
+ const notRejected = or(
+ ne(vendorTbeView.rfqVendorStatus, "REJECTED"),
+ isNull(vendorTbeView.rfqVendorStatus)
+ )
+
+ // 5) finalWhere
+ const finalWhere = and(
+ eq(vendorTbeView.rfqId, rfqId),
+ advancedWhere,
+ globalWhere
+ )
+
+ // 6) 정렬
+ const orderBy = input.sort?.length
+ ? input.sort.map((s) => {
+ const col = (vendorTbeView as any)[s.id]
+ return s.desc ? desc(col) : asc(col)
+ })
+ : [asc(vendorTbeView.vendorId)]
- // 6) 정렬
- const orderBy = input.sort?.length
- ? input.sort.map((s) => {
- const col = (vendorTbeView as any)[s.id]
- return s.desc ? desc(col) : asc(col)
+ // 7) 메인 SELECT
+ const [rows, total] = await db.transaction(async (tx) => {
+ const data = await tx
+ .select({
+ // 원하는 컬럼들
+ id: vendorTbeView.vendorId,
+ tbeId: vendorTbeView.tbeId,
+ vendorId: vendorTbeView.vendorId,
+ vendorName: vendorTbeView.vendorName,
+ vendorCode: vendorTbeView.vendorCode,
+ address: vendorTbeView.address,
+ country: vendorTbeView.country,
+ email: vendorTbeView.email,
+ website: vendorTbeView.website,
+ vendorStatus: vendorTbeView.vendorStatus,
+
+ rfqId: vendorTbeView.rfqId,
+ rfqCode: vendorTbeView.rfqCode,
+ projectCode: vendorTbeView.projectCode,
+ projectName: vendorTbeView.projectName,
+ description: vendorTbeView.description,
+ dueDate: vendorTbeView.dueDate,
+
+ rfqVendorStatus: vendorTbeView.rfqVendorStatus,
+ rfqVendorUpdated: vendorTbeView.rfqVendorUpdated,
+
+ tbeResult: vendorTbeView.tbeResult,
+ tbeNote: vendorTbeView.tbeNote,
+ tbeUpdated: vendorTbeView.tbeUpdated,
+
+ technicalResponseId:vendorTbeView.technicalResponseId,
+ technicalResponseStatus:vendorTbeView.technicalResponseStatus,
+ technicalSummary:vendorTbeView.technicalSummary,
+ technicalNotes:vendorTbeView.technicalNotes,
+ technicalUpdated:vendorTbeView.technicalUpdated,
})
- : [asc(vendorTbeView.vendorId)]
-
- // 7) 메인 SELECT
- const [rows, total] = await db.transaction(async (tx) => {
- const data = await tx
- .select({
- // 원하는 컬럼들
- id: vendorTbeView.vendorId,
- tbeId: vendorTbeView.tbeId,
- vendorId: vendorTbeView.vendorId,
- vendorName: vendorTbeView.vendorName,
- vendorCode: vendorTbeView.vendorCode,
- address: vendorTbeView.address,
- country: vendorTbeView.country,
- email: vendorTbeView.email,
- website: vendorTbeView.website,
- vendorStatus: vendorTbeView.vendorStatus,
-
- rfqId: vendorTbeView.rfqId,
- rfqCode: vendorTbeView.rfqCode,
- projectCode: vendorTbeView.projectCode,
- projectName: vendorTbeView.projectName,
- description: vendorTbeView.description,
- dueDate: vendorTbeView.dueDate,
-
- rfqVendorStatus: vendorTbeView.rfqVendorStatus,
- rfqVendorUpdated: vendorTbeView.rfqVendorUpdated,
-
- tbeResult: vendorTbeView.tbeResult,
- tbeNote: vendorTbeView.tbeNote,
- tbeUpdated: vendorTbeView.tbeUpdated,
+ .from(vendorTbeView)
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .offset(offset)
+ .limit(limit)
+
+ const [{ count }] = await tx
+ .select({ count: sql<number>`count(*)`.as("count") })
+ .from(vendorTbeView)
+ .where(finalWhere)
+
+ return [data, Number(count)]
+ })
- technicalResponseId:vendorTbeView.technicalResponseId,
- technicalResponseStatus:vendorTbeView.technicalResponseStatus,
- technicalSummary:vendorTbeView.technicalSummary,
- technicalNotes:vendorTbeView.technicalNotes,
- technicalUpdated:vendorTbeView.technicalUpdated,
- })
- .from(vendorTbeView)
- .where(finalWhere)
- .orderBy(...orderBy)
- .offset(offset)
- .limit(limit)
+ if (!rows.length) {
+ return { data: [], pageCount: 0 }
+ }
- const [{ count }] = await tx
- .select({ count: sql<number>`count(*)`.as("count") })
- .from(vendorTbeView)
- .where(finalWhere)
+ // 8) Comments 조회
+ const distinctVendorIds = [...new Set(rows.map((r) => r.vendorId))]
- return [data, Number(count)]
+ const commAll = await db
+ .select({
+ id: rfqComments.id,
+ commentText: rfqComments.commentText,
+ vendorId: rfqComments.vendorId,
+ evaluationId: rfqComments.evaluationId,
+ createdAt: rfqComments.createdAt,
+ commentedBy: rfqComments.commentedBy,
+ evalType: rfqEvaluations.evalType,
})
-
- if (!rows.length) {
- return { data: [], pageCount: 0 }
- }
-
- // 8) Comments 조회
- const distinctVendorIds = [...new Set(rows.map((r) => r.vendorId))]
-
- const commAll = await db
- .select({
- id: rfqComments.id,
- commentText: rfqComments.commentText,
- vendorId: rfqComments.vendorId,
- evaluationId: rfqComments.evaluationId,
- createdAt: rfqComments.createdAt,
- commentedBy: rfqComments.commentedBy,
- evalType: rfqEvaluations.evalType,
- })
- .from(rfqComments)
- .innerJoin(
- rfqEvaluations,
- and(
- eq(rfqEvaluations.id, rfqComments.evaluationId),
- eq(rfqEvaluations.evalType, "TBE")
- )
+ .from(rfqComments)
+ .innerJoin(
+ rfqEvaluations,
+ and(
+ eq(rfqEvaluations.id, rfqComments.evaluationId),
+ eq(rfqEvaluations.evalType, "TBE")
)
- .where(
- and(
- isNotNull(rfqComments.evaluationId),
- eq(rfqComments.rfqId, rfqId),
- inArray(rfqComments.vendorId, distinctVendorIds)
- )
+ )
+ .where(
+ and(
+ isNotNull(rfqComments.evaluationId),
+ eq(rfqComments.rfqId, rfqId),
+ inArray(rfqComments.vendorId, distinctVendorIds)
)
+ )
- // 8-A) vendorId -> comments grouping
- const commByVendorId = new Map<number, any[]>()
- for (const c of commAll) {
- const vid = c.vendorId!
- if (!commByVendorId.has(vid)) {
- commByVendorId.set(vid, [])
- }
- commByVendorId.get(vid)!.push({
- id: c.id,
- commentText: c.commentText,
- vendorId: c.vendorId,
- evaluationId: c.evaluationId,
- createdAt: c.createdAt,
- commentedBy: c.commentedBy,
- })
- }
+ // 8-A) vendorId -> comments grouping
+ const commByVendorId = new Map<number, any[]>()
+ for (const c of commAll) {
+ const vid = c.vendorId!
+ if (!commByVendorId.has(vid)) {
+ commByVendorId.set(vid, [])
+ }
+ commByVendorId.get(vid)!.push({
+ id: c.id,
+ commentText: c.commentText,
+ vendorId: c.vendorId,
+ evaluationId: c.evaluationId,
+ createdAt: c.createdAt,
+ commentedBy: c.commentedBy,
+ })
+ }
- // 9) TBE 파일 조회 - vendorResponseAttachments로 대체
- // Step 1: Get vendorResponses for the rfqId and vendorIds
- const responsesAll = await db
- .select({
- id: vendorResponses.id,
- vendorId: vendorResponses.vendorId
- })
- .from(vendorResponses)
- .where(
- and(
- eq(vendorResponses.rfqId, rfqId),
- inArray(vendorResponses.vendorId, distinctVendorIds)
- )
- );
+ // 9) TBE 파일 조회 - vendorResponseAttachments로 대체
+ // Step 1: Get vendorResponses for the rfqId and vendorIds
+ const responsesAll = await db
+ .select({
+ id: vendorResponses.id,
+ vendorId: vendorResponses.vendorId
+ })
+ .from(vendorResponses)
+ .where(
+ and(
+ eq(vendorResponses.rfqId, rfqId),
+ inArray(vendorResponses.vendorId, distinctVendorIds)
+ )
+ );
- // Group responses by vendorId for later lookup
- const responsesByVendorId = new Map<number, number[]>();
- for (const resp of responsesAll) {
- if (!responsesByVendorId.has(resp.vendorId)) {
- responsesByVendorId.set(resp.vendorId, []);
- }
- responsesByVendorId.get(resp.vendorId)!.push(resp.id);
+ // Group responses by vendorId for later lookup
+ const responsesByVendorId = new Map<number, number[]>();
+ for (const resp of responsesAll) {
+ if (!responsesByVendorId.has(resp.vendorId)) {
+ responsesByVendorId.set(resp.vendorId, []);
}
+ responsesByVendorId.get(resp.vendorId)!.push(resp.id);
+ }
- // Step 2: Get all responseIds
- const allResponseIds = responsesAll.map(r => r.id);
+ // Step 2: Get all responseIds
+ const allResponseIds = responsesAll.map(r => r.id);
- // Step 3: Get technicalResponses for these responseIds
- const technicalResponsesAll = await db
- .select({
- id: vendorTechnicalResponses.id,
- responseId: vendorTechnicalResponses.responseId
- })
- .from(vendorTechnicalResponses)
- .where(inArray(vendorTechnicalResponses.responseId, allResponseIds));
+ // Step 3: Get technicalResponses for these responseIds
+ const technicalResponsesAll = await db
+ .select({
+ id: vendorTechnicalResponses.id,
+ responseId: vendorTechnicalResponses.responseId
+ })
+ .from(vendorTechnicalResponses)
+ .where(inArray(vendorTechnicalResponses.responseId, allResponseIds));
- // Create mapping from responseId to technicalResponseIds
- const technicalResponseIdsByResponseId = new Map<number, number[]>();
- for (const tr of technicalResponsesAll) {
- if (!technicalResponseIdsByResponseId.has(tr.responseId)) {
- technicalResponseIdsByResponseId.set(tr.responseId, []);
- }
- technicalResponseIdsByResponseId.get(tr.responseId)!.push(tr.id);
+ // Create mapping from responseId to technicalResponseIds
+ const technicalResponseIdsByResponseId = new Map<number, number[]>();
+ for (const tr of technicalResponsesAll) {
+ if (!technicalResponseIdsByResponseId.has(tr.responseId)) {
+ technicalResponseIdsByResponseId.set(tr.responseId, []);
}
+ technicalResponseIdsByResponseId.get(tr.responseId)!.push(tr.id);
+ }
- // Step 4: Get all technicalResponseIds
- const allTechnicalResponseIds = technicalResponsesAll.map(tr => tr.id);
+ // Step 4: Get all technicalResponseIds
+ const allTechnicalResponseIds = technicalResponsesAll.map(tr => tr.id);
- // Step 5: Get attachments for these technicalResponseIds
- const filesAll = await db
- .select({
- id: vendorResponseAttachments.id,
- fileName: vendorResponseAttachments.fileName,
- filePath: vendorResponseAttachments.filePath,
- technicalResponseId: vendorResponseAttachments.technicalResponseId,
- fileType: vendorResponseAttachments.fileType,
- attachmentType: vendorResponseAttachments.attachmentType,
- description: vendorResponseAttachments.description,
- uploadedAt: vendorResponseAttachments.uploadedAt,
- uploadedBy: vendorResponseAttachments.uploadedBy
- })
- .from(vendorResponseAttachments)
- .where(
- and(
- inArray(vendorResponseAttachments.technicalResponseId, allTechnicalResponseIds),
- isNotNull(vendorResponseAttachments.technicalResponseId)
- )
- );
-
- // Step 6: Create mapping from technicalResponseId to attachments
- const filesByTechnicalResponseId = new Map<number, any[]>();
- for (const file of filesAll) {
- // Skip if technicalResponseId is null (should never happen due to our filter above)
- if (file.technicalResponseId === null) continue;
+ // Step 5: Get attachments for these technicalResponseIds
+ const filesAll = await db
+ .select({
+ id: vendorResponseAttachments.id,
+ fileName: vendorResponseAttachments.fileName,
+ filePath: vendorResponseAttachments.filePath,
+ technicalResponseId: vendorResponseAttachments.technicalResponseId,
+ fileType: vendorResponseAttachments.fileType,
+ attachmentType: vendorResponseAttachments.attachmentType,
+ description: vendorResponseAttachments.description,
+ uploadedAt: vendorResponseAttachments.uploadedAt,
+ uploadedBy: vendorResponseAttachments.uploadedBy
+ })
+ .from(vendorResponseAttachments)
+ .where(
+ and(
+ inArray(vendorResponseAttachments.technicalResponseId, allTechnicalResponseIds),
+ isNotNull(vendorResponseAttachments.technicalResponseId)
+ )
+ );
- if (!filesByTechnicalResponseId.has(file.technicalResponseId)) {
- filesByTechnicalResponseId.set(file.technicalResponseId, []);
- }
- filesByTechnicalResponseId.get(file.technicalResponseId)!.push({
- id: file.id,
- fileName: file.fileName,
- filePath: file.filePath,
- fileType: file.fileType,
- attachmentType: file.attachmentType,
- description: file.description,
- uploadedAt: file.uploadedAt,
- uploadedBy: file.uploadedBy
- });
- }
+ // Step 6: Create mapping from technicalResponseId to attachments
+ const filesByTechnicalResponseId = new Map<number, any[]>();
+ for (const file of filesAll) {
+ // Skip if technicalResponseId is null (should never happen due to our filter above)
+ if (file.technicalResponseId === null) continue;
+
+ if (!filesByTechnicalResponseId.has(file.technicalResponseId)) {
+ filesByTechnicalResponseId.set(file.technicalResponseId, []);
+ }
+ filesByTechnicalResponseId.get(file.technicalResponseId)!.push({
+ id: file.id,
+ fileName: file.fileName,
+ filePath: file.filePath,
+ fileType: file.fileType,
+ attachmentType: file.attachmentType,
+ description: file.description,
+ uploadedAt: file.uploadedAt,
+ uploadedBy: file.uploadedBy
+ });
+ }
- // Step 7: Create the final filesByVendorId map
- const filesByVendorId = new Map<number, any[]>();
- for (const [vendorId, responseIds] of responsesByVendorId.entries()) {
- filesByVendorId.set(vendorId, []);
+ // Step 7: Create the final filesByVendorId map
+ const filesByVendorId = new Map<number, any[]>();
+ for (const [vendorId, responseIds] of responsesByVendorId.entries()) {
+ filesByVendorId.set(vendorId, []);
- for (const responseId of responseIds) {
- const technicalResponseIds = technicalResponseIdsByResponseId.get(responseId) || [];
+ for (const responseId of responseIds) {
+ const technicalResponseIds = technicalResponseIdsByResponseId.get(responseId) || [];
- for (const technicalResponseId of technicalResponseIds) {
- const files = filesByTechnicalResponseId.get(technicalResponseId) || [];
- filesByVendorId.get(vendorId)!.push(...files);
- }
+ for (const technicalResponseId of technicalResponseIds) {
+ const files = filesByTechnicalResponseId.get(technicalResponseId) || [];
+ filesByVendorId.get(vendorId)!.push(...files);
}
}
+ }
- // 10) 최종 합치기
- const final = rows.map((row) => ({
- ...row,
- dueDate: row.dueDate ? new Date(row.dueDate) : null,
- comments: commByVendorId.get(row.vendorId) ?? [],
- files: filesByVendorId.get(row.vendorId) ?? [],
- }))
+ // 10) 최종 합치기
+ const final = rows.map((row) => ({
+ ...row,
+ dueDate: row.dueDate ? new Date(row.dueDate) : null,
+ comments: commByVendorId.get(row.vendorId) ?? [],
+ files: filesByVendorId.get(row.vendorId) ?? [],
+ }))
- const pageCount = Math.ceil(total / limit)
- return { data: final, pageCount }
- },
- [JSON.stringify({ input, rfqId })],
- {
- revalidate: 3600,
- tags: ["tbe", `rfq-${rfqId}`],
- }
- )()
+ const pageCount = Math.ceil(total / limit)
+ return { data: final, pageCount }
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
}
export async function getTBEforVendor(input: GetTBESchema, vendorId: number) {
@@ -1235,246 +1165,239 @@ export async function getTBEforVendor(input: GetTBESchema, vendorId: number) {
throw new Error("유효하지 않은 vendorId: 숫자 값이 필요합니다");
}
- return unstable_cache(
- async () => {
- // 1) 페이징
- const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10)
- const limit = input.perPage ?? 10
-
- // 2) 고급 필터
- const advancedWhere = filterColumns({
- table: vendorTbeView,
- filters: input.filters ?? [],
- joinOperator: input.joinOperator ?? "and",
- })
-
- // 3) 글로벌 검색
- let globalWhere
- if (input.search) {
- const s = `%${input.search}%`
- globalWhere = or(
- sql`${vendorTbeView.vendorName} ILIKE ${s}`,
- sql`${vendorTbeView.vendorCode} ILIKE ${s}`,
- sql`${vendorTbeView.email} ILIKE ${s}`
- )
- }
+ try {
+ // 1) 페이징
+ const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10)
+ const limit = input.perPage ?? 10
+
+ // 2) 고급 필터
+ const advancedWhere = filterColumns({
+ table: vendorTbeView,
+ filters: input.filters ?? [],
+ joinOperator: input.joinOperator ?? "and",
+ })
- // 4) REJECTED 아니거나 NULL
- const notRejected = or(
- ne(vendorTbeView.rfqVendorStatus, "REJECTED"),
- isNull(vendorTbeView.rfqVendorStatus)
+ // 3) 글로벌 검색
+ let globalWhere
+ if (input.search) {
+ const s = `%${input.search}%`
+ globalWhere = or(
+ sql`${vendorTbeView.vendorName} ILIKE ${s}`,
+ sql`${vendorTbeView.vendorCode} ILIKE ${s}`,
+ sql`${vendorTbeView.email} ILIKE ${s}`
)
+ }
- // 5) finalWhere
- const finalWhere = and(
- isNotNull(vendorTbeView.tbeId),
- eq(vendorTbeView.vendorId, vendorId),
- // notRejected,
- advancedWhere,
- globalWhere
- )
+ // 4) REJECTED 아니거나 NULL
+ const notRejected = or(
+ ne(vendorTbeView.rfqVendorStatus, "REJECTED"),
+ isNull(vendorTbeView.rfqVendorStatus)
+ )
+
+ // 5) finalWhere
+ const finalWhere = and(
+ isNotNull(vendorTbeView.tbeId),
+ eq(vendorTbeView.vendorId, vendorId),
+ // notRejected,
+ advancedWhere,
+ globalWhere
+ )
+
+ // 6) 정렬
+ const orderBy = input.sort?.length
+ ? input.sort.map((s) => {
+ const col = (vendorTbeView as any)[s.id]
+ return s.desc ? desc(col) : asc(col)
+ })
+ : [asc(vendorTbeView.vendorId)]
- // 6) 정렬
- const orderBy = input.sort?.length
- ? input.sort.map((s) => {
- const col = (vendorTbeView as any)[s.id]
- return s.desc ? desc(col) : asc(col)
+ // 7) 메인 SELECT - vendor 기준으로 GROUP BY
+ const [rows, total] = await db.transaction(async (tx) => {
+ const data = await tx
+ .select({
+ // 원하는 컬럼들
+ id: vendorTbeView.vendorId,
+ tbeId: vendorTbeView.tbeId,
+ vendorId: vendorTbeView.vendorId,
+ vendorName: vendorTbeView.vendorName,
+ vendorCode: vendorTbeView.vendorCode,
+ address: vendorTbeView.address,
+ country: vendorTbeView.country,
+ email: vendorTbeView.email,
+ website: vendorTbeView.website,
+ vendorStatus: vendorTbeView.vendorStatus,
+
+ rfqId: vendorTbeView.rfqId,
+ rfqCode: vendorTbeView.rfqCode,
+
+ rfqStatus:vendorTbeView.rfqStatus,
+ rfqDescription: vendorTbeView.description,
+ rfqDueDate: vendorTbeView.dueDate,
+
+ projectCode: vendorTbeView.projectCode,
+ projectName: vendorTbeView.projectName,
+ description: vendorTbeView.description,
+ dueDate: vendorTbeView.dueDate,
+
+ vendorResponseId: vendorTbeView.vendorResponseId,
+ rfqVendorStatus: vendorTbeView.rfqVendorStatus,
+ rfqVendorUpdated: vendorTbeView.rfqVendorUpdated,
+
+ tbeResult: vendorTbeView.tbeResult,
+ tbeNote: vendorTbeView.tbeNote,
+ tbeUpdated: vendorTbeView.tbeUpdated,
})
- : [asc(vendorTbeView.vendorId)]
-
- // 7) 메인 SELECT - vendor 기준으로 GROUP BY
- const [rows, total] = await db.transaction(async (tx) => {
- const data = await tx
- .select({
- // 원하는 컬럼들
- id: vendorTbeView.vendorId,
- tbeId: vendorTbeView.tbeId,
- vendorId: vendorTbeView.vendorId,
- vendorName: vendorTbeView.vendorName,
- vendorCode: vendorTbeView.vendorCode,
- address: vendorTbeView.address,
- country: vendorTbeView.country,
- email: vendorTbeView.email,
- website: vendorTbeView.website,
- vendorStatus: vendorTbeView.vendorStatus,
-
- rfqId: vendorTbeView.rfqId,
- rfqCode: vendorTbeView.rfqCode,
-
- rfqStatus:vendorTbeView.rfqStatus,
- rfqDescription: vendorTbeView.description,
- rfqDueDate: vendorTbeView.dueDate,
-
- projectCode: vendorTbeView.projectCode,
- projectName: vendorTbeView.projectName,
- description: vendorTbeView.description,
- dueDate: vendorTbeView.dueDate,
-
- vendorResponseId: vendorTbeView.vendorResponseId,
- rfqVendorStatus: vendorTbeView.rfqVendorStatus,
- rfqVendorUpdated: vendorTbeView.rfqVendorUpdated,
+ .from(vendorTbeView)
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .offset(offset)
+ .limit(limit)
+
+ const [{ count }] = await tx
+ .select({ count: sql<number>`count(*)`.as("count") })
+ .from(vendorTbeView)
+ .where(finalWhere)
+
+ return [data, Number(count)]
+ })
- tbeResult: vendorTbeView.tbeResult,
- tbeNote: vendorTbeView.tbeNote,
- tbeUpdated: vendorTbeView.tbeUpdated,
- })
- .from(vendorTbeView)
- .where(finalWhere)
- .orderBy(...orderBy)
- .offset(offset)
- .limit(limit)
+ if (!rows.length) {
+ return { data: [], pageCount: 0 }
+ }
- const [{ count }] = await tx
- .select({ count: sql<number>`count(*)`.as("count") })
- .from(vendorTbeView)
- .where(finalWhere)
+ // 8) Comments 조회
+ // - evaluationId != null && evalType = "TBE"
+ // - => leftJoin(rfqEvaluations) or innerJoin
+ const distinctVendorIds = [...new Set(rows.map((r) => r.vendorId))]
+ const distinctTbeIds = [...new Set(rows.map((r) => r.tbeId).filter(Boolean))]
- return [data, Number(count)]
+ // (A) 조인 방식
+ const commAll = await db
+ .select({
+ id: rfqComments.id,
+ commentText: rfqComments.commentText,
+ vendorId: rfqComments.vendorId,
+ evaluationId: rfqComments.evaluationId,
+ createdAt: rfqComments.createdAt,
+ commentedBy: rfqComments.commentedBy,
+ evalType: rfqEvaluations.evalType, // (optional)
})
-
- if (!rows.length) {
- return { data: [], pageCount: 0 }
- }
-
- // 8) Comments 조회
- // - evaluationId != null && evalType = "TBE"
- // - => leftJoin(rfqEvaluations) or innerJoin
- const distinctVendorIds = [...new Set(rows.map((r) => r.vendorId))]
- const distinctTbeIds = [...new Set(rows.map((r) => r.tbeId).filter(Boolean))]
-
- // (A) 조인 방식
- const commAll = await db
- .select({
- id: rfqComments.id,
- commentText: rfqComments.commentText,
- vendorId: rfqComments.vendorId,
- evaluationId: rfqComments.evaluationId,
- createdAt: rfqComments.createdAt,
- commentedBy: rfqComments.commentedBy,
- evalType: rfqEvaluations.evalType, // (optional)
- })
- .from(rfqComments)
- // evalType = 'TBE'
- .innerJoin(
- rfqEvaluations,
- and(
- eq(rfqEvaluations.id, rfqComments.evaluationId),
- eq(rfqEvaluations.evalType, "TBE") // ★ TBE만
- )
+ .from(rfqComments)
+ // evalType = 'TBE'
+ .innerJoin(
+ rfqEvaluations,
+ and(
+ eq(rfqEvaluations.id, rfqComments.evaluationId),
+ eq(rfqEvaluations.evalType, "TBE") // ★ TBE만
)
- .where(
- and(
- isNotNull(rfqComments.evaluationId),
- inArray(rfqComments.vendorId, distinctVendorIds)
- )
+ )
+ .where(
+ and(
+ isNotNull(rfqComments.evaluationId),
+ inArray(rfqComments.vendorId, distinctVendorIds)
)
+ )
- // 8-A) vendorId -> comments grouping
- const commByVendorId = new Map<number, any[]>()
- for (const c of commAll) {
- const vid = c.vendorId!
- if (!commByVendorId.has(vid)) {
- commByVendorId.set(vid, [])
- }
- commByVendorId.get(vid)!.push({
- id: c.id,
- commentText: c.commentText,
- vendorId: c.vendorId,
- evaluationId: c.evaluationId,
- createdAt: c.createdAt,
- commentedBy: c.commentedBy,
- })
- }
+ // 8-A) vendorId -> comments grouping
+ const commByVendorId = new Map<number, any[]>()
+ for (const c of commAll) {
+ const vid = c.vendorId!
+ if (!commByVendorId.has(vid)) {
+ commByVendorId.set(vid, [])
+ }
+ commByVendorId.get(vid)!.push({
+ id: c.id,
+ commentText: c.commentText,
+ vendorId: c.vendorId,
+ evaluationId: c.evaluationId,
+ createdAt: c.createdAt,
+ commentedBy: c.commentedBy,
+ })
+ }
- // 9) TBE 템플릿 파일 수 조회
- const templateFiles = await db
- .select({
- tbeId: rfqAttachments.evaluationId,
- fileCount: sql<number>`count(*)`.as("file_count"),
- })
- .from(rfqAttachments)
- .where(
- and(
- inArray(rfqAttachments.evaluationId, distinctTbeIds),
- isNull(rfqAttachments.vendorId),
- isNull(rfqAttachments.commentId)
- )
+ // 9) TBE 템플릿 파일 수 조회
+ const templateFiles = await db
+ .select({
+ tbeId: rfqAttachments.evaluationId,
+ fileCount: sql<number>`count(*)`.as("file_count"),
+ })
+ .from(rfqAttachments)
+ .where(
+ and(
+ inArray(rfqAttachments.evaluationId, distinctTbeIds),
+ isNull(rfqAttachments.vendorId),
+ isNull(rfqAttachments.commentId)
)
- .groupBy(rfqAttachments.evaluationId)
+ )
+ .groupBy(rfqAttachments.evaluationId)
- // tbeId -> fileCount 매핑 - null 체크 추가
- const templateFileCountMap = new Map<number, number>()
- for (const tf of templateFiles) {
- if (tf.tbeId !== null) {
- templateFileCountMap.set(tf.tbeId, Number(tf.fileCount))
- }
+ // tbeId -> fileCount 매핑 - null 체크 추가
+ const templateFileCountMap = new Map<number, number>()
+ for (const tf of templateFiles) {
+ if (tf.tbeId !== null) {
+ templateFileCountMap.set(tf.tbeId, Number(tf.fileCount))
}
+ }
- // 10) TBE 응답 파일 확인 (각 tbeId + vendorId 조합에 대해)
- const tbeResponseFiles = await db
- .select({
- tbeId: rfqAttachments.evaluationId,
- vendorId: rfqAttachments.vendorId,
- responseFileCount: sql<number>`count(*)`.as("response_file_count"),
- })
- .from(rfqAttachments)
- .where(
- and(
- inArray(rfqAttachments.evaluationId, distinctTbeIds),
- inArray(rfqAttachments.vendorId, distinctVendorIds),
- isNull(rfqAttachments.commentId)
- )
+ // 10) TBE 응답 파일 확인 (각 tbeId + vendorId 조합에 대해)
+ const tbeResponseFiles = await db
+ .select({
+ tbeId: rfqAttachments.evaluationId,
+ vendorId: rfqAttachments.vendorId,
+ responseFileCount: sql<number>`count(*)`.as("response_file_count"),
+ })
+ .from(rfqAttachments)
+ .where(
+ and(
+ inArray(rfqAttachments.evaluationId, distinctTbeIds),
+ inArray(rfqAttachments.vendorId, distinctVendorIds),
+ isNull(rfqAttachments.commentId)
)
- .groupBy(rfqAttachments.evaluationId, rfqAttachments.vendorId)
+ )
+ .groupBy(rfqAttachments.evaluationId, rfqAttachments.vendorId)
- // 10-A) TBE 제출 파일 상세 정보 조회 (vendor별로 그룹화)
-
+ // 10-A) TBE 제출 파일 상세 정보 조회 (vendor별로 그룹화)
+
- // tbeId_vendorId -> hasResponse 매핑 - null 체크 추가
- const tbeResponseMap = new Map<string, number>()
- for (const rf of tbeResponseFiles) {
- if (rf.tbeId !== null && rf.vendorId !== null) {
- const key = `${rf.tbeId}_${rf.vendorId}`
- tbeResponseMap.set(key, Number(rf.responseFileCount))
- }
+ // tbeId_vendorId -> hasResponse 매핑 - null 체크 추가
+ const tbeResponseMap = new Map<string, number>()
+ for (const rf of tbeResponseFiles) {
+ if (rf.tbeId !== null && rf.vendorId !== null) {
+ const key = `${rf.tbeId}_${rf.vendorId}`
+ tbeResponseMap.set(key, Number(rf.responseFileCount))
}
+ }
- // 11) 최종 합치기
- const final = rows.map((row) => {
- const tbeId = row.tbeId
- const vendorId = row.vendorId
+ // 11) 최종 합치기
+ const final = rows.map((row) => {
+ const tbeId = row.tbeId
+ const vendorId = row.vendorId
- // 템플릿 파일 수
- const templateFileCount = tbeId !== null ? templateFileCountMap.get(tbeId) || 0 : 0
+ // 템플릿 파일 수
+ const templateFileCount = tbeId !== null ? templateFileCountMap.get(tbeId) || 0 : 0
- // 응답 파일 여부
- const responseKey = tbeId !== null ? `${tbeId}_${vendorId}` : ""
- const responseFileCount = responseKey ? tbeResponseMap.get(responseKey) || 0 : 0
+ // 응답 파일 여부
+ const responseKey = tbeId !== null ? `${tbeId}_${vendorId}` : ""
+ const responseFileCount = responseKey ? tbeResponseMap.get(responseKey) || 0 : 0
- return {
- ...row,
- dueDate: row.dueDate ? new Date(row.dueDate) : null,
- comments: commByVendorId.get(row.vendorId) ?? [],
- templateFileCount, // 추가: 템플릿 파일 수
- hasResponse: responseFileCount > 0, // 추가: 응답 파일 제출 여부
- }
- })
+ return {
+ ...row,
+ dueDate: row.dueDate ? new Date(row.dueDate) : null,
+ comments: commByVendorId.get(row.vendorId) ?? [],
+ templateFileCount, // 추가: 템플릿 파일 수
+ hasResponse: responseFileCount > 0, // 추가: 응답 파일 제출 여부
+ }
+ })
- const pageCount = Math.ceil(total / limit)
- return { data: final, pageCount }
- },
- [JSON.stringify(input), String(vendorId)], // 캐싱 키에 packagesId 추가
- {
- revalidate: 3600,
- tags: [`tbe-vendor-${vendorId}`],
- }
- )()
+ const pageCount = Math.ceil(total / limit)
+ return { data: final, pageCount }
+ } catch (err) {
+ return { data: null, error: getErrorMessage(err) };
+ }
}
export async function inviteTbeVendorsAction(formData: FormData) {
// 캐싱 방지
- unstable_noStore()
-
try {
// 1) FormData에서 기본 필드 추출
const rfqId = Number(formData.get("rfqId"))
@@ -1624,7 +1547,7 @@ export async function inviteTbeVendorsAction(formData: FormData) {
// 5) 각 고유 이메일 주소로 초대 메일 발송
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://3.36.56.124:3000'
- const loginUrl = `${baseUrl}/ko/partners/rfq`
+ const loginUrl = `${baseUrl}/ko/partners/rfq-tech`
console.log(`협력업체 ID ${vendor.id}(${vendor.name})에 대해 ${uniqueEmails.length}개의 고유 이메일로 TBE 초대 발송`)
@@ -1670,12 +1593,6 @@ export async function inviteTbeVendorsAction(formData: FormData) {
}
}
- // 6) 캐시 무효화
- revalidateTag("tbe")
- revalidateTag("vendors")
- revalidateTag(`rfq-${rfqId}`)
- revalidateTag("tbe-vendors")
-
})
// 성공
@@ -1689,7 +1606,6 @@ export async function inviteTbeVendorsAction(formData: FormData) {
export async function modifyRfqVendor(input: UpdateRfqVendorSchema) {
- unstable_noStore();
try {
const data = await db.transaction(async (tx) => {
const [res] = await updateRfqVendor(tx, input.id, {
@@ -1698,9 +1614,6 @@ export async function modifyRfqVendor(input: UpdateRfqVendorSchema) {
return res;
});
- revalidateTag("vendors");
- revalidateTag("tbe");
-
return { data: null, error: null };
} catch (err) {
return { data: null, error: getErrorMessage(err) };
@@ -1768,26 +1681,6 @@ export async function createRfqCommentWithAttachments(params: {
})
}
}
-
- revalidateTag("rfq-vendors");
- revalidateTag(`rfq-${rfqId}`);
-
- if (vendorId) {
- revalidateTag(`vendor-${vendorId}`);
- }
-
- if (evaluationId) {
- revalidateTag("tbe");
- revalidateTag(`tbe-vendor-${vendorId}`);
- revalidateTag("all-tbe-vendors");
- }
-
- if (cbeId) {
- revalidateTag("cbe");
- revalidateTag(`cbe-vendor-${vendorId}`);
- revalidateTag("all-cbe-vendors");
- }
-
return { ok: true, commentId: insertedComment.id, createdAt: insertedComment.createdAt }
}
@@ -1834,9 +1727,6 @@ export async function updateRfqComment(params: {
// 해당 id가 없으면 예외
throw new Error("Comment not found or already deleted.")
}
- revalidateTag("rfq-vendors");
- revalidateTag("tbe");
- revalidateTag("cbe");
return { ok: true }
}
@@ -1902,119 +1792,74 @@ export async function getBidProjects(): Promise<Project[]> {
}
}
+export async function getAllVendors(input?: {
+ page?: number;
+ perPage?: number;
+ search?: string;
+ filters?: any[];
+ sort?: { id: string; desc: boolean }[];
+ joinOperator?: "and" | "or";
+}) {
+ try {
+ const page = input?.page ?? 1;
+ const perPage = input?.perPage ?? 50;
+ const offset = (page - 1) * perPage;
+
+ // 고급 필터
+ const advancedWhere = input?.filters ? filterColumns({
+ table: vendors,
+ filters: input.filters,
+ joinOperator: input.joinOperator ?? "and",
+ }) : undefined;
+
+ // 글로벌 검색
+ let globalWhere;
+ if (input?.search) {
+ const s = `%${input.search}%`;
+ globalWhere = or(
+ ilike(vendors.vendorName, s),
+ ilike(vendors.vendorCode, s),
+ ilike(vendors.email, s),
+ ilike(vendors.country, s),
+ ilike(vendors.phone, s)
+ );
+ }
-// 반환 타입 명시적 정의 - rfqCode가 null일 수 있음을 반영
-export interface BudgetaryRfq {
- id: number;
- rfqCode: string | null; // null 허용으로 변경
- description: string | null;
- projectId: number | null;
- projectCode: string | null;
- projectName: string | null;
-}
+ // 최종 where 조건
+ const finalWhere = and(advancedWhere, globalWhere);
-type GetBudgetaryRfqsResponse =
- | { rfqs: BudgetaryRfq[]; totalCount: number; error?: never }
- | { error: string; rfqs?: never; totalCount: number }
-/**
- * Budgetary 타입의 RFQ 목록을 가져오는 서버 액션
- * Purchase RFQ 생성 시 부모 RFQ로 선택할 수 있도록 함
- * 페이징 및 필터링 기능 포함
- */
-export interface GetBudgetaryRfqsParams {
- search?: string;
- projectId?: number;
- rfqId?: number; // 특정 ID로 단일 RFQ 검색
+ // 정렬
+ const orderBy = input?.sort?.length
+ ? input.sort.map((s) => {
+ const col = (vendors as any)[s.id];
+ return s.desc ? desc(col) : asc(col);
+ })
+ : [asc(vendors.vendorName)]; // 기본 정렬은 vendor 이름
- limit?: number;
- offset?: number;
-}
+ // 데이터 조회
+ const [data, total] = await db.transaction(async (tx) => {
+ const vendorData = await tx
+ .select()
+ .from(vendors)
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .offset(offset)
+ .limit(perPage);
-export async function getBudgetaryRfqs(params: GetBudgetaryRfqsParams = {}): Promise<GetBudgetaryRfqsResponse> {
- const { search, projectId, rfqId, limit = 50, offset = 0 } = params;
- const cacheKey = `rfqs-query-${JSON.stringify(params)}`;
+ const [{ count }] = await tx
+ .select({ count: sql<number>`count(*)`.as("count") })
+ .from(vendors)
+ .where(finalWhere);
- return unstable_cache(
- async () => {
- try {
- // 기본 검색 조건 구성
- let baseCondition;
-
+ return [vendorData, Number(count)];
+ });
-
- // 특정 ID로 검색하는 경우
- if (rfqId) {
- baseCondition = and(baseCondition, eq(rfqs.id, rfqId));
- }
-
- let where1;
- // 검색어 조건 추가 (있을 경우)
- if (search && search.trim()) {
- const searchTerm = `%${search.trim()}%`;
- const searchCondition = or(
- ilike(rfqs.rfqCode, searchTerm),
- ilike(rfqs.description, searchTerm),
- ilike(projects.code, searchTerm),
- ilike(projects.name, searchTerm)
- );
- where1 = searchCondition;
- }
-
- let where2;
- // 프로젝트 ID 조건 추가 (있을 경우)
- if (projectId) {
- where2 = eq(rfqs.projectId, projectId);
- }
-
- const finalWhere = and(baseCondition, where1, where2);
-
- // 총 개수 조회
- const [countResult] = await db
- .select({ count: count() })
- .from(rfqs)
- .leftJoin(projects, eq(rfqs.projectId, projects.id))
- .where(finalWhere);
-
- // 실제 데이터 조회
- const resultRfqs = await db
- .select({
- id: rfqs.id,
- rfqCode: rfqs.rfqCode,
- description: rfqs.description,
- projectId: rfqs.projectId,
- projectCode: projects.code,
- projectName: projects.name,
- })
- .from(rfqs)
- .leftJoin(projects, eq(rfqs.projectId, projects.id))
- .where(finalWhere)
- .orderBy(desc(rfqs.createdAt))
- .limit(limit)
- .offset(offset);
-
- return {
- rfqs: resultRfqs,
- totalCount: Number(countResult?.count) || 0
- };
- } catch (error) {
- console.error("Error fetching RFQs:", error);
- return {
- error: "Failed to fetch RFQs",
- totalCount: 0
- };
- }
- },
- [cacheKey],
- {
- revalidate: 60, // 1분 캐시
- tags: ["rfqs-query"],
- }
- )();
-}
-export async function getAllVendors() {
- // Adjust the query as needed (add WHERE, ORDER, etc.)
- const allVendors = await db.select().from(vendors)
- return allVendors
+ const pageCount = Math.ceil(total / perPage);
+ return { data, pageCount, total };
+ } catch (error) {
+ console.error("Error fetching vendors:", error);
+ return { data: [], pageCount: 0, total: 0 };
+ }
}
@@ -2106,9 +1951,6 @@ export async function addItemToVendors(rfqId: number, vendorIds: number[]) {
insertedCount = recordsToInsert.length;
}
- // 5. Revalidate to refresh data
- revalidateTag("rfq-vendors");
-
// 6. Return success with counts
return {
success: true,
@@ -2259,11 +2101,6 @@ export async function uploadTbeResponseFile(formData: FormData) {
uploadedAt: new Date(),
});
- // 경로 재검증 (캐시된 데이터 새로고침)
- // revalidatePath(`/rfq/${rfqId}/tbe`) // 화면 새로고침 방지를 위해 제거
- revalidateTag(`tbe-vendor-${vendorId}`)
- revalidateTag("all-tbe-vendors")
-
return {
success: true,
message: "파일이 성공적으로 업로드되었습니다."
@@ -2384,8 +2221,6 @@ export async function getTbeFilesForVendor(rfqId: number, vendorId: number) {
}
export async function getAllTBE(input: GetTBESchema) {
- return unstable_cache(
- async () => {
// 1) 페이징
const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10)
const limit = input.perPage ?? 10
@@ -2657,19 +2492,10 @@ export async function getAllTBE(input: GetTBESchema) {
const pageCount = Math.ceil(total / limit)
return { data: final, pageCount }
- },
- [JSON.stringify(input)],
- {
- revalidate: 3600,
- tags: ["tbe"],
- }
- )()
}
export async function getCBE(input: GetCBESchema, rfqId: number) {
- return unstable_cache(
- async () => {
// [1] 페이징
const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10);
const limit = input.perPage ?? 10;
@@ -2933,14 +2759,6 @@ export async function getCBE(input: GetCBESchema, rfqId: number) {
pageCount,
total
};
- },
- // 캐싱 키 & 옵션
- [`cbe-vendors-${rfqId}-${JSON.stringify(input)}`],
- {
- revalidate: 3600,
- tags: [`cbe`, `rfq-${rfqId}`],
- }
- )();
}
export async function generateNextRfqCode(): Promise<{ code: string; error?: string }> {
@@ -3017,11 +2835,6 @@ export async function saveTbeResult({
)
)
- // Revalidate the tbe-vendors tag to refresh the data
- revalidateTag("tbe-vendors")
- revalidateTag("all-tbe-vendors")
-
-
return {
success: true,
message: "TBE evaluation updated successfully",
@@ -3286,7 +3099,7 @@ export async function createCbeEvaluation(formData: FormData) {
incoterms: validData.incoterms,
deliverySchedule: validData.deliverySchedule,
notes: validData.notes,
- loginUrl: `http://${host}/en/partners/cbe`
+ loginUrl: `http://${host}/en/partners/cbe-tech`
}
// 각 고유 이메일 주소로 이메일 발송
@@ -3321,11 +3134,6 @@ export async function createCbeEvaluation(formData: FormData) {
failedVendors.push({ id: vendorId, reason: "예기치 않은 오류" })
}
}
-
- // UI 업데이트를 위한 경로 재검증
- revalidatePath(`/rfq/${rfqId}`)
- revalidateTag(`cbe-vendors-${rfqId}`)
- revalidateTag("all-cbe-vendors")
// 결과 반환
if (createdCbeIds.length === 0) {
@@ -3347,8 +3155,6 @@ export async function createCbeEvaluation(formData: FormData) {
}
export async function getCBEbyVendorId(input: GetCBESchema, vendorId: number) {
- return unstable_cache(
- async () => {
// [1] 페이징
const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10);
const limit = input.perPage ?? 10;
@@ -3613,14 +3419,6 @@ export async function getCBEbyVendorId(input: GetCBESchema, vendorId: number) {
pageCount,
total
};
- },
- // 캐싱 키 & 옵션
- [`cbe-vendor-${vendorId}-${JSON.stringify(input)}`],
- {
- revalidate: 3600,
- tags: [`cbe-vendor-${vendorId}`],
- }
- )();
}
export async function fetchCbeFiles(vendorId: number, rfqId: number) {
@@ -3683,8 +3481,6 @@ export async function fetchCbeFiles(vendorId: number, rfqId: number) {
}
export async function getAllCBE(input: GetCBESchema) {
- return unstable_cache(
- async () => {
// [1] 페이징
const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10);
const limit = input.perPage ?? 10;
@@ -3987,12 +3783,4 @@ export async function getAllCBE(input: GetCBESchema) {
pageCount,
total
};
- },
- // 캐싱 키 & 옵션
- [`all-cbe-vendors-${JSON.stringify(input)}`],
- {
- revalidate: 3600,
- tags: ["all-cbe-vendors"],
- }
- )();
} \ No newline at end of file
diff --git a/lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table-column.tsx b/lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table-column.tsx
index bfcbe75b..f9c5d9df 100644
--- a/lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table-column.tsx
+++ b/lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table-column.tsx
@@ -14,13 +14,11 @@ export interface DataTableRowAction<TData> {
}
interface GetColumnsProps {
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<VendorData> | null>>
setSelectedVendorIds: React.Dispatch<React.SetStateAction<number[]>> // Changed to array
}
/** getColumns: return array of ColumnDef for 'vendors' data */
export function getColumns({
- setRowAction,
setSelectedVendorIds, // Changed parameter name
}: GetColumnsProps): ColumnDef<VendorData>[] {
return [
diff --git a/lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table.tsx b/lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table.tsx
index e34a5052..defbac86 100644
--- a/lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table.tsx
+++ b/lib/rfqs-tech/vendor-table/vendor-list/vendor-list-table.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { ClientDataTable } from "@/components/client-data-table/data-table"
-import { DataTableRowAction, getColumns } from "./vendor-list-table-column"
+import { getColumns } from "./vendor-list-table-column"
import { DataTableAdvancedFilterField } from "@/types/table"
import { addItemToVendors, getAllVendors } from "../../service"
import { Loader2, Plus } from "lucide-react"
@@ -30,27 +30,63 @@ interface VendorsListTableProps {
export function VendorsListTable({ rfqId }: VendorsListTableProps) {
const { toast } = useToast()
- const [rowAction, setRowAction] =
- React.useState<DataTableRowAction<VendorData> | null>(null)
// Changed to array for multiple selection
const [selectedVendorIds, setSelectedVendorIds] = React.useState<number[]>([])
const [isSubmitting, setIsSubmitting] = React.useState(false)
- const columns = React.useMemo(
- () => getColumns({ setRowAction, setSelectedVendorIds }),
- [setRowAction, setSelectedVendorIds]
- )
-
const [vendors, setVendors] = React.useState<VendorData[]>([])
const [isLoading, setIsLoading] = React.useState(false)
+ const columns = React.useMemo(
+ () => getColumns({ setSelectedVendorIds }),
+ [setSelectedVendorIds]
+ )
+
+ // 고급 필터 필드 정의
+ const advancedFilterFields: DataTableAdvancedFilterField<VendorData>[] = [
+ {
+ id: "vendorName",
+ label: "Vendor Name",
+ type: "text",
+ },
+ {
+ id: "vendorCode",
+ label: "Vendor Code",
+ type: "text",
+ },
+ {
+ id: "status",
+ label: "Status",
+ type: "select",
+ options: [
+ { label: "Active", value: "ACTIVE" },
+ { label: "Inactive", value: "INACTIVE" },
+ { label: "Pending", value: "PENDING" },
+ ],
+ },
+ {
+ id: "country",
+ label: "Country",
+ type: "text",
+ },
+ {
+ id: "email",
+ label: "Email",
+ type: "text",
+ },
+ ]
+
+ // 초기 데이터 로드
React.useEffect(() => {
- async function loadAllVendors() {
+ async function loadVendors() {
setIsLoading(true)
try {
- const allVendors = await getAllVendors()
- setVendors(allVendors)
+ const result = await getAllVendors()
+
+ if (result.data) {
+ setVendors(result.data)
+ }
} catch (error) {
console.error("협력업체 목록 로드 오류:", error)
toast({
@@ -62,11 +98,10 @@ export function VendorsListTable({ rfqId }: VendorsListTableProps) {
setIsLoading(false)
}
}
- loadAllVendors()
+
+ loadVendors()
}, [toast])
- const advancedFilterFields: DataTableAdvancedFilterField<VendorData>[] = []
-
async function handleAddVendors() {
if (selectedVendorIds.length === 0) return // Safety check
@@ -100,9 +135,9 @@ export function VendorsListTable({ rfqId }: VendorsListTableProps) {
setIsSubmitting(false)
}
}
-
+
// If loading, show a flex container that fills the parent and centers the spinner
- if (isLoading) {
+ if (isLoading && vendors.length === 0) {
return (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
@@ -110,7 +145,6 @@ export function VendorsListTable({ rfqId }: VendorsListTableProps) {
)
}
- // Otherwise, show the table
return (
<ClientDataTable
data={vendors}