// getVendorQuotationsLast.ts 'use server' import { revalidatePath, unstable_cache } from "next/cache"; import db from "@/db/db"; import { and, or, eq, desc, asc, count, ilike, inArray } from "drizzle-orm"; import { rfqsLastView, rfqLastDetails, rfqLastVendorResponses,vendorQuotationView, type RfqsLastView } from "@/db/schema"; import { filterColumns } from "@/lib/filter-columns"; import type { GetQuotationsLastSchema, UpdateParticipationSchema } from "@/lib/rfq-last/vendor-response/validations"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { getRfqAttachmentsAction } from "../service"; export type VendorQuotationStatus = | "미응답" // 초대받았지만 참여 여부 미결정 | "불참" // 참여 거절 | "작성중" // 참여 후 작성중 | "제출완료" // 견적서 제출 완료 | "수정요청" // 구매자가 수정 요청 | "최종확정" // 최종 확정됨 | "취소" // 취소됨 /** * 벤더별 RFQ 목록 조회 */ export async function getVendorQuotationsLast( input: GetQuotationsLastSchema, vendorId: string ) { return unstable_cache( async () => { try { const numericVendorId = parseInt(vendorId); if (isNaN(numericVendorId)) { return { data: [], pageCount: 0 }; } // 페이지네이션 설정 const page = input.page || 1; const perPage = input.perPage || 10; const offset = (page - 1) * perPage; // 필터링 설정 const advancedWhere = filterColumns({ table: vendorQuotationView, filters: input.filters, joinOperator: input.joinOperator, }); // 글로벌 검색 조건 let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(vendorQuotationView.rfqCode, s), ilike(vendorQuotationView.rfqTitle, s), ilike(vendorQuotationView.itemName, s), ilike(vendorQuotationView.projectName, s), ilike(vendorQuotationView.packageName, s), ilike(vendorQuotationView.status, s), ilike(vendorQuotationView.displayStatus, s) ); } // 벤더 ID 조건 (필수) const vendorIdWhere = eq(vendorQuotationView.vendorId, numericVendorId); // 모든 조건 결합 let whereConditions = [vendorIdWhere]; if (advancedWhere) whereConditions.push(advancedWhere); if (globalWhere) whereConditions.push(globalWhere); const finalWhere = and(...whereConditions); // 정렬 설정 const orderBy = input.sort && input.sort.length > 0 ? input.sort.map((item) => { // @ts-ignore return item.desc ? desc(vendorQuotationView[item.id]) : asc(vendorQuotationView[item.id]); }) : [desc(vendorQuotationView.updatedAt)]; // 메인 쿼리 실행 - 이제 한 번의 쿼리로 모든 데이터를 가져옴 const quotations = await db .select() .from(vendorQuotationView) .where(finalWhere) .orderBy(...orderBy) .limit(perPage) .offset(offset); // 전체 개수 조회 const { totalCount } = await db .select({ totalCount: count() }) .from(vendorQuotationView) .where(finalWhere) .then(rows => rows[0]); // 페이지 수 계산 const pageCount = Math.ceil(Number(totalCount) / perPage); return { data: quotations, pageCount }; } catch (err) { console.error("getVendorQuotationsLast 에러:", err); return { data: [], pageCount: 0 }; } }, [`vendor-quotations-last-${vendorId}-${JSON.stringify(input)}`], { revalidate: 60, tags: [`vendor-quotations-last-${vendorId}`], } )(); } export async function getQuotationStatusCountsLast(vendorId: string) { return unstable_cache( async () => { try { const numericVendorId = parseInt(vendorId); if (isNaN(numericVendorId)) { return { "미응답": 0, "불참": 0, "작성중": 0, "제출완료": 0, "수정요청": 0, "최종확정": 0, "취소": 0, } as Record; } // 1. 벤더가 초대받은 전체 RFQ 조회 const invitedRfqs = await db .select({ rfqsLastId: rfqLastDetails.rfqsLastId, }) .from(rfqLastDetails) .where( and( eq(rfqLastDetails.vendorsId, numericVendorId), eq(rfqLastDetails.isLatest, true) ) ); const invitedRfqIds = invitedRfqs.map(r => r.rfqsLastId); const totalInvited = invitedRfqIds.length; // 초대받은 RFQ가 없으면 모두 0 반환 if (totalInvited === 0) { return { "미응답": 0, "불참": 0, "작성중": 0, "제출완료": 0, "수정요청": 0, "최종확정": 0, "취소": 0, } as Record; } // 2. 벤더의 응답 상태 조회 const vendorResponses = await db .select({ participationStatus: rfqLastVendorResponses.participationStatus, status: rfqLastVendorResponses.status, rfqsLastId: rfqLastVendorResponses.rfqsLastId, }) .from(rfqLastVendorResponses) .where( and( eq(rfqLastVendorResponses.vendorId, numericVendorId), eq(rfqLastVendorResponses.isLatest, true), inArray(rfqLastVendorResponses.rfqsLastId, invitedRfqIds) ) ); // 3. 상태별 카운트 계산 const result: Record = { "미응답": 0, "불참": 0, "작성중": 0, "제출완료": 0, "수정요청": 0, "최종확정": 0, "취소": 0, }; // 응답이 있는 RFQ ID 세트 const respondedRfqIds = new Set(vendorResponses.map(r => r.rfqsLastId)); // 미응답 = 초대받았지만 응답 레코드가 없거나 participationStatus가 미응답인 경우 result["미응답"] = totalInvited - respondedRfqIds.size; // 응답별 상태 카운트 vendorResponses.forEach(response => { // 불참한 경우 if (response.participationStatus === "불참") { result["불참"]++; } // 참여했지만 아직 participationStatus가 없는 경우 (기존 데이터 호환성) else if (!response.participationStatus || response.participationStatus === "미응답") { // 응답 레코드는 있지만 참여 여부 미결정 result["미응답"]++; } // 참여한 경우 - status에 따라 분류 else if (response.participationStatus === "참여") { switch (response.status) { case "대기중": case "작성중": result["작성중"]++; break; case "제출완료": result["제출완료"]++; break; case "수정요청": result["수정요청"]++; break; case "최종확정": result["최종확정"]++; break; case "취소": result["취소"]++; break; default: // 기존 상태 호환성 처리 if (response.status === "초대됨") { result["미응답"]++; } else if (response.status === "제출완료" || response.status === "Submitted") { result["제출완료"]++; } break; } } }); return result; } catch (err) { console.error("getQuotationStatusCountsLast 에러:", err); return { "미응답": 0, "불참": 0, "작성중": 0, "제출완료": 0, "수정요청": 0, "최종확정": 0, "취소": 0, } as Record; } }, [`quotation-status-counts-last-${vendorId}`], { revalidate: 60, tags: [`quotation-status-last-${vendorId}`], } )(); } export async function updateParticipationStatus( input: UpdateParticipationSchema ) { try { const session = await getServerSession(authOptions) if (!session?.user?.id) { throw new Error("인증이 필요합니다.") } const vendorId = session.user.companyId; const { rfqId, rfqLastDetailsId, participationStatus, nonParticipationReason } = input // 기존 응답 레코드 찾기 또는 생성 const existingResponse = await db .select() .from(rfqLastVendorResponses) .where( and( eq(rfqLastVendorResponses.rfqsLastId, rfqId), eq(rfqLastVendorResponses.vendorId, Number(vendorId)), eq(rfqLastVendorResponses.rfqLastDetailsId, rfqLastDetailsId), eq(rfqLastVendorResponses.isLatest, true) ) ) .limit(1) const now = new Date() const userId = parseInt(session.user.id) if (existingResponse.length > 0) { // 기존 레코드 업데이트 await db .update(rfqLastVendorResponses) .set({ participationStatus, participationRepliedAt: now, participationRepliedBy: userId, nonParticipationReason: participationStatus === "불참" ? nonParticipationReason : null, status: participationStatus === "참여" ? "작성중" : "대기중", updatedAt: now, updatedBy:userId, }) .where(eq(rfqLastVendorResponses.id, existingResponse[0].id)) } // revalidatePath("/vendor/quotations") return { success: true, message: participationStatus === "참여" ? "견적 참여가 확정되었습니다." : "견적 불참이 처리되었습니다." } } catch (error) { console.error("참여 여부 업데이트 에러:", error) return { success: false, message: "참여 여부 업데이트 중 오류가 발생했습니다." } } } interface UpdateVendorContractRequirementsParams { rfqId: number; detailId: number; contractRequirements: { agreementYn: boolean; ndaYn: boolean; gtcType: "general" | "project" | "none"; }; } interface UpdateResult { success: boolean; error?: string; data?: any; }