summaryrefslogtreecommitdiff
path: root/lib/tbe-last/vendor-tbe-service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tbe-last/vendor-tbe-service.ts')
-rw-r--r--lib/tbe-last/vendor-tbe-service.ts355
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