diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-15 14:41:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-15 14:41:01 +0000 |
| commit | 4ee8b24cfadf47452807fa2af801385ed60ab47c (patch) | |
| tree | e1d1fb029f0cf5519c517494bf9a545505c35700 /lib/tbe-last/vendor-tbe-service.ts | |
| parent | 265859d691a01cdcaaf9154f93c38765bc34df06 (diff) | |
(대표님) 작업사항 - rfqLast, tbeLast, pdfTron, userAuth
Diffstat (limited to 'lib/tbe-last/vendor-tbe-service.ts')
| -rw-r--r-- | lib/tbe-last/vendor-tbe-service.ts | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/lib/tbe-last/vendor-tbe-service.ts b/lib/tbe-last/vendor-tbe-service.ts new file mode 100644 index 00000000..8335eb4f --- /dev/null +++ b/lib/tbe-last/vendor-tbe-service.ts @@ -0,0 +1,355 @@ +// lib/vendor-rfq-response/vendor-tbe-service-simplified.ts + +'use server' + +import { unstable_cache } from "next/cache" +import db from "@/db/db" +import { and, desc, asc, eq, sql, or } from "drizzle-orm" +import { tbeLastView, rfqLastTbeSessions } from "@/db/schema" +import { rfqPrItems } from "@/db/schema/rfqLast" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { revalidateTag } from "next/cache" +// ========================================== +// 간단한 벤더 Q&A 타입 정의 +// ========================================== +export interface VendorQuestion { + id: string // UUID + category: "general" | "technical" | "commercial" | "delivery" | "quality" | "document" | "clarification" + question: string + askedAt: string + askedBy: number + askedByName?: string + answer?: string + answeredAt?: string + answeredBy?: number + answeredByName?: string + status: "open" | "answered" | "closed" + priority?: "high" | "normal" | "low" + attachments?: string[] // 파일 경로들 +} + +// ========================================== +// 1. 벤더용 TBE 세션 목록 조회 (기존 뷰 활용) +// ========================================== +export async function getTBEforVendor( + input: any, + vendorId: number +) { + return unstable_cache( + async () => { + const offset = ((input.page ?? 1) - 1) * (input.perPage ?? 10) + const limit = input.perPage ?? 10 + + // 벤더 필터링 + const vendorWhere = eq(tbeLastView.vendorId, vendorId) + + // 데이터 조회 + const [rows, total] = await db.transaction(async (tx) => { + const data = await tx + .select() + .from(tbeLastView) + .where(vendorWhere) + .orderBy(desc(tbeLastView.createdAt)) + .offset(offset) + .limit(limit) + + const [{ count }] = await tx + .select({ count: sql<number>`count(*)`.as("count") }) + .from(tbeLastView) + .where(vendorWhere) + + return [data, Number(count)] + }) + + const pageCount = Math.ceil(total / limit) + return { data: rows, pageCount } + }, + [`vendor-tbe-sessions-${vendorId}`, JSON.stringify(input)], + { + revalidate: 60, + tags: [`vendor-tbe-sessions-${vendorId}`], + } + )() +} + +// ========================================== +// 2. 벤더 질문/코멘트 추가 (기존 필드 활용) +// ========================================== +export async function addVendorQuestion( + sessionId: number, + vendorId: number, + question: Omit<VendorQuestion, "id" | "askedAt"> +) { + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다") + } + + const userId = typeof session.user.id === 'string' ? parseInt(session.user.id) : session.user.id + + // 권한 체크 + const [tbeSession] = await db + .select() + .from(rfqLastTbeSessions) + .where( + and( + eq(rfqLastTbeSessions.id, sessionId), + eq(rfqLastTbeSessions.vendorId, vendorId) + ) + ) + .limit(1) + + if (!tbeSession) { + throw new Error("권한이 없습니다") + } + + // 기존 질문 로그 가져오기 + const existingQuestions = tbeSession.vendorQuestionsLog || [] + + // 새 질문 추가 + const newQuestion: VendorQuestion = { + id: crypto.randomUUID(), + ...question, + askedAt: new Date().toISOString(), + askedBy: userId, + status: "open" + } + + // 업데이트 + const [updated] = await db + .update(rfqLastTbeSessions) + .set({ + vendorQuestionsLog: [...existingQuestions, newQuestion], + vendorRemarks: tbeSession.vendorRemarks + ? `${tbeSession.vendorRemarks}\n\n[${new Date().toLocaleString()}] ${question.question}` + : `[${new Date().toLocaleString()}] ${question.question}`, + updatedAt: new Date(), + updatedBy: userId + }) + .where(eq(rfqLastTbeSessions.id, sessionId)) + .returning() + + // 캐시 무효화 + revalidateTag(`vendor-tbe-sessions-${vendorId}`) + revalidateTag(`tbe-session-${sessionId}`) + + return newQuestion +} + +// ========================================== +// 3. 구매자가 답변 추가 +// ========================================== +export async function answerVendorQuestion( + sessionId: number, + questionId: string, + answer: string +) { + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다") + } + + const userId = typeof session.user.id === 'string' ? parseInt(session.user.id) : session.user.id + + // TBE 세션 조회 + const [tbeSession] = await db + .select() + .from(rfqLastTbeSessions) + .where(eq(rfqLastTbeSessions.id, sessionId)) + .limit(1) + + if (!tbeSession) { + throw new Error("세션을 찾을 수 없습니다") + } + + // 질문 로그 업데이트 + const questions = (tbeSession.vendorQuestionsLog || []) as VendorQuestion[] + const updatedQuestions = questions.map(q => { + if (q.id === questionId) { + return { + ...q, + answer, + answeredAt: new Date().toISOString(), + answeredBy: userId, + status: "answered" as const + } + } + return q + }) + + // 업데이트 + const [updated] = await db + .update(rfqLastTbeSessions) + .set({ + vendorQuestionsLog: updatedQuestions, + updatedAt: new Date(), + updatedBy: userId + }) + .where(eq(rfqLastTbeSessions.id, sessionId)) + .returning() + + // 캐시 무효화 + revalidateTag(`tbe-session-${sessionId}`) + + return updated +} + +// ========================================== +// 4. 벤더 질문 목록 조회 +// ========================================== +export async function getVendorQuestions( + sessionId: number, + vendorId: number +): Promise<VendorQuestion[]> { + // 권한 체크 + const [tbeSession] = await db + .select() + .from(rfqLastTbeSessions) + .where( + and( + eq(rfqLastTbeSessions.id, sessionId), + eq(rfqLastTbeSessions.vendorId, vendorId) + ) + ) + .limit(1) + + if (!tbeSession) { + return [] + } + + return (tbeSession.vendorQuestionsLog || []) as VendorQuestion[] +} + +// ========================================== +// 5. 벤더 의견 업데이트 (간단한 텍스트) +// ========================================== +export async function updateVendorRemarks( + sessionId: number, + vendorId: number, + remarks: string +) { + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다") + } + + const userId = typeof session.user.id === 'string' ? parseInt(session.user.id) : session.user.id + + // 권한 체크 + const [tbeSession] = await db + .select() + .from(rfqLastTbeSessions) + .where( + and( + eq(rfqLastTbeSessions.id, sessionId), + eq(rfqLastTbeSessions.vendorId, vendorId) + ) + ) + .limit(1) + + if (!tbeSession) { + throw new Error("권한이 없습니다") + } + + // 업데이트 + const [updated] = await db + .update(rfqLastTbeSessions) + .set({ + vendorRemarks: remarks, + updatedAt: new Date(), + updatedBy: userId + }) + .where(eq(rfqLastTbeSessions.id, sessionId)) + .returning() + + // 캐시 무효화 + revalidateTag(`vendor-tbe-sessions-${vendorId}`) + revalidateTag(`tbe-session-${sessionId}`) + + return updated +} + +// ========================================== +// 6. 통계 조회 +// ========================================== +export async function getVendorQuestionStats(sessionId: number) { + const [tbeSession] = await db + .select() + .from(rfqLastTbeSessions) + .where(eq(rfqLastTbeSessions.id, sessionId)) + .limit(1) + + if (!tbeSession) { + return { + total: 0, + open: 0, + answered: 0, + closed: 0 + } + } + + const questions = (tbeSession.vendorQuestionsLog || []) as VendorQuestion[] + + return { + total: questions.length, + open: questions.filter(q => q.status === "open").length, + answered: questions.filter(q => q.status === "answered").length, + closed: questions.filter(q => q.status === "closed").length, + highPriority: questions.filter(q => q.priority === "high").length + } +} + + +// ========================================== +// 6. PR 아이템 조회 (벤더용) +// ========================================== +export async function getVendorPrItems( + rfqId: number + ) { + + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + throw new Error("로그인이 필요합니다."); + } + + const vendorId = session.user.companyId + + // RFQ가 해당 벤더의 것인지 체크 + const [tbeSession] = await db + .select() + .from(tbeLastView) + .where( + and( + eq(tbeLastView.rfqId, rfqId), + eq(tbeLastView.vendorId, vendorId) + ) + ) + .limit(1) + + if (!tbeSession) { + return [] + } + + // PR 아이템 조회 + const prItems = await db + .select({ + id: rfqPrItems.id, + prNo: rfqPrItems.prNo, + prItem: rfqPrItems.prItem, + materialCode: rfqPrItems.materialCode, + materialCategory: rfqPrItems.materialCategory, + materialDescription: rfqPrItems.materialDescription, + size: rfqPrItems.size, + quantity: rfqPrItems.quantity, + uom: rfqPrItems.uom, + deliveryDate: rfqPrItems.deliveryDate, + majorYn: rfqPrItems.majorYn, + remarks: rfqPrItems.remark, + }) + .from(rfqPrItems) + .where(eq(rfqPrItems.rfqsLastId, rfqId)) + .orderBy(desc(rfqPrItems.majorYn), asc(rfqPrItems.prItem)) + + return prItems + }
\ No newline at end of file |
